Search

Search guides and the API reference

Sending gifts

Creating orders

Everything about POST /v1/orders, from the request fields to retries, errors and the guarantees behind them.

For AI agents: this page is available as Markdown, and every guide plus the API reference is in llms-full.txt.

An order is one request that sends between 1 and 10 gifts. We debit the total from your organization balance once, create every gift, and start delivering them before we respond. This guide covers the request, the response, and what to do when something goes wrong.

The request

POST/v1/ordersView in API reference

Here's a complete request for one $25 coffee gift sent by email:

IDEMPOTENCY_KEY=$(uuidgen)

curl https://api.onme.com/v1/orders \
  -H "Authorization: Bearer $ONME_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d '{
    "gifts": [
      {
        "amount": 2500,
        "category": "coffee",
        "delivery": { "method": "email", "email": "sam@example.com" },
        "personalization": { "recipient_name": "Sam", "sender_name": "The Acme team" }
      }
    ]
  }'

What each part does:

  • Authorization: Bearer $ONME_API_KEY identifies your organization. See Authentication.
  • Content-Type: application/json is required. Without it the body isn't read, and you get parameter_invalid with no param. Python's json= sets it for you.
  • Idempotency-Key is required on every order. Generate it once per order and store it before you send, so a retry reuses it. See Idempotency and retries.
  • gifts is the list of gifts to create, 1 to 10 of them, all paid for in one debit.
  • amount is the gift value in cents: 2500 is $25.00. It must be between 1000 and 50000.
  • category is a category id from GET /v1/categories. It decides where the recipient can spend the card.
  • delivery says how the gift reaches the recipient: email, phone (SMS) or link.
  • personalization is optional. It holds the names the recipient sees.

Request body

  • giftsarray of objectsrequired
    1–10 items

    The gifts to create, 1–10. The whole order is funded from your organization balance in one debit.

  • gifts[].amountintegerrequired
    min 1000max 50000

    Gift value in cents, between 1000 and 50000.

  • gifts[].categorystringrequired
    non-empty

    ID of the category, as returned by GET /v1/categories.

  • gifts[].deliveryone of: email | phone | linkrequired
  • gifts[].delivery.methodstringrequired
    enum: email | phone | link
  • gifts[].delivery.emailstring
    format: email

    Recipient address. Required when method is "email".

  • gifts[].delivery.phonestring

    Recipient number in international format, such as +14155551234. Required when method is "phone". Spaces, dashes and parentheses are accepted; the number is stored in E.164.

  • gifts[].personalizationobject

    Optional names to show the recipient. sender_name defaults to your organization name; recipient_name is shown only when you supply it.

  • gifts[].personalization.recipient_namestring
    1–200 chars
  • gifts[].personalization.sender_namestring
    1–200 chars

Choosing a category

Each gift is a card for one category, such as coffee or dinner. The category decides where the card works: a coffee card can be spent at the coffee brands in that category, and nowhere else. The recipient doesn't pick a brand in advance; they spend the card at any brand the category covers. Some categories, such as choice, are a gift of choice instead: the recipient picks one of several categories when they claim the gift.

Fetch the catalog with GET /v1/categories. It returns every category in one response, with has_more always false:

curl https://api.onme.com/v1/categories \
  -H "Authorization: Bearer $ONME_API_KEY"

Each category has:

  • id: what you pass as category in an order. IDs are short slugs such as coffee, and some contain spaces, such as board games. Pass them exactly as returned.
  • name and description: a display name and a short line about who the category suits. Use them if you let your own users choose.
  • artwork: the card image URL, or null if the category has none.
  • product_details_url: reserved for a public details page. It may be an empty string, so don't build on it.

The catalog changes rarely. Cache it for a few hours or a day instead of fetching it before every order. If you send a category that no longer exists, the order fails with parameter_invalid and a param such as gifts[0].category, and nothing is charged.

Amounts and limits

  • Amounts are in cents. Send integers: 2500 for $25.00. 25.5 is rejected.
  • Each gift is $10 to $500, so amount must be between 1000 and 50000.
  • An order holds 1 to 10 gifts. Split larger sends into several orders, each with its own Idempotency-Key.
  • One debit per order. We debit exactly the sum of the gift amounts from your organization balance, in one transaction. If the balance can't cover the whole order, you get insufficient_balance and nothing is created or charged.
  • All or nothing. If any gift in the order is invalid, the whole order is rejected and nothing is charged.

When a field fails validation, the error names it in param. We report the first problem we find, so fix it and send again:

