Skip to main content
Version: 2026-05

Create a gift request

POST /gift_requests

Description​

Creates a single gift request for a recipient. A gift request is one gifting transaction: you name the campaign and the recipient, and &Open takes care of the rest. The campaign you reference drives everything else: which gifts are offered, how the recipient is invited to redeem, the theme they see, and which of your users is recorded as responsible. In the simplest case you supply just the campaign and the recipient.

When to use​

Reach for this endpoint whenever you want to send one gift to one person. To read a gift request back, to poll its status or fetch the redemption_url, use Get a gift request. To discover which campaigns are available and what each one offers, browse Campaigns.

There are two ways to create a gift request, and the body you send chooses between them:

  • Choice mode: send only campaign_id and recipient. &Open derives the gifts on offer from the campaign's configured variants, and the recipient picks one when they redeem. This is the common case.
  • Direct-send: additionally send shipping_address and line_items to ship specific items straight to a specific address, skipping the choice step. The two fields are required together, and the campaign's invitation method must be sender_provided_address. See Parameters for the exact rules.

Parameters​

Headers​

AndOpen-API-Version (string · required) is the API version this request targets. Always send 2026-05. Requests authenticate with a bearer token; see Authentication for how to present it and Environments for the regional base URL to send it to.

Idempotency-Key (string · optional) makes a retry safe. Send an opaque string of your own, up to 255 characters and carrying no personal data. A repeat within 24 hours returns the original gift request, with an Idempotent-Replayed: true header so you can tell a replay from a fresh create. Reusing a key within that window with a different body returns a 422 with idempotency_key_mismatch. After 24 hours the record may be purged, and the same key then reads as a new request.

Retries are matched even when you send no key. &Open pairs campaign_id with recipient.email for 24 hours, comparing both case-insensitively and ignoring surrounding whitespace. A repeat of that pairing behaves the same way a repeated key does:

  • An identical body returns the original gift request, with Idempotent-Replayed: true, and creates nothing. This is what a double submit gets.
  • A different body is rejected with a 422 and idempotency_conflict. Send an Idempotency-Key when you want a second, deliberately different gift for that same pairing.

A request carrying no recipient.email is matched on nothing, so a key is the only way to make it retry-safe.

Body​

campaign_id (string · uuid · required) is the campaign this gift request belongs to. It's the most consequential field on the request: it drives which gifts are offered and how the gift is sent. It must reference a campaign that belongs to your account. An unknown or out-of-account ID is rejected with a 422 validation_error (code invalid_reference, message does not exist, param campaign_id), not a 404.

recipient (object · required) is who the gift is for. The recipient is an owned association created inline with the gift request; recipients have no endpoint of their own.

  • recipient.first_name (string · required) and recipient.last_name (string · required): the recipient's name.
  • recipient.email (string · email · optional · nullable): the recipient's email. Required when the effective invitation method is email_invitation, because that's where the invitation goes. Otherwise it's optional: you can create a gift request without one, for example when you share the link yourself.

invitation_method (string · optional · nullable) overrides the campaign's configured invitation method for this one send. Omit it to use the campaign's own setting; an explicit null means the same thing. Three values are accepted:

  • email_invitation emails the recipient their redemption link. It needs recipient.email, and an effective body, supplied as email_message.body here or defaulted from the campaign.
  • direct_message_invitation sends no email. You pass on the response's redemption_url yourself, however you like.
  • sender_provided_address is a direct send, and requires the shipping_address and line_items described below on a campaign that supports them.

The first two are recipient-completes flows: the recipient supplies their own address when they redeem. Sending either alongside shipping_address or line_items is rejected, even on a campaign configured for direct send.

email_message (object · optional) is the personal note that goes into the branded invitation email. It applies only when the effective invitation method is email_invitation, whether that came from this request or from the campaign; supplying it in any other case is rejected, as is an explicit null.

  • email_message.subject (string · optional · nullable) overrides the subject line, up to 255 characters.
  • email_message.body (string · optional · nullable) is the message text, up to 1000 characters. &Open wraps it in the branded email, so this is the note itself and not the whole body.

Both fields are plain text, and either can be omitted to fall back to the campaign's default.

notecard_message (string · optional) is the handwritten note the vendor packs with a Marketplace physical gift, up to 150 characters. It's sent verbatim, with no template substitution. Omit it to use the campaign's default note, which Get a campaign returns as default_notecard_message.

Check the campaign's accepts_notecard_message before you send one. A note is accepted only on a choice-mode send from a campaign whose menu has an enabled Marketplace physical product, which is exactly when that field is true. It's rejected on a direct send, and on any other campaign. An explicit null, an empty string, and a whitespace-only value are all rejected too.

