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.
Find the campaign
Section titled “Find the campaign”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:
brandIdlists the campaigns that serve that brand.status=COMPLETEDlists ended campaigns, to look up the campaign of an older link.
Store the campaign id with your journey or rule. It doesn’t change.
Create a link
Section titled “Create a link”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-linksAuthorization: Bearer lxr_live_…Idempotency-Key: birthday-2026:player-123Content-Type: application/json{ "campaignId": "1e2d3c4b-5a69-4c1f-9a52-8d0e4f4b9a3e", "fullName": "Alice Smith", "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:
fieldIdis the field’sid.- A
TEXTfield takestextValue. ADROPDOWNfield takes one of itsoptionsastextValue. ACHECKBOXfield takeschecked. - A
requiredfield must have an answer. - An answer whose
fieldIddoesn’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.
One link per occasion
Section titled “One link per occasion”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-123ordeposit-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
201and body, withIdempotent-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_reusedafter 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
4xxcode, 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.
When the request is refused
Section titled “When the request is refused”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:
requiredoncampaignIdandfullName, and onbrandIdif the campaign has more than one brand available to your key.invalidoncampaignIdandbrandIdif they aren’t valid IDs.too_longonfullName,email, andmessage.invalidonemailif it isn’t an email address.not_enabledonlocale.invalidoncustomFields[i].fieldIdif it isn’t a UUID, and oncustomFieldsif a required field is missing or an answer doesn’t fit the field.
Approval
Section titled “Approval”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.
Statuses
Section titled “Statuses”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.
Expiry
Section titled “Expiry”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.
Timestamps
Section titled “Timestamps”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.
Read and list links
Section titled “Read and list links”You can read links in two ways:
GET /personal-links/{linkId}reads one link.GET /personal-linkslists links of your key’s brands, newest first. You can filter bybrandId,campaignId, andstatus. 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.
Cancel a link
Section titled “Cancel a 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-Keyis 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 returns200and changes nothing: the first reason stays, and no event is sent. - If the link is
CLAIMEDorEXPIRED, the API returns409 not_cancellablewithcurrentStatus. - If your team or another of your keys created the link, the API returns
409 not_cancellablewithreason: "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.
Claims
Section titled “Claims”When a player claims a link, the following happens:
- The player opens
claimUrl, chooses a gift, and enters an address on the gift site. - Luxorr places the order. Your team follows it in the Luxorr workspace.
- You receive
personal_link.claimed, with the linkCLAIMED. Match it to your player by itscustomFields.
Events
Section titled “Events”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.