422Unprocessable Entity
{
  "error": {
    "type": "invalid_request_error",
    "code": "parameter_invalid",
    "message": "Too big: expected number to be <=50000",
    "param": "gifts[1].amount",
    "doc_url": "https://business.onme.com/docs/reference#errors/parameter_invalid",
    "request_id": "req-DBGtBm16engFigKpuCLGT5GtxtaRQTUaBy51BZkQuSM"
  }
}

gifts[1].amount means the amount of the second gift; indexes start at 0. A validation error doesn't use up your Idempotency-Key, so you can resend the corrected order with the same key.

Delivery

Every gift has a delivery object. Pick one method per gift; one order can mix all three.

Email
"delivery": { "method": "email", "email": "sam@example.com" }
SMS
"delivery": { "method": "phone", "phone": "+1 415 555 1234" }
Link
"delivery": { "method": "link" }

With email and phone, we send the gift to the recipient right away. With link, we send nothing: the response includes delivery.link, a URL you give the recipient yourself. Phone numbers need a country code and are stored in E.164 format, such as +14155551234.

Delivery methods covers what recipients receive, delivery statuses, link expiry and how to fix a failed delivery.

Personalization

personalization sets the names the recipient sees. Both fields are optional, and each is 1 to 200 characters after we trim surrounding whitespace.

  • sender_name is who the gift is from. It defaults to your organization name. It appears in the email's sender name and subject line, and in the text message.
  • recipient_name greets the recipient by name, such as "Hi Sam" in the text message. If you leave it out, we don't invent one.
"personalization": { "recipient_name": "Sam", "sender_name": "The Acme team" }

The response echoes both. sender_name is always present there, set to your organization name when you didn't send one.

Recipes

Each recipe is a complete request. Swap in your own category IDs from GET /v1/categories.

One email gift

A $50 dinner gift for one person:

curl https://api.onme.com/v1/orders \
  -H "Authorization: Bearer $ONME_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "gifts": [
      {
        "amount": 5000,
        "category": "dinner",
        "delivery": { "method": "email", "email": "jordan@example.com" },
        "personalization": { "recipient_name": "Jordan" }
      }
    ]
  }'

In production, generate the key before the request and save it with your record, so a retry sends the same key. The inline $(uuidgen) makes a new key every time the command runs.

A batch of ten

Ten $25 coffee gifts in one order, one debit of $250. This is the most an order can hold.

curl https://api.onme.com/v1/orders \
  -H "Authorization: Bearer $ONME_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "gifts": [
      { "amount": 2500, "category": "coffee", "delivery": { "method": "email", "email": "ava@example.com" } },
      { "amount": 2500, "category": "coffee", "delivery": { "method": "email", "email": "ben@example.com" } },
      { "amount": 2500, "category": "coffee", "delivery": { "method": "email", "email": "chloe@example.com" } },
      { "amount": 2500, "category": "coffee", "delivery": { "method": "email", "email": "dev@example.com" } },
      { "amount": 2500, "category": "coffee", "delivery": { "method": "email", "email": "emma@example.com" } },
      { "amount": 2500, "category": "coffee", "delivery": { "method": "email", "email": "finn@example.com" } },
      { "amount": 2500, "category": "coffee", "delivery": { "method": "email", "email": "grace@example.com" } },
      { "amount": 2500, "category": "coffee", "delivery": { "method": "email", "email": "hugo@example.com" } },
      { "amount": 2500, "category": "coffee", "delivery": { "method": "email", "email": "isla@example.com" } },
      { "amount": 2500, "category": "coffee", "delivery": { "method": "email", "email": "jack@example.com" } }
    ]
  }'

If one address is invalid, the whole order is rejected with a param such as gifts[6].delivery.email. Fix that entry and resend. For more than ten recipients, send several orders, each with its own key.

Use link when you deliver the gift through your own channel, such as your app, a chat message or a printed card. We don't contact the recipient.

curl https://api.onme.com/v1/orders \
  -H "Authorization: Bearer $ONME_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "gifts": [
      {
        "amount": 2000,
        "category": "coffee",
        "delivery": { "method": "link" },
        "personalization": { "recipient_name": "Riley" }
      }
    ]
  }'

Anyone with the link can claim the gift, so treat it like cash. Links expire; see Link expiry.

Mixed delivery methods

One order can use different methods, amounts and categories for each gift:

