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. Optional: you can create a gift request without one, for example when the redemption flow collects it later.

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 an effective subject and body, supplied as email_message 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.

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

{
"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'
{
"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; 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.
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 · nullable)The link the recipient follows to redeem.
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.
{
"id": "4fb4cb3f-9666-43b5-8884-7f5194483d1a",
"reference": "ACME-M5RD6J8",
"status": "submitted",
"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!"
},
"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 can be null on a just-created request. The link is generated shortly after creation; poll Get a gift request if you need it immediately.
  • 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, 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.
    • invalid: a value is well-formed, and the campaign it names exists, but the campaign's state rules it out. An archived campaign, or one that doesn't support direct send when you sent an address, both 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.
    • 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.
  • 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:

{
"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"
}
]
}