Skip to content

Personal links

A personal link lets one player choose one gift from a campaign. You send the link (claimUrl) through your own channel. The player opens it on the Luxorr gift site, chooses a gift, and enters a delivery address. You never receive the address.

Every link is for one campaign. Luxorr sets up campaigns with your team. Each campaign holds the gifts a player can choose from, under one or more of your brands. Your team can copy a campaign’s ID from its page in the Luxorr workspace.

GET /campaigns lists the campaigns your key can send links for, sorted by name. A listed campaign is ACTIVE and not past its expiresAt.

{
"items": [
{
"id": "1e2d3c4b-5a69-4c1f-9a52-8d0e4f4b9a3e",
"name": "VIP birthdays",
"brandIds": ["0a6f2e1c-3b5d-4e7f-8a9b-0c1d2e3f4a5b"],
"status": "ACTIVE",
"expiresAt": "2026-12-31T23:59:59Z"
}
],
"nextCursor": null
}

brandIds lists the campaign’s brands that your key can use. A campaign is listed when your key can use any of them.

You can filter the list:

  • brandId lists the campaigns that serve that brand.
  • status=COMPLETED lists ended campaigns, to look up the campaign of an older link.

Store the campaign id with your journey or rule. It doesn’t change.

POST /personal-links creates one link for one player. The body asks for the same details your team enters when it creates a link in the workspace. Send it with an Idempotency-Key built from the occasion and the player:

POST /v1/personal-links
Authorization: Bearer lxr_live_…
Idempotency-Key: birthday-2026:player-123
Content-Type: application/json
{
"campaignId": "1e2d3c4b-5a69-4c1f-9a52-8d0e4f4b9a3e",
"fullName": "Alice Smith",
"email": "[email protected]",
"message": "Happy birthday from the VIP team!",
"locale": "de-DE",
"customFields": [
{ "fieldId": "5c4b3a29-1807-4f6e-8d5c-4b3a29180746", "textValue": "player-123" }
]
}

The following table describes each field:

Field Rules
campaignId Required. An ACTIVE campaign of your key’s brands.
brandId The brand the link is for: one of the campaign’s brandIds. The claim URL and the gift page show this brand. Required if more than one of the campaign’s brands is available to your key; otherwise that brand is used.
fullName Required. The player’s name, up to 255 characters. Your team sees the link and its order under this name, and the gift page greets the player with it.
email Optional. The player’s email, up to 255 characters. After the player claims the link, Luxorr sends order status updates to it.
message Optional. A personal note on the gift page, up to 255 characters.
locale Optional. The language of the gift page: one of tenant.locales in GET /me. The default is en-US.
customFields Optional. Answers to your own order fields: [{fieldId, textValue, checked}]. A textValue is up to 1,000 characters.

customFields follow tenant.customFields in GET /me:

  • fieldId is the field’s id.
  • A TEXT field takes textValue. A DROPDOWN field takes one of its options as textValue. A CHECKBOX field takes checked.
  • A required field must have an answer.
  • An answer whose fieldId doesn’t match one of your fields is ignored. If your team removes a field, a running journey keeps working.

To match a link to your player, put your player ID or login in one of your fields, such as “Customer ID”. The link returns that answer in every response, in the event feed, and in every webhook.

A link lasts as long as its campaign; its expiresAt shows when it ends. The API ignores fields that the body doesn’t define. A link created through the API has no manager; your team can assign one.

The API returns 201 with the link:

{
"id": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
"campaignId": "1e2d3c4b-5a69-4c1f-9a52-8d0e4f4b9a3e",
"selection": null,
"brandId": "0a6f2e1c-3b5d-4e7f-8a9b-0c1d2e3f4a5b",
"status": "ACTIVE",
"claimUrl": "https://gifts.luxorr.io/c/example-brand/Xk2p9QaB",
"customFields": [
{ "fieldId": "5c4b3a29-1807-4f6e-8d5c-4b3a29180746", "textValue": "player-123", "checked": null },
{ "fieldId": "9e8d7c6b-5a49-4382-a1b0-c9d8e7f6a5b4", "textValue": null, "checked": false }
],
"expiresAt": "2026-12-31T23:59:59Z",
"createdAt": "2026-10-05T09:12:44.512731Z",
"approvedAt": null,
"claimedAt": null,
"cancelledAt": null,
"updatedAt": "2026-10-05T09:12:44.512731Z"
}

customFields has one entry for each field the link captured when it was created, answered or not. A TEXT or DROPDOWN field uses textValue, a CHECKBOX field uses checked, and the other value is null.