curl https://api.onme.com/v1/orders \
  -H "Authorization: Bearer $ONME_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "gifts": [
      {
        "amount": 2500,
        "category": "coffee",
        "delivery": { "method": "email", "email": "sam@example.com" }
      },
      {
        "amount": 5000,
        "category": "dinner",
        "delivery": { "method": "phone", "phone": "+1 (415) 555-1234" },
        "personalization": { "recipient_name": "Alex" }
      },
      {
        "amount": 1000,
        "category": "coffee",
        "delivery": { "method": "link" }
      }
    ]
  }'

This debits $85.00 once. Only the third gift comes back with a delivery.link. The second comes back with delivery.phone set to +14155551234.

Idempotency keys from your own order IDs

If your system already has an ID for each order, derive the key from it. A retry after a crash then produces the same key without any extra storage:

export function idempotencyKeyFor(yourOrderId) {
  return `order-${yourOrderId}`
}

// reward is your own record, for example { id: '1234', email: 'sam@example.com', amountCents: 2500 }
export async function sendReward(reward) {
  const response = await fetch('https://api.onme.com/v1/orders', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.ONME_API_KEY}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': idempotencyKeyFor(reward.id),
    },
    body: JSON.stringify({
      gifts: [
        {
          amount: reward.amountCents,
          category: 'coffee',
          delivery: { method: 'email', email: reward.email },
        },
      ],
    }),
  })
  return response.json()
}

Reading the response

A new order comes back as 201 Created with the order and all of its gifts:

201Created
{
  "order_id": "o-zerANhhHtZFvz39EFRa0K0GWpooV-GhnyNgY0s-Btt4",
  "status": "processed",
  "gifts": [
    {
      "gift_id": "g-XLotjYqCLoI9pgf0jKYl5gXsNzxkBANTiq1VyivdeRA",
      "amount": 2500,
      "category": "coffee",
      "delivery": { "method": "email", "status": "sent", "email": "sam@example.com" },
      "personalization": { "sender_name": "Acme" }
    },
    {
      "gift_id": "g-p6wJKWU-3Lwpa7_v1w5_t2rmfYR7jEjCAtTDa3aqjOw",
      "amount": 5000,
      "category": "dinner",
      "delivery": { "method": "phone", "status": "sent", "phone": "+14155551234" },
      "personalization": { "recipient_name": "Alex", "sender_name": "Acme" }
    },
    {
      "gift_id": "g-j9zTmu2Tuq26cc2F6Yisb6C9beQfFd9dhCShe6GXhFc",
      "amount": 1000,
      "category": "coffee",
      "delivery": { "method": "link", "status": "sent", "link": "https://gift.onme.com/Xq7mT2pLk9RwZ4vB" },
      "personalization": { "sender_name": "Acme" }
    }
  ]
}

If you repeat a request with the same Idempotency-Key and the same body, you get the order that key already created, with 200 OK and an Idempotent-Replayed: true header. No second order is created and nothing is charged again:

200OK, with Idempotent-Replayed: true
{
  "order_id": "o-zerANhhHtZFvz39EFRa0K0GWpooV-GhnyNgY0s-Btt4",
  "status": "processed",
  "gifts": [
    {
      "gift_id": "g-XLotjYqCLoI9pgf0jKYl5gXsNzxkBANTiq1VyivdeRA",
      "amount": 2500,
      "category": "coffee",
      "delivery": { "method": "email", "status": "opened", "email": "sam@example.com" },
      "personalization": { "sender_name": "Acme" }
    },
    {
      "gift_id": "g-p6wJKWU-3Lwpa7_v1w5_t2rmfYR7jEjCAtTDa3aqjOw",
      "amount": 5000,
      "category": "dinner",
      "delivery": { "method": "phone", "status": "sent", "phone": "+14155551234" },
      "personalization": { "recipient_name": "Alex", "sender_name": "Acme" }
    },
    {
      "gift_id": "g-j9zTmu2Tuq26cc2F6Yisb6C9beQfFd9dhCShe6GXhFc",
      "amount": 1000,
      "category": "coffee",
      "delivery": { "method": "link", "status": "sent", "link": "https://gift.onme.com/Xq7mT2pLk9RwZ4vB" },
      "personalization": { "sender_name": "Acme" }
    }
  ]
}

A replay shows the order as it is now, so delivery statuses may have moved on since the first response. Treat 200 and 201 the same way: both mean the order exists.

Gifts come back in the same order you sent them, so gifts[i] in the response is the gift you sent at gifts[i] in the request. Map them back to your own records by index, and store each gift_id to look the gift up later with GET /v1/gifts/{gift_id}.

