# Creating orders

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


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/orders` ([API reference](https://business.onme.com/docs/reference#tag/orders/POST/v1/orders))

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

**cURL**

```bash
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" }
      }
    ]
  }'
```
**Node**

```js
import { randomUUID } from 'node:crypto'

const idempotencyKey = randomUUID()

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': idempotencyKey,
  },
  body: JSON.stringify({
    gifts: [
      {
        amount: 2500,
        category: 'coffee',
        delivery: { method: 'email', email: 'sam@example.com' },
        personalization: { recipient_name: 'Sam', sender_name: 'The Acme team' },
      },
    ],
  }),
})
const order = await response.json()
```
**Python**

```python
import os
import uuid

import requests

idempotency_key = str(uuid.uuid4())

response = requests.post(
    "https://api.onme.com/v1/orders",
    headers={
        "Authorization": f"Bearer {os.environ['ONME_API_KEY']}",
        "Idempotency-Key": idempotency_key,
    },
    json={
        "gifts": [
            {
                "amount": 2500,
                "category": "coffee",
                "delivery": {"method": "email", "email": "sam@example.com"},
                "personalization": {"recipient_name": "Sam", "sender_name": "The Acme team"},
            }
        ]
    },
    timeout=30,
)
order = response.json()
```

What each part does:

- **`Authorization: Bearer $ONME_API_KEY`** identifies your organization. See [Authentication](https://business.onme.com/docs/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](https://business.onme.com/docs/idempotency).
- **`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.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `gifts` | array of objects | Yes | The gifts to create, 1–10. The whole order is funded from your organization balance in one debit. (1–10 items) |
| `gifts[].amount` | integer | Yes | Gift value in cents, between 1000 and 50000. (min 1000, max 50000) |
| `gifts[].category` | string | Yes | ID of the category, as returned by GET /v1/categories. (non-empty) |
| `gifts[].delivery` | one of: email \| phone \| link | Yes |  |
| `gifts[].delivery.method` | string | Yes | (enum: email \| phone \| link) |
| `gifts[].delivery.email` | string | No | Recipient address. Required when method is "email". (format: email) |
| `gifts[].delivery.phone` | string | No | 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[].personalization` | object | No | 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_name` | string | No | (1–200 chars) |
| `gifts[].personalization.sender_name` | string | No | (1–200 chars) |

> **Note: Unknown fields are ignored** We drop fields we don't recognize instead of rejecting them, so a misspelled `personalisation` is silently ignored. Check field names against the table above.

## 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**

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

```js
const response = await fetch('https://api.onme.com/v1/categories', {
  headers: { Authorization: `Bearer ${process.env.ONME_API_KEY}` },
})
const { data: categories } = await response.json()
```
**Python**

```python
import os

import requests

response = requests.get(
    "https://api.onme.com/v1/categories",
    headers={"Authorization": f"Bearer {os.environ['ONME_API_KEY']}"},
    timeout=30,
)
categories = response.json()["data"]
```

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:

**422 Unprocessable Entity**
```json
{
  "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**

```json
"delivery": { "method": "email", "email": "sam@example.com" }
```

**SMS**

```json
"delivery": { "method": "phone", "phone": "+1 415 555 1234" }
```

**Link**

```json
"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](https://business.onme.com/docs/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.

```json
"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**

```bash
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" }
      }
    ]
  }'
```
**Node**

```js
import { randomUUID } from 'node:crypto'

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': randomUUID(),
  },
  body: JSON.stringify({
    gifts: [
      {
        amount: 5000,
        category: 'dinner',
        delivery: { method: 'email', email: 'jordan@example.com' },
        personalization: { recipient_name: 'Jordan' },
      },
    ],
  }),
})
const order = await response.json()
```
**Python**

```python
import os
import uuid

import requests

response = requests.post(
    "https://api.onme.com/v1/orders",
    headers={
        "Authorization": f"Bearer {os.environ['ONME_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "gifts": [
            {
                "amount": 5000,
                "category": "dinner",
                "delivery": {"method": "email", "email": "jordan@example.com"},
                "personalization": {"recipient_name": "Jordan"},
            }
        ]
    },
    timeout=30,
)
order = response.json()
```

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**

```bash
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" } }
    ]
  }'
```
**Node**

```js
import { randomUUID } from 'node:crypto'

