Skip to content
Log in
Guests & Loyalty

Guests & Loyalty

Use these endpoints to register the guest a reservation belongs to, keep their profile current, and read or spend their loyalty points. Each guest belongs to one property and can have a loyalty account that earns on confirmed stays.


POST /guests

Scope: guests:write

Finds a guest by email within the property, or creates one. Calling it again with the same address returns the existing guest rather than creating a duplicate, so it is safe to call on every checkout. An existing guest's name and other fields are not changed; use PATCH /guests/{id} for that. Guests that were merged into another profile are not matched.

Body fieldRequiredNotes
emailyesA valid email address. Used as the match key.
first_nameyes1 to 100 characters.
last_nameyes1 to 100 characters.
phonenoUp to 50 characters.
nationalitynoUp to 100 characters.
curl -X POST "$VRDN_BASE/guests" \
  -H "Authorization: Bearer $VRDN_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "email": "ada@example.com", "first_name": "Ada", "last_name": "Lovelace", "nationality": "GB" }'
{ "guest_id": "3f8a2c1b-9d4e-4c6a-8b7f-5e2d1a9c0b34", "created": true }

Returns 201 with "created": true for a new guest, or 200 with "created": false when an existing guest was linked.


Get a guest

GET /guests/{id}

Scope: guests:read (PII fields additionally require guests:read:pii)

curl "$VRDN_BASE/guests/3f8a2c1b-9d4e-4c6a-8b7f-5e2d1a9c0b34" -H "Authorization: Bearer $VRDN_KEY"

With guests:read only:

{
  "id": "3f8a2c1b-9d4e-4c6a-8b7f-5e2d1a9c0b34",
  "first_name": "Ada",
  "last_name": "Lovelace",
  "nationality": "GB",
  "is_local_verified": false,
  "merged_into_id": null
}

With guests:read:pii, the response also includes email, phone, date_of_birth, id_type, and id_number. An unknown id returns 404 guest_not_found.

FieldTypeNotes
is_local_verifiedbooleanWhether the guest is verified as a local resident (unlocks local-only rates).
merged_into_idstring | nullSet when this profile was merged into another guest. That guest now holds the stays and loyalty, and this record can no longer be updated.

Update a guest

PATCH /guests/{id}

Scope: guests:write

Updates a guest's profile. Send only the fields that change, and at least one. Setting local_verified marks a guest as a verified local resident and records when.

Body fieldNotes
first_name, last_name1 to 100 characters.
phoneUp to 50 characters.
nationalityUp to 100 characters.
local_verifiedBoolean. true unlocks local-only rates for this guest.
curl -X PATCH "$VRDN_BASE/guests/3f8a2c1b-9d4e-4c6a-8b7f-5e2d1a9c0b34" \
  -H "Authorization: Bearer $VRDN_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "local_verified": true }'

Returns the updated guest in the same shape as GET /guests/{id}. A guest that was merged into another returns 409 guest_merged; update the guest named in merged_into_id instead.


Get loyalty standing

GET /guests/{id}/loyalty

Scope: loyalty:read

A guest's points balance, lifetime points, current tier, progress to the next tier, and the recent ledger. Guests with no loyalty activity yet return a zeroed account rather than a 404.

curl "$VRDN_BASE/guests/3f8a2c1b-9d4e-4c6a-8b7f-5e2d1a9c0b34/loyalty" -H "Authorization: Bearer $VRDN_KEY"
{
  "points_balance": 1260,
  "lifetime_points": 1260,
  "tier": "Coral",
  "next_tier": { "name": "Lagoon", "min_points": 5000 },
  "points_to_next_tier": 3740,
  "recent_transactions": [
    { "type": "earn", "points": 1260, "description": "Stay B7XKQ4M2NZ", "created_at": "2026-06-19T08:46:00.000Z" }
  ]
}
FieldTypeNotes
points_balanceintegerSpendable points.
lifetime_pointsintegerCumulative earned points (drives tier; never decremented).
tierstringCurrent tier name.
next_tierobject | nullThe next tier and its threshold, or null at the top tier.
recent_transactionsarrayUp to 10 most-recent ledger entries (earn, redeem, expire, adjust).

Redeem loyalty points

POST /loyalty/redemptions

Scope: loyalty:write

Spends a guest's points and records a redeem ledger entry. The balance is checked in the same step as the debit, so a redemption can never overdraw; redeeming more than the balance returns 409 insufficient_points. The property's plan must include loyalty, or you get 403 loyalty_not_enabled.

Body fieldRequiredNotes
guest_idyesMust belong to this property.
pointsyesPositive integer.
reservation_idnoPays the points toward this stay. Must belong to this property and guest.
curl -X POST "$VRDN_BASE/loyalty/redemptions" \
  -H "Authorization: Bearer $VRDN_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "guest_id": "3f8a2c1b-9d4e-4c6a-8b7f-5e2d1a9c0b34", "points": 1000, "reservation_id": "7d5dc2ea-1f4e-4bfa-9a52-6c3f08b41e27" }'

With reservation_id, the points pay toward that stay: a loyalty discount line is posted to the stay's folio, the program's per-stay cap applies, and the points come back if the stay is cancelled or becomes a no-show. A redemption the program refuses (over the per-stay cap, for example) returns 409 redemption_refused with the reason.

{
  "redeemed_points": 1000,
  "redeemed_value_usd": 10,
  "new_balance": 260,
  "folio_line_amount": "-10.00",
  "folio_line_currency": "USD"
}

Without reservation_id, it is a points-only redemption, and the response returns the guest's tier in place of the folio fields:

{
  "redeemed_points": 1000,
  "redeemed_value_usd": 10,
  "new_balance": 260,
  "tier": "Coral"
}
FieldTypeNotes
redeemed_value_usdnumberThe value of the points in the property's base currency, whatever that currency is. The name is historical.
folio_line_amount / folio_line_currencystringThe discount line posted to the folio, in the folio's currency.

On a points-only redemption, apply redeemed_value_usd as the discount in your own checkout. Points are earned automatically when a stay is confirmed, on its room revenue in the base currency (see Reservations).