Order object

  • order_idstringrequired
  • statusstringrequired
    enum: processing | processed

    Whether every gift has been created. processed means they all exist. processing means the order is paid for and some gifts are still being created; it happens only if we hit a problem after charging you, and the order finishes on its own. Retrying the original request with the same Idempotency-Key returns it.

  • giftsarray of objectsrequired

    The order's gifts, in the order you submitted them, so the Nth gift here is the Nth gift you sent.

  • gifts[].gift_idstringrequired
  • gifts[].amountintegerrequired

    Gift value in cents.

  • gifts[].categorystringrequired

    ID of the category this gift is for.

  • gifts[].deliveryobjectrequired
  • gifts[].delivery.methodstringrequired
    enum: email | phone | link
  • gifts[].delivery.statusstringrequired
    enum: sent | failed | opened | claimed
  • gifts[].delivery.emailstring

    Present only when method is "email".

  • gifts[].delivery.phonestring

    Present only when method is "phone". E.164 format.

  • gifts[].delivery.linkstring

    Redemption URL. Present only when method is "link". Retrieving a single gift refreshes this if it has expired.

  • gifts[].personalizationobject

    What the recipient sees. sender_name is always present, falling back to your organization name when you do not supply one; recipient_name appears only when you do.

  • gifts[].personalization.recipient_namestring
  • gifts[].personalization.sender_namestring

When the status is processing

status is almost always processed, which means every gift exists. Rarely, it's processing. That means we debited your balance but hit a problem while creating the gifts. The order is paid for, and we finish it on our side; you don't need to do anything to complete it.

While an order is processing, gifts may be empty or incomplete, so don't map gifts by index yet. Instead:

  1. Store the order_id.
  2. Poll GET /v1/orders/{order_id} with backoff (for example after 5, 10, 20 and 40 seconds, then every few minutes) until status is processed.
  3. Read the gifts from that response.

Retrying the original POST with the same key and body also works: it returns the order, and nudges it to finish. Never create a new order to replace a processing one. That sends and charges the gifts twice.

curl https://api.onme.com/v1/orders/o-zerANhhHtZFvz39EFRa0K0GWpooV-GhnyNgY0s-Btt4 \
  -H "Authorization: Bearer $ONME_API_KEY"

Handling errors

An error response has an error object with a code you can branch on. It always means nothing happened: no order, no charge, no gifts. These are the codes POST /v1/orders returns, besides the authentication errors:

CodeHTTPWhat happenedWhat to do
parameter_invalid422A field or header failed validation. param names it, such as gifts[0].delivery.phone or Idempotency-Key.Fix it and resend. The same key is fine.
insufficient_balance402Your balance can't cover the order total.Add funds under Billing in the business portal, then resend with the same key.
idempotency_key_reused409This key already created an order with a different body.Don't retry. For a new order, use a new key. For a retry, send the original body unchanged.
idempotency_in_progress409An earlier request with this key is still running.Wait for Retry-After and resend with the same key. You get that request's result.
resource_locked409Another operation briefly held a resource this order needs.Wait for Retry-After and resend with the same key.
rate_limit_exceeded429Per API key, you sent more than 30 order requests in a minute, or more than 120 requests to the API in a minute.Wait the number of seconds in Retry-After, then resend.
request_body_invalid400The body isn't valid JSON.Fix the JSON and resend. The same key is fine.
request_body_too_large413The body is over 1MB.An order of 10 gifts is far smaller, so check what you're sending, then resend.
organization_not_configured422Your organization has no active admin to attribute orders to.Ask an admin to set a billing contact in the business portal, then resend.
internal_error500Something failed on our side before anything was charged.Resend with the same key after a short wait. If it persists, contact support with the request_id.

Every error also carries a request_id, which matches the Request-Id response header. Log it; support can trace any request from it.

Guarantees

These rules hold for every call to POST /v1/orders, and you can build on them:

  1. Every order has an idempotency key. A request without an Idempotency-Key header is rejected before anything happens.
  2. A 2xx means the order exists. processed means every gift has been created. processing means we've been paid and will finish creating them.
  3. An error response means nothing happened. No order, no debit, no gifts. Retrying with the same key is safe.
  4. No response, or an error without our error object, means the outcome is unknown, and a retry resolves it. Resend with the same key: you get the order if it was created, and nothing is charged twice.
  5. A key belongs to one order. Reusing a key with a different body is rejected with idempotency_key_reused.
  6. Every cent we debit becomes a gift. There are no partial orders and no refunds: once we debit your balance, we finish creating every gift in the order.