Skip to main content
Version: 2026-05

Create a gift request

POST /gift_requests

Shape changed since 2025-10

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_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 (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 · required and recipient.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, and shipping_address.postal_code (body) · string · required — 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 (body) · string · required — an ISO 3166-1 alpha-2 code, uppercase. Lowercase is rejected at the boundary (IE, not ie) 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 · optional and line_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 a validation_error (out_of_stock), with param pointing at the offending item.
  • line_items.quantity (body) · integer · required — 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. 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 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.
  • 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 and null in 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_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 — 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".
  • 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, status, 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.
403Authorised but not permitted to access this resource.
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 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 a country_code that 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 a campaign_id that references no campaign in your tenant (message campaign_id does not exist, no param).
    • invalid_reference — a line_items entry's sku or variant_id matches no enabled variant; param points 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.
  • 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"
}
]
}