const recipients = [
  'ava@example.com', 'ben@example.com', 'chloe@example.com', 'dev@example.com',
  'emma@example.com', 'finn@example.com', 'grace@example.com', 'hugo@example.com',
  'isla@example.com', 'jack@example.com',
]

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': randomUUID(),
  },
  body: JSON.stringify({
    gifts: recipients.map((email) => ({
      amount: 2500,
      category: 'coffee',
      delivery: { method: 'email', email },
    })),
  }),
})
const order = await response.json()

// order.gifts[i] is the gift for recipients[i]
const giftIdsByEmail = Object.fromEntries(
  recipients.map((email, i) => [email, order.gifts[i].gift_id]),
)
```
**Python**

```python
import os
import uuid

import requests

recipients = [
    "ava@example.com", "ben@example.com", "chloe@example.com", "dev@example.com",
    "emma@example.com", "finn@example.com", "grace@example.com", "hugo@example.com",
    "isla@example.com", "jack@example.com",
]

response = requests.post(
    "https://api.onme.com/v1/orders",
    headers={
        "Authorization": f"Bearer {os.environ['ONME_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "gifts": [
            {"amount": 2500, "category": "coffee", "delivery": {"method": "email", "email": email}}
            for email in recipients
        ]
    },
    timeout=30,
)
order = response.json()

# order["gifts"][i] is the gift for recipients[i]
gift_ids_by_email = {email: gift["gift_id"] for email, gift in zip(recipients, order["gifts"])}
```

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.

### A link you share yourself

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**

```bash
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" }
      }
    ]
  }'
```
**Node**

```js
import { randomUUID } from 'node:crypto'

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': randomUUID(),
  },
  body: JSON.stringify({
    gifts: [
      {
        amount: 2000,
        category: 'coffee',
        delivery: { method: 'link' },
        personalization: { recipient_name: 'Riley' },
      },
    ],
  }),
})
const order = await response.json()
const link = order.gifts[0].delivery.link
```
**Python**

```python
import os
import uuid

import requests

response = requests.post(
    "https://api.onme.com/v1/orders",
    headers={
        "Authorization": f"Bearer {os.environ['ONME_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "gifts": [
            {
                "amount": 2000,
                "category": "coffee",
                "delivery": {"method": "link"},
                "personalization": {"recipient_name": "Riley"},
            }
        ]
    },
    timeout=30,
)
order = response.json()
link = order["gifts"][0]["delivery"]["link"]
```

Anyone with the link can claim the gift, so treat it like cash. Links expire; see [Link expiry](https://business.onme.com/docs/delivery-methods#link-expiry).

### Mixed delivery methods

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

**cURL**

```bash
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" }
      }
    ]
  }'
```
**Node**

```js
import { randomUUID } from 'node:crypto'

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': randomUUID(),
  },
  body: JSON.stringify({
    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' },
      },
    ],
  }),
})
const order = await response.json()
```
**Python**

```python
import os
import uuid

import requests

response = requests.post(
    "https://api.onme.com/v1/orders",
    headers={
        "Authorization": f"Bearer {os.environ['ONME_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "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"},
            },
        ]
    },
    timeout=30,
)
order = response.json()
```

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:

**Node**

```js
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()
}
```
**Python**

```python
import os

import requests


def idempotency_key_for(your_order_id: str) -> str:
    return f"order-{your_order_id}"


# reward is your own record, for example {"id": "1234", "email": "sam@example.com", "amount_cents": 2500}
def send_reward(reward: dict) -> dict:
    response = requests.post(
        "https://api.onme.com/v1/orders",
        headers={
            "Authorization": f"Bearer {os.environ['ONME_API_KEY']}",
            "Idempotency-Key": idempotency_key_for(reward["id"]),
        },
        json={
            "gifts": [
                {
                    "amount": reward["amount_cents"],
                    "category": "coffee",
                    "delivery": {"method": "email", "email": reward["email"]},
                }
            ]
        },
        timeout=30,
    )
    return response.json()
