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_idandrecipient. &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_addressandline_itemsto 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 besender_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
422andidempotency_conflict. Send anIdempotency-Keywhen 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) andrecipient.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_invitationemails the recipient their redemption link. It needs an effective subject and body, supplied asemail_messagehere or defaulted from the campaign.direct_message_invitationsends no email. You pass on the response'sredemption_urlyourself, however you like.sender_provided_addressis a direct send, and requires theshipping_addressandline_itemsdescribed 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), andshipping_address.postal_code(string · required) make up the street address.regionis 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, notie), 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) andline_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 avalidation_error(out_of_stock), withparampointing at the offending item.line_items.quantity(integer · required) is how many of the variant to ship. Must be at least1.
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
- JavaScript
- Python
- Ruby
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
const response = await fetch("https://api.andopen.co/gift_requests", {
method: "POST",
headers: {
Authorization: "Bearer <api_key>",
"AndOpen-API-Version": "2026-05",
"Content-Type": "application/json",
},
body: JSON.stringify({
campaign_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
recipient: {
first_name: "Alice",
last_name: "Smith",
email: "alice@example.com",
},
}),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const giftRequest = await response.json();
console.log(giftRequest.status, giftRequest.redemption_url);
import requests
response = requests.post(
"https://api.andopen.co/gift_requests",
headers={
"Authorization": "Bearer <api_key>",
"AndOpen-API-Version": "2026-05",
},
json={
"campaign_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"recipient": {
"first_name": "Alice",
"last_name": "Smith",
"email": "alice@example.com",
},
},
)
response.raise_for_status()
gift_request = response.json()
print(gift_request["status"], gift_request["redemption_url"])
require "net/http"
require "json"
uri = URI("https://api.andopen.co/gift_requests")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer <api_key>"
request["AndOpen-API-Version"] = "2026-05"
request["Content-Type"] = "application/json"
request.body = {
campaign_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
recipient: {
first_name: "Alice",
last_name: "Smith",
email: "alice@example.com"
}
}.to_json
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request(request)
end
response.value # raises on a non-2xx response
gift_request = JSON.parse(response.body)
puts "#{gift_request['status']} #{gift_request['redemption_url']}"
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
- JavaScript
- Python
- Ruby
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
const response = await fetch("https://api.andopen.co/gift_requests", {
method: "POST",
headers: {
Authorization: "Bearer <api_key>",
"AndOpen-API-Version": "2026-05",
"Content-Type": "application/json",
},
body: JSON.stringify({
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 }],
}),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const giftRequest = await response.json();
console.log(giftRequest.status, giftRequest.shipping_address.city);
import requests
response = requests.post(
"https://api.andopen.co/gift_requests",
headers={
"Authorization": "Bearer <api_key>",
"AndOpen-API-Version": "2026-05",
},
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}],
},
)
response.raise_for_status()
gift_request = response.json()
print(gift_request["status"], gift_request["shipping_address"]["city"])
require "net/http"
require "json"
uri = URI("https://api.andopen.co/gift_requests")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer <api_key>"
request["AndOpen-API-Version"] = "2026-05"
request["Content-Type"] = "application/json"
request.body = {
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 }]
}.to_json
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request(request)
end
response.value # raises on a non-2xx response
gift_request = JSON.parse(response.body)
puts "#{gift_request['status']} #{gift_request['shipping_address']['city']}"
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.
| Field | Description |
|---|---|
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_addressandline_itemsmirror the creation mode. In choice mode both come backnull; in direct-send both are populated. They are always present as keys, so read their value, not their presence.line_itemsisnull, never[], before items are locked. For direct-send the items lock at creation, soline_itemsis populated from the start. For choice mode they lock at redemption, so the field staysnulluntil the recipient chooses. Treatnullas "not yet locked", not "no items".deliveredcovers 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_itemsandline_itemshold different sets, and never the same item twice. Before redemptionoffering.line_itemscarries what the recipient was shown andline_itemsisnull; after redemptionline_itemscarries what they chose andoffering.line_itemscarries what they left. Voucher, cause, and gift-set offerings are not represented yet and come backnull.offering.line_itemsis 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_urlcan benullon 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 associationids 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.
| Status | Meaning |
|---|---|
| 400 | The 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). |
| 401 | Authentication failed: missing or invalid bearer token. |
| 403 | Authorized but not permitted to access this resource. |
| 409 | An identical request under the same idempotency key is still being processed. Retry after the interval in the Retry-After header. |
| 422 | Validation error: the request was parseable but contains invalid data. All validation errors are returned at once. |
| 429 | Too many requests. Rate limit exceeded. |
| 500 | Internal server error. |
| 503 | Service 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 acountry_codethat 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. Acampaign_idthat references no campaign in your account returns this withparamcampaign_idand messagedoes not exist. Aline_itemsentry whoseskuorvariant_idmatches no enabled variant returns it withparampointing at the item, for exampleline_items[0].sku.out_of_stock: aline_itemsvariant has no available stock;parampoints at the offending item.idempotency_conflict: thiscampaign_idandrecipient.emailpairing was used by a different request body in the last 24 hours. Send anIdempotency-Keyto create a second, different gift for that recipient.idempotency_key_mismatch: theIdempotency-Keyyou 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 theRetry-Afterheader and send it again.400 api_error: the request could not be processed at all:malformed_requestfor a body that is not valid JSON, orunsupported_api_versionwhen theAndOpen-API-Versionheader 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"
}
]
}