notecard_message is independent of email_message: one goes in the box and the other in the invitation email, so a request can carry both.

The next two fields turn a choice-mode request into a direct-send request. Send both or neither. Supplying one without the other is rejected, as is an explicit null for either.

shipping_address (object · optional) is where to ship, in direct-send mode. Note that the shipping label takes the recipient's name from recipient.first_name and recipient.last_name; this object carries no name fields of its own.

  • shipping_address.address1 (string · required), shipping_address.address2 (string · optional · nullable), shipping_address.city (string · required), shipping_address.region (string · required), and shipping_address.postal_code (string · required) make up the street address. region is required even for countries that do not themselves need one, because carrier rate cards sometimes do.
  • shipping_address.country_code (string · required) is an ISO 3166-1 alpha-2 code, uppercase. The API rejects lowercase at the boundary (IE, not ie), even though the underlying model would normalize it.
  • shipping_address.phone (string · required) is a contact number for the carrier, validated server-side against the destination country.

line_items (array · optional) is the items to ship, in direct-send mode. At least one item is required; the API rejects an empty array. Each item names a single enabled variant on your account:

  • line_items.sku (string · optional) and line_items.variant_id (string · uuid · optional) identify the variant. Supply at least one of the two per item; supplying both is accepted only if they resolve to the same variant. A variant that has no available stock fails the request with a validation_error (out_of_stock), with param pointing at the offending item.
  • line_items.quantity (integer · required) is how many of the variant to ship. Must be at least 1.

Request example​

Request body
{
"campaign_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"recipient": {
"first_name": "Alice",
"last_name": "Smith",
"email": "alice@example.com"
}
}

Worked examples​

These examples show a choice-mode request, the common case. Its cURL body is the rendered contract example, so it can't drift from the API; the JavaScript, Python, and Ruby versions are idiomatic equivalents.

Choice mode​

curl -X POST https://api.andopen.co/gift_requests \
-H "Authorization: Bearer <api_key>" \
-H "AndOpen-API-Version: 2026-05" \
-H "Content-Type: application/json" \
-d @- <<'JSON'
Request body
{
"campaign_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"recipient": {
"first_name": "Alice",
"last_name": "Smith",
"email": "alice@example.com"
}
}
JSON

Direct-send​

To ship specific items straight to an address, include shipping_address and line_items; the campaign's invitation method must be sender_provided_address. The shipping label uses the name from recipient, so the address carries no name of its own.

curl -X POST https://api.andopen.co/gift_requests \
-H "Authorization: Bearer <api_key>" \
-H "AndOpen-API-Version: 2026-05" \
-H "Content-Type: application/json" \
-d @- <<'JSON'
{
"campaign_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"recipient": {
"first_name": "Alice",
"last_name": "Smith",
"email": "alice@example.com"
},
"shipping_address": {
"address1": "12 Merrion Square",
"city": "Dublin",
"region": "Leinster",
"postal_code": "D02 XY45",
"country_code": "IE",
"phone": "+353871234567"
},
"line_items": [{ "sku": "HOODIE-L-BLK", "quantity": 1 }]
}
JSON

Response shape​

A successful call returns 201 Created with the new gift request as a flat top-level JSON object. Owned associations are returned inline rather than as references you have to fetch separately.