```

> **Warning: One key per distinct order** The key must be unique for each order you intend to create. If one of your records can produce more than one On Me order, such as a monthly reward for the same employee, add the distinguishing part to the key: `order-1234-2026-09`. Keep personal data such as emails out of keys.

## Reading the response

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

**201 Created**
```json
{
  "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:

**200 OK, with Idempotent-Replayed: true**
```json
{
  "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}`.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `order_id` | string | Yes |  |
| `status` | string | Yes | 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. (enum: processing \| processed) |
| `gifts` | array of objects | Yes | The order's gifts, in the order you submitted them, so the Nth gift here is the Nth gift you sent. |
| `gifts[].gift_id` | string | Yes |  |
| `gifts[].amount` | integer | Yes | Gift value in cents. |
| `gifts[].category` | string | Yes | ID of the category this gift is for. |
| `gifts[].delivery` | object | Yes |  |
| `gifts[].delivery.method` | string | Yes | (enum: email \| phone \| link) |
| `gifts[].delivery.status` | string | Yes | (enum: sent \| failed \| opened \| claimed) |
| `gifts[].delivery.email` | string | No | Present only when method is "email". |
| `gifts[].delivery.phone` | string | No | Present only when method is "phone". E.164 format. |
| `gifts[].delivery.link` | string | No | Redemption URL. Present only when method is "link". Retrieving a single gift refreshes this if it has expired. |
| `gifts[].personalization` | object | No | 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_name` | string | No |  |
| `gifts[].personalization.sender_name` | string | No |  |

## 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**

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

```js
async function waitUntilProcessed(orderId) {
  const delays = [5, 10, 20, 40, 60, 120, 300]
  for (const seconds of delays) {
    await new Promise((resolve) => setTimeout(resolve, seconds * 1000))
    const response = await fetch(`https://api.onme.com/v1/orders/${orderId}`, {
      headers: { Authorization: `Bearer ${process.env.ONME_API_KEY}` },
    })
    const order = await response.json()
    if (order.status === 'processed') return order
  }
  throw new Error(`Order ${orderId} is still processing; keep checking later`)
}
```
**Python**

```python
import os
import time

import requests


def wait_until_processed(order_id):
    for seconds in [5, 10, 20, 40, 60, 120, 300]:
        time.sleep(seconds)
        order = requests.get(
            f"https://api.onme.com/v1/orders/{order_id}",
            headers={"Authorization": f"Bearer {os.environ['ONME_API_KEY']}"},
            timeout=30,
        ).json()
        if order["status"] == "processed":
            return order
    raise RuntimeError(f"Order {order_id} is still processing; keep checking later")
```

## 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](https://business.onme.com/docs/authentication#authentication-errors):

| Code | HTTP | What happened | What to do |
| --- | --- | --- | --- |
| [`parameter_invalid`](https://business.onme.com/docs/reference#errors/parameter_invalid) | 422 | A 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_balance`](https://business.onme.com/docs/reference#errors/insufficient_balance) | 402 | Your balance can't cover the order total. | Add funds under Billing in the business portal, then resend with the same key. |
| [`idempotency_key_reused`](https://business.onme.com/docs/reference#errors/idempotency_key_reused) | 409 | This 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_progress`](https://business.onme.com/docs/reference#errors/idempotency_in_progress) | 409 | An 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_locked`](https://business.onme.com/docs/reference#errors/resource_locked) | 409 | Another operation briefly held a resource this order needs. | Wait for `Retry-After` and resend with the same key. |
| [`rate_limit_exceeded`](https://business.onme.com/docs/reference#errors/rate_limit_exceeded) | 429 | Per 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_invalid`](https://business.onme.com/docs/reference#errors/request_body_invalid) | 400 | The body isn't valid JSON. | Fix the JSON and resend. The same key is fine. |
| [`request_body_too_large`](https://business.onme.com/docs/reference#errors/request_body_too_large) | 413 | The body is over 1MB. | An order of 10 gifts is far smaller, so check what you're sending, then resend. |
| [`organization_not_configured`](https://business.onme.com/docs/reference#errors/organization_not_configured) | 422 | Your organization has no active admin to attribute orders to. | Ask an admin to set a billing contact in the business portal, then resend. |
| [`internal_error`](https://business.onme.com/docs/reference#errors/internal_error) | 500 | Something 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.

> **Tip: A timeout means retry with the same key** If the request times out, the connection drops, or you get a response without an `error` object (such as a 502 from a proxy), you can't tell whether the order was created. Resend the same body with the same `Idempotency-Key`. You get the existing order if there is one, or a new one if there isn't, never two. [Idempotency and retries](https://business.onme.com/docs/idempotency) has a complete retry loop.

## 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.