The Idempotency-Key keeps a player from getting two links for one occasion:

  • Build it from the occasion and the player, such as birthday-2026:player-123 or deposit-milestone-5:player-123. If the same journey runs again for the same player, it sends the same key.
  • A resend within 7 days returns the first link: the same 201 and body, with Idempotent-Replayed: true. Nothing is created twice. After a timeout, send the same request again. You get the link whether or not the first attempt reached Luxorr.
  • The same key with a different body returns 422 idempotency_key_reused after a link was created under it. The API compares the JSON content: key order and whitespace don’t matter, but a changed value does.
  • A refusal isn’t stored. If the API refuses the link with a 4xx code, fix the request or its cause and send it again with the same key.
  • After 7 days, Luxorr forgets the key, and the same key creates a new link. Keep your own record of which players got a link for an occasion.

For more information, see Idempotency.

If the API refuses the request, nothing is created. The following table lists the refusals:

Response Meaning
400 invalid_request A field is missing or invalid. errors[] names each field with a code.
422 campaign_not_found Your key can’t use this campaign. This also applies to a brandId that the campaign doesn’t serve or that your key can’t use.
422 campaign_not_active The campaign isn’t ready yet, has ended, or is past its expiresAt.
422 idempotency_key_reused A link was already created under this key with a different body.

The codes in errors[] are the following:

  • required on campaignId and fullName, and on brandId if the campaign has more than one brand available to your key.
  • invalid on campaignId and brandId if they aren’t valid IDs.
  • too_long on fullName, email, and message.
  • invalid on email if it isn’t an email address.
  • not_enabled on locale.
  • invalid on customFields[i].fieldId if it isn’t a UUID, and on customFields if a required field is missing or an answer doesn’t fit the field.

If your account requires approval (tenant.approvalRequired in GET /me), a new link starts as REQUESTED. The player can’t claim it until your team approves it in the Luxorr workspace. A key can’t approve links. When the link becomes ACTIVE, you receive personal_link.approved, so send the link to the player then. If your account doesn’t require approval, a link starts as ACTIVE.

The following table describes each link status:

Status Meaning
REQUESTED Waiting for approval. The player can’t claim it.
ACTIVE The player can claim it.
CLAIMED The player chose a gift.
CANCELLED The link was withdrawn. The player sees it as expired.
EXPIRED The link’s campaign ended. The player sees it as expired.

CLAIMED, CANCELLED, and EXPIRED are final.

A link lasts as long as its campaign. Its expiresAt is the campaign’s, or null if the campaign never expires. A REQUESTED or ACTIVE link expires when its campaign ends or passes its expiresAt.

When a link expires, its status changes to EXPIRED, and you receive personal_link.expired. The gift site shows the link as expired as soon as the campaign ends. If the campaign is reopened, an expired link stays expired.

The following table describes each timestamp:

Field Meaning
createdAt When the link was created.
expiresAt When the link expires with its campaign, or null if the campaign never expires.
approvedAt When the link was approved, or null if it started as ACTIVE or is still waiting.
claimedAt When the link was claimed, or null until it’s CLAIMED.
cancelledAt When the link was canceled, or null unless it’s CANCELLED.
updatedAt When the link last changed.

All timestamps are in UTC, with up to six decimal places.

You can read links in two ways:

  • GET /personal-links/{linkId} reads one link.
  • GET /personal-links lists links of your key’s brands, newest first. You can filter by brandId, campaignId, and status. Use the list to browse and to backfill, for example after more than 30 days without reading the event feed.

The list also includes links that your team sends from the workspace, with their own customFields. A link your team sent with gifts picked from the catalog has campaignId: null and selection: "CATALOG". Every other link has selection: null.

To follow changes, read GET /events instead of the list. The feed has every change, in the order the changes happened, so a reader that resumes from its cursor misses nothing. Each event carries the whole link.

To withdraw a REQUESTED or ACTIVE link that your key created, send POST /personal-links/{linkId}:cancel with {"reason": "Player self-excluded"}. The reason is up to 1,000 characters, and only your team can see it.

  • An Idempotency-Key is optional, because a repeat is safe without one. If you send one, a retry with it returns the first answer again.
  • If the link is already CANCELLED, the API returns 200 and changes nothing: the first reason stays, and no event is sent.
  • If the link is CLAIMED or EXPIRED, the API returns 409 not_cancellable with currentStatus.
  • If your team or another of your keys created the link, the API returns 409 not_cancellable with reason: "created_elsewhere". Your key can read the link but not cancel it. Ask your team to cancel it.

To stop a gift that the player already claimed, contact your team or your Luxorr contact.

When a player claims a link, the following happens:

  1. The player opens claimUrl, chooses a gift, and enters an address on the gift site.
  2. Luxorr places the order. Your team follows it in the Luxorr workspace.
  3. You receive personal_link.claimed, with the link CLAIMED. Match it to your player by its customFields.

The following table lists the link events:

Event Sent when
personal_link.created A link is created, by your key or another channel. Your key doesn’t receive webhooks for its own links; they’re in its event feed.
personal_link.approved Your team approved a REQUESTED link.
personal_link.claimed The player chose a gift.
personal_link.cancelled The link was withdrawn.
personal_link.expired The link’s campaign ended. Every key with PERSONAL_LINKS receives it, including the key that created the link.

For more information, see Webhooks and events.