FieldDescription
reference (string · required)A human-readable handle for the gift request, unique within your account and stable for its lifetime. Show it to your own users and quote it to support; the API itself takes the id.
status (string · required)The public-facing status, one of submitted, redeemed, dispatched, delivered, cancelled. A freshly-created request is submitted, or redeemed for a direct send; the others are reached as the gift is redeemed, dispatched, and delivered, or cancelled. Poll Get a gift request to follow it. delivered is not terminal, as the per-field notes below explain.
cancellable (boolean · required)Whether Cancel a gift request would accept this gift request as it stands. true only while status is submitted.
invitation_method (string · required)How this send reaches the recipient, one of email_invitation, direct_message_invitation, sender_provided_address. Fixed when the gift request is created, from the request's own invitation_method or else the campaign's.
campaign (object · required · nullable)The campaign this request belongs to, embedded as an { id, name, web_url } summary. null for an ad-hoc send that belongs to no campaign. Fetch the rest of the campaign with Get a campaign.
redemption_url (string · uri · required)The link the recipient follows to redeem. Always set, including on this response, so you can share it straight away.
web_url (string · uri · required)The gift request's page in the &Open web app, on your organization's own domain. This is the sender-facing view and needs a signed-in session, so show it to your own users rather than the recipient.
recipient (object · required)The recipient, returned inline.
sender (object · required)The user recorded as responsible for the send, embedded as an { id, name, email } summary. The campaign decides who that is. Read their team and budget with Get a user.
shipping_address (object · required · nullable)The shipping address, returned inline in direct-send mode and null in choice mode.
offering (object · required)What was put in front of the recipient: the items on offer, whether they receive all of them or choose one, and whether they see them at redemption.
line_items (array · required · nullable)The line items, returned inline once locked.
email_message (object · required · nullable)The personal message that went into the invitation email, or null when the send carries none.
invitation_subject (string · required · nullable)The invitation email's subject line, or null when the send carries no invitation email.
notecard_message (string · required · nullable)The note packed with a Marketplace physical gift, or null when the send carries none.
lifecycle_events (object · required)When each step of the gift request happened. On a new gift request every timestamp is null, except redeemed_at on a direct send; see Get a gift request for what each one records.
Response · 201
{
"id": "4fb4cb3f-9666-43b5-8884-7f5194483d1a",
"reference": "ACME-M5RD6J8",
"status": "submitted",
"cancellable": true,
"invitation_method": "email_invitation",
"campaign": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Q4 Customer Appreciation",
"web_url": "https://acme.andopen.co/sender/gift-history/campaigns/a1b2c3d4-e5f6-7890-abcd-ef1234567890"
},
"redemption_url": "https://gift.andopen.co/r/abc123",
"web_url": "https://acme.andopen.co/sender/gift-history/4fb4cb3f-9666-43b5-8884-7f5194483d1a",
"recipient": {
"id": "b2c3d4e5-f6a7-8901-bcde-f01234567890",
"thank_you_count": 0,
"first_name": "Alice",
"last_name": "Smith",
"name": "Alice Smith",
"email": "alice@example.com"
},
"sender": {
"id": "9f8e7d6c-5b4a-3210-fedc-ba9876543210",
"name": "Dana Okafor",
"email": "dana@acme.example"
},
"shipping_address": null,
"offering": {
"selection_mode": "one",
"visible_at_redemption": true,
"line_items": [
{
"id": "d4e5f6a7-b8c9-0123-4567-890abcdef012",
"sku": "HOODIE-L-BLK",
"variant_id": "c3d4e5f6-a7b8-9012-cdef-012345678901",
"name": "Hoodie: Large / Black",
"quantity": 1,
"images": [
{
"name": "hoodie-black.jpg",
"url": "https://files.andopen.co/8k2mfp4qz7x9v1c3b5n0jhtl",
"urls": {
"thumbnail": "https://files.andopen.co/p3wq7m2zx8k4n6v0bjhtldsf",
"large": "https://files.andopen.co/r9td5c1yfw3jq7n2vk8mzbxh",
"large_wide": "https://files.andopen.co/h4nj8sv2qd6kz0wt3mfy9lpc"
}
}
]
},
{
"id": "f6a7b8c9-d0e1-2345-6789-0abcdef01234",
"sku": "NOTEBOOK-A5",
"variant_id": "a7b8c9d0-e1f2-3456-789a-bcdef0123456",
"name": "Leather Notebook",
"quantity": 1,
"images": [
{
"name": "leather-notebook.jpg",
"url": "https://files.andopen.co/rndd63mwgj94j2vf3v2ngnye0hkn",
"urls": {
"thumbnail": "https://files.andopen.co/vw2mvwevyqxh98l7esf67nvunr4h",
"large": "https://files.andopen.co/mz4rc8kx1nvq7j93wtf5bd2hs0yl",
"large_wide": "https://files.andopen.co/q7pz3n8htv2ke0d5rjw94flxs6cb"
}
}
]
}
]
},
"line_items": null,
"email_message": {
"subject": "A small thank-you",
"body": "Thanks for being a great customer!"
},
"invitation_subject": "A small thank-you",
"notecard_message": "Congratulations on your promotion!",
"lifecycle_events": {
"invitation_email_sent_at": null,
"invitation_first_opened_at": null,
"invitation_last_opened_at": null,
"reminder_sent_at": null,
"redeemed_at": null,
"dispatched_at": null,
"delivered_at": null
},
"created_at": "2026-04-16T10:30:00.000Z",
"updated_at": "2026-04-16T10:30:00.000Z"
}

