Create a gift request
POST /gift_requests
The gift-request payload and response changed in 2026-05. If you're upgrading an existing integration, read Gift requests in the migration guide before you start.
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 (header) · string · required — 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.
Body
campaign_id (body) · string · required — 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 tenant — an unknown or out-of-tenant ID is rejected with a
validation_error (code invalid, message campaign_id does not exist, no
param), not a 404.
recipient (body) · object · required — 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 (body) · string · requiredandrecipient.last_name (body) · string · required— the recipient's name.recipient.email (body) · string · optional— the recipient's email. Optional: you can create a gift request without one, for example when the redemption flow collects it later.
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, and an
explicit null for either is rejected too.
shipping_address (body) · object · optional — 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 (body) · string · required,shipping_address.address2 (body) · string · optional,shipping_address.city (body) · string · required,shipping_address.region (body) · string · required, andshipping_address.postal_code (body) · string · required— the street address.regionis required even for countries that do not themselves need one, because carrier rate cards sometimes do.shipping_address.country_code (body) · string · required— an ISO 3166-1 alpha-2 code, uppercase. Lowercase is rejected at the boundary (IE, notie) even though the underlying model would normalise it.shipping_address.phone (body) · string · required— a contact number for the carrier, validated server-side against the destination country.
line_items (body) · array · optional — the items to ship, in direct-send mode. At
least one item is required; an empty array is rejected. Each item names a single
enabled tenant variant:
line_items.sku (body) · string · optionalandline_items.variant_id (body) · string · 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 (body) · integer · required— 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. The 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
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",
},
}),
});
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
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.
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 }],
}),
});
const giftRequest = await response.json();
console.log(giftRequest.status, giftRequest.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.
string · required— the public-facing status. It is one ofsubmitted,redeemed,dispatched,delivered,cancelled. A freshly-created request issubmitted; the others are reached as the gift is redeemed, dispatched, and delivered (or cancelled). Poll Get a gift request to follow it.string · uri · required · nullable— the link the recipient follows to redeem.string · uuid · optional · nullable— the campaign this request belongs to. It is a cross-resource reference; fetch the full campaign with Get a campaign.object · required— the recipient, returned inline.object · required · nullable— the shipping address, returned inline in direct-send mode andnullin choice-mode.array · required · nullable— the line items, returned inline once locked.
{
"id": "4fb4cb3f-9666-43b5-8884-7f5194483d1a",
"status": "submitted",
"campaign_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"redemption_url": "https://gift.andopen.co/r/abc123",
"recipient": {
"id": "b2c3d4e5-f6a7-8901-bcde-f01234567890",
"first_name": "Alice",
"last_name": "Smith",
"email": "alice@example.com"
},
"shipping_address": null,
"line_items": null,
"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 — 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".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,status, 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 | Authorised but not permitted to access this resource. |
| 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 tenant 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 but not accepted for a reason specific to your tenant's data, such as acampaign_idthat references no campaign in your tenant (messagecampaign_id does not exist, noparam).invalid_reference— aline_itemsentry'sskuorvariant_idmatches no enabled variant;parampoints at the item, for exampleline_items[0].sku.out_of_stock— aline_itemsvariant has no available stock;parampoints at the offending item.
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"
}
]
}