Per-field notes​

  • shipping_address and line_items mirror the creation mode. In choice mode both come back null; in direct-send both are populated. They are always present as keys, so read their value, not their presence.
  • line_items is null, never [], before items are locked. For direct-send the items lock at creation, so line_items is populated from the start. For choice mode they lock at redemption, so the field stays null until the recipient chooses. Treat null as "not yet locked", not "no items".
  • delivered covers partial fulfillment, and it is not terminal. It means the gift reached the recipient, including a request whose remaining items failed or were cancelled, and the status can change again while a remaining item is retried. Don't treat it as the end of the lifecycle or stop polling on it.
  • offering.line_items and line_items hold different sets, and never the same item twice. Before redemption offering.line_items carries what the recipient was shown and line_items is null; after redemption line_items carries what they chose and offering.line_items carries what they left. Voucher, cause, and gift-set offerings are not represented yet and come back null.
  • offering.line_items is what was offered at send time, not a stock guarantee. Availability is re-checked when the recipient opens their link, so an item listed here can be gone by the time they choose. Packaging is excluded.
  • redemption_url is set in the create response. With direct_message_invitation &Open sends no invitation, so this is the link you pass on to the recipient.
  • notecard_message comes back as the note this send carries: yours, or the campaign's default when you sent none.
  • Server-set fields are read-only. id, reference, status, sender, timestamps, and the association ids are assigned by &Open and ignored if sent on the request.

Error cases​

Every failure uses the shared error model: a top-level errors array of objects carrying type, code, message, and (for field-level problems) param. See Errors for the full type/code taxonomy and how to handle each category; the codes below are the ones this endpoint produces.

StatusMeaning
400The request is malformed or violates a precondition. Typical triggers are a missing or unsupported `AndOpen-API-Version` header, a request body that is not valid JSON, or pagination parameters that fail server-side validation (`limit` non-numeric or non-positive, `after` referencing a record that does not exist).
401Authentication failed: missing or invalid bearer token.
403Authorized but not permitted to access this resource.
409An identical request under the same idempotency key is still being processed. Retry after the interval in the Retry-After header.
422Validation error: the request was parseable but contains invalid data. All validation errors are returned at once.
429Too many requests. Rate limit exceeded.
500Internal server error.
503Service unavailable. The account may be under maintenance. Check the `Retry-After` header.

Every error carries a type, one of: validation_error, authentication_error, authorization_error, not_found_error, rate_limit_error, api_error.

Most failures on this endpoint are validation errors:

  • 422 validation_error: the body was understood but a value was not accepted. The common codes here:

    • required: a required field (campaign_id, recipient.first_name, recipient.last_name, recipient.email for an email invitation, or, in direct-send, the address and item fields) was missing or empty.
    • invalid_format: a value is the right kind of thing but malformed, such as a country_code that is not two uppercase letters, or a notecard_message that is null, not a string, blank, or longer than 150 characters.
    • invalid: a value is well-formed, and the campaign it names exists, but the campaign's state rules it out. An archived campaign, one that doesn't support direct send when you sent an address, and a notecard_message on a direct send or on a campaign whose accepts_notecard_message is false all land here.
    • invalid_reference: a well-formed id matches no record you can reach. A campaign_id that references no campaign in your account returns this with param campaign_id and message does not exist. A line_items entry whose sku or variant_id matches no enabled variant returns it with param pointing at the item, for example line_items[0].sku.
    • out_of_stock: a line_items variant has no available stock; param points at the offending item.
    • insufficient_budget: the send would cost more than the sender's budget has left. The message states the send's cost against the budget, which includes any handling and shipping the budget counts, and what remains. An admin can raise it from the Users page, then you can retry the same request.
    • budget_unavailable: &Open couldn't check the gift's cost against the sender's budget. Nothing was created, and you can retry the same request.
    • no_payment_method: the gift would be charged to a card, and the sender's team has no payment method on file. An admin can add one in the team's settings, from the Teams page, then you can retry the same request.
    • shallow_voucher_pool: the campaign offers a prepaid voucher whose pool has no codes left for this send. The message names the voucher. Add codes to the pool, then you can retry the same request.
    • idempotency_conflict: this campaign_id and recipient.email pairing was used by a different request body in the last 24 hours. Send an Idempotency-Key to create a second, different gift for that recipient.
    • idempotency_key_mismatch: the Idempotency-Key you sent was used in the last 24 hours with a different body. Use a fresh key, or resend the original body to get the original result back.

    The budget, payment-method, and voucher-pool refusals depend on your account's state, not on the request. &Open doesn't store them against your Idempotency-Key, so once you've fixed the cause, you can retry the same request with the same key.

  • 409 api_error: request_in_progress. An identical request is still being processed, so this one was neither run nor rejected. Wait the number of seconds in the Retry-After header and send it again.

  • 400 api_error: the request could not be processed at all: malformed_request for a body that is not valid JSON, or unsupported_api_version when the AndOpen-API-Version header is missing or unsupported (returned before authentication).

Because all validation problems are returned together, a single response can carry several objects; surface them against the right field using param:

Response · 422
{
"errors": [
{
"type": "validation_error",
"code": "required",
"message": "is required",
"param": "recipient.last_name"
},
{
"type": "validation_error",
"code": "invalid_format",
"message": "must be an ISO 3166-1 alpha-2 country code in uppercase",
"param": "shipping_address.country_code"
}
]
}