# On Me API > Buy and send On Me digital gifts from your own systems. REST over HTTPS, JSON bodies, bearer-token auth. ## Rules for agents - Every `POST /v1/orders` needs a unique `Idempotency-Key` header (a UUID, or a value derived from your own order ID). - Amounts are integer cents: 1000 is $10.00. Each gift is 1000–50000; an order has 1–10 gifts. - On a timeout, dropped connection or a response without an `error` object, retry with the SAME Idempotency-Key. - An error response with an `error` object means nothing happened. Retry only when the code's `retry` is `yes`, or `after_fix` once fixed. Never retry `retry: no`. - Branch on `error.type` and `error.code`, never `error.message`. - The API is in beta: any part of it, including request and response bodies and endpoints, can change at any time. Report problems to support@onme.com. - Ignore unknown response fields; new ones are added without notice. - Never put API keys in browser or mobile code. --- # Overview > What the On Me API does, how a gift moves from order to recipient, and the core concepts every integration uses. The On Me API lets your platform buy and send On Me gifts from your own code. You pick a category, choose an amount and a way to deliver it, and we take care of the rest: the gift reaches your recipient, they claim it, and they get a virtual card they can spend at that category's merchants. Every gift is paid for from your organization's On Me balance, so there are no cards to charge and no checkout to build. > **Warning: The API is in beta** The On Me API is still in beta, so any part of it can change at any time, including but not limited to request and response bodies, endpoints, headers and error codes. If something breaks or doesn't behave the way you expect, email [support@onme.com](mailto:support@onme.com). ## What you can do - **Browse the catalog.** List every gift category with `GET /v1/categories`. - **Send gifts.** Create an order of 1 to 10 gifts with `POST /v1/orders`. The whole order is paid for in one debit from your balance. - **Deliver them your way.** We can email or text each gift for you, or hand you a link to share however you like. - **Track what happens next.** Read orders and gifts back to see whether each one was sent, opened, claimed or failed. - **Fix a failed delivery.** Point an undelivered email or text gift at a new address with `PUT /v1/gifts/{gift_id}/delivery`. ## Core concepts - **Category**: what the gift is for, such as Coffee or Dining. The category decides where the recipient's card can be spent. You pass its `id` when you create a gift. Some categories, like Choice, let the recipient pick a category when they claim. - **Order**: one purchase of 1 to 10 gifts, created with a single request and paid for with a single debit from your balance. Order IDs look like `o-…`. - **Gift**: one gift for one recipient, with an amount, a category, a delivery method and, optionally, the names the recipient sees. Gift IDs look like `g-…`. Every gift belongs to exactly one order. - **Recipient**: the person who receives the gift. They don't need an On Me account to receive it; they sign in or create one when they claim it. - **Balance**: your organization's prepaid funds. You top it up in the business portal under **Billing**, and each order draws it down by the order's total. ## How a gift moves Fund balance → POST /v1/orders → Gifts created → Delivered (email, SMS or link) → Recipient claims → Card issued. Delivery status moves sent → opened → claimed, or failed. **Fund your balance.** Add funds in the [business portal](https://business.onme.com/billing). Your balance must cover an order's full total before we accept it. **Create an order.** `POST /v1/orders` debits your balance and creates every gift in the order before it responds. Either the whole order goes through, or nothing happens. In rare cases we hit a problem after the debit: the order then comes back as `processing`, possibly with `gifts: []`, and its gifts are finished shortly after. See [when the status is processing](https://business.onme.com/docs/creating-orders#when-the-status-is-processing). **We deliver each gift.** Email and SMS gifts go out straight away. Link gifts come back with a `delivery.link` URL for you to share. **The recipient opens and claims it.** They open the gift, see who it's from, and claim it to their On Me account. **We issue the card.** Claiming issues a virtual card that works at the category's merchants, or adds the funds to a card they already hold for that category. You can follow each gift through `delivery.status`: | `delivery.status` | What it means | | --- | --- | | `sent` | We sent the email or text, or created the link. This is where every gift starts. | | `opened` | The recipient opened the gift. | | `claimed` | The recipient claimed the gift and has a card to spend it on. | | `failed` | We couldn't deliver the email or text. You can [send it somewhere else](https://business.onme.com/docs/delivery-methods). | The status always shows the furthest point a gift has reached. A gift that failed, was re-sent and then claimed reads `claimed`. ## Requests and responses Every request goes to `https://api.onme.com`, over HTTPS, and every path starts with `/v1`. Send request bodies as JSON with `Content-Type: application/json`; bodies are limited to 1 MB. Responses are always JSON. Responses come in three shapes. There is no wrapper around them: what you see below is the entire body. A single resource comes back as the object itself: **200 GET /v1/orders/{order_id}** ```json { "order_id": "o-bc1KeWwb90nw_0Jfspeu_oNs0EM7XcAKLQKS64QdQIo", "status": "processed", "gifts": [ { "gift_id": "g-Q5-Vef_a-vnAqGgTVfBArnb8sjHOBCjz7Z7nm4Udlww", "amount": 2500, "category": "coffee", "delivery": { "method": "email", "status": "sent", "email": "sam@example.com" }, "personalization": { "recipient_name": "Sam", "sender_name": "Acme" } } ] } ``` A list comes back in a list object, with the items in `data`: **200 GET /v1/gifts?limit=1** ```json { "object": "list", "data": [ { "gift_id": "g-Q5-Vef_a-vnAqGgTVfBArnb8sjHOBCjz7Z7nm4Udlww", "amount": 2500, "category": "coffee", "delivery": { "method": "email", "status": "opened", "email": "sam@example.com" }, "personalization": { "recipient_name": "Sam", "sender_name": "Acme" } } ], "has_more": true } ``` An error comes back as a single `error` object, with a non-2xx status: **422 Unprocessable Entity** ```json { "error": { "type": "invalid_request_error", "code": "parameter_invalid", "message": "Invalid email address", "param": "gifts[0].delivery.email", "doc_url": "https://business.onme.com/docs/reference#errors/parameter_invalid", "request_id": "req-C4451Fpu_emD-fIdoqAQ8dKd9HqnoBiJUf-EvRdz0qM" } } ``` Branch on `type` and `code`, never on `message`: the message is written for people and can change. When the error is about one field, `param` names it. Every code has its own entry in the [error reference](https://business.onme.com/docs/reference#errors), which `doc_url` links to directly. > **Tip: An error body means nothing happened** If you get a response with an `error` object, we made no changes: no order, no charge, no gifts. That holds for every error code, including `internal_error`. If you get no response at all, or a gateway error without an `error` object, the outcome is unknown. Retry `POST /v1/orders` with the same `Idempotency-Key` and you get the original result. [Idempotency and retries](https://business.onme.com/docs/idempotency) covers this in full. ## Conventions ### Amounts Amounts are integers in US cents. `2500` is $25.00. A gift can be worth between `1000` ($10) and `50000` ($500). ### IDs IDs are opaque strings with a prefix that tells you what they are: | Prefix | Identifies | | --- | --- | | `o-` | An order | | `g-` | A gift | | `req-` | A single API request | Treat everything after the prefix as opaque: don't parse it or rely on its length. Category IDs are strings such as `coffee`. ### Pagination `GET /v1/orders` and `GET /v1/gifts` return results newest first, one page at a time. They take three query parameters: | Parameter | Description | | --- | --- | | `limit` | Page size, from 1 to 100. Defaults to 10. | | `starting_after` | An ID from the current page. Returns the page after it. | | `ending_before` | An ID from the current page. Returns the page before it. | Send `starting_after` or `ending_before`, not both. When `has_more` is `true`, pass the last item's ID as `starting_after` to get the next page: **cURL** ```bash curl "https://api.onme.com/v1/orders?limit=25&starting_after=o-bc1KeWwb90nw_0Jfspeu_oNs0EM7XcAKLQKS64QdQIo" \ -H "Authorization: Bearer $ONME_API_KEY" ``` **Node** ```js const params = new URLSearchParams({ limit: '25', starting_after: 'o-bc1KeWwb90nw_0Jfspeu_oNs0EM7XcAKLQKS64QdQIo', }) const response = await fetch(`https://api.onme.com/v1/orders?${params}`, { headers: { Authorization: `Bearer ${process.env.ONME_API_KEY}` }, }) const page = await response.json() console.log(page.data.length, page.has_more) ``` **Python** ```python import os import requests response = requests.get( "https://api.onme.com/v1/orders", headers={"Authorization": f"Bearer {os.environ['ONME_API_KEY']}"}, params={ "limit": 25, "starting_after": "o-bc1KeWwb90nw_0Jfspeu_oNs0EM7XcAKLQKS64QdQIo", }, ) page = response.json() print(len(page["data"]), page["has_more"]) ``` A cursor that doesn't match one of your orders or gifts returns [`cursor_invalid`](https://business.onme.com/docs/reference#errors/cursor_invalid). `GET /v1/categories` returns the whole catalog in one response, so its `has_more` is always `false`. ### Request IDs Every response, success or error, carries a `Request-Id` header, such as `req-C4451Fpu_emD-fIdoqAQ8dKd9HqnoBiJUf-EvRdz0qM`. Error bodies repeat it as `request_id`. Log it alongside your own records, and include it when you contact support@onme.com so we can find the exact request. ### Rate limits Limits apply per API key, over a 60-second window: | Scope | Limit | | --- | --- | | All endpoints | 120 requests per minute | | `POST /v1/orders` | 30 requests per minute, which also count toward the 120 | A separate limit of 300 requests per minute per IP address covers all traffic, including requests without a valid key. Every response tells you where you stand, using the IETF `RateLimit` headers: ```http RateLimit-Policy: 120;w=60 RateLimit: limit=120, remaining=117, reset=42 ``` `remaining` is how many requests you have left in the window, and `reset` is the number of seconds until it resets. Responses from `POST /v1/orders` report its tighter limit. Go over a limit and you get a `429` with the [`rate_limit_exceeded`](https://business.onme.com/docs/reference#errors/rate_limit_exceeded) code and a `Retry-After` header. Wait that many seconds, then retry. ### Compatibility While the API is in beta, any part of it can change, including in ways that break working integrations. We also add things without notice: new fields on responses, new endpoints, new optional parameters and new error codes. Build your integration so it ignores fields it doesn't recognize and handles a value it hasn't seen before, such as a new `delivery.status`, without crashing. If a change breaks your integration, email [support@onme.com](mailto:support@onme.com). ## Next steps - [Authentication](https://business.onme.com/docs/authentication): get your API key and learn how to keep it safe. - [Quickstart](https://business.onme.com/docs/quickstart): send your first gift in about five minutes. - [Creating orders](https://business.onme.com/docs/creating-orders): everything a `POST /v1/orders` request can do. - [API reference](https://business.onme.com/docs/reference): every endpoint, field and error code. --- # Authentication > Get an On Me API key, send it with every request, keep it safe, and rotate or revoke it when you need to. The On Me API authenticates every request with a secret API key. Your key belongs to your organization, and anything you do with it, including spending your balance, happens on your organization's behalf. ## Get an API key Org admins create and manage keys in the business portal, under [Account → Developers](https://business.onme.com/account/developers). 1. Make sure your organization has a [business portal](https://business.onme.com/) account. If it doesn't, [sign up](https://business.onme.com/signup) first. 2. Fund your balance under [Billing](https://business.onme.com/billing). Every order you create through the API is paid for from it. 3. Open [Account → Developers](https://business.onme.com/account/developers) and select **Create key**. Give the key a name that says where it's used, such as "Production server". If your organization doesn't have API access yet, the Developers page shows a **Request access** button instead. We email your organization's admins as soon as it's on. Keys start with `omk-`. Keys we issued before self-serve keys launched start with `sk_`, and they keep working. > **Warning: You only see your key once** We don't store your key in a form we can show you again. Copy it straight into a secrets manager, such as AWS Secrets Manager, Google Secret Manager, 1Password or Vault. If you lose it, revoke it and create a new one. ## Authenticate requests Send your key as a bearer token in the `Authorization` header of every request: ```http Authorization: Bearer omk-... ``` The examples in these docs read the key from an environment variable called `ONME_API_KEY`. List the categories to check that yours works: `GET /v1/categories` ([API reference](https://business.onme.com/docs/reference#tag/categories/GET/v1/categories)) **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 categories = await response.json() console.log(categories.data.map((category) => category.id)) ``` **Python** ```python import os import requests response = requests.get( "https://api.onme.com/v1/categories", headers={"Authorization": f"Bearer {os.environ['ONME_API_KEY']}"}, ) categories = response.json() print([category["id"] for category in categories["data"]]) ``` A `200` with a list of categories means you're in. A `401` means the key is missing or wrong; the error's `code` tells you which (see [authentication errors](#authentication-errors)). Every endpoint needs a key except two: `GET /v1/health` and `GET /v1/openapi.json`, which serves the API's OpenAPI document. ## Keep your key safe Your key can spend your organization's balance, so treat it like a password to your bank account. - **Use it only on your servers.** Never put it in a browser, a mobile app or anything else you ship to users, where anyone can extract it. If your frontend needs to send a gift, have it call your backend, and have your backend call On Me. - **Keep it out of source control.** Load it from a secrets manager or an environment variable, never from a file you commit. - **Keep it out of logs.** Make sure your HTTP client and error tracker redact the `Authorization` header. Your organization can have up to 10 active keys. Use a separate key for each system that calls the API, so you can revoke one without touching the others. Every key has full access to all of your organization's orders and gifts. There are no read-only or per-endpoint keys yet, so limit who and what can read them. ## Rotate or revoke a key To rotate a key without downtime: 1. Create a new key under [Account → Developers](https://business.onme.com/account/developers). 2. Deploy it everywhere the old key is used. 3. Watch the old key's **Last used** time on the Developers page. Once it stops changing, nothing is using it any more. 4. Revoke the old key. If you think a key has leaked, revoke it straight away. Revocation takes effect immediately: the next request with that key fails with [`api_key_revoked`](https://business.onme.com/docs/reference#errors/api_key_revoked). It's also permanent, so create a new key to carry on with. Every admin in your organization gets an email whenever a key is created or revoked. ## Sandbox Sandbox keys start with `omk-sandbox-`. Create one by switching the business portal to **Sandbox** mode, then adding a key under **Account → Developers**. **There is no separate sandbox URL.** You call the same base URL either way and your key decides which environment serves the request, so moving to production means swapping the key and changing nothing else. A sandbox key spends a sandbox balance, reaches no real recipient, and charges no card. Keys are not interchangeable between environments, and that is enforced rather than assumed: a key that somehow reaches the wrong environment is refused with [`api_key_environment_mismatch`](https://business.onme.com/docs/reference#errors/api_key_environment_mismatch) rather than being served. You should never see it. See [Testing in the sandbox](https://business.onme.com/docs/sandbox) for the recipient addresses that let you choose what happens to a gift. The **Try it** button in the [API reference](https://business.onme.com/docs/reference) sends its requests to the sandbox, so it is safe to click around. ## Authentication errors When we can't authenticate a request, you get one of these errors. None of them change anything, so once you've fixed the cause it's safe to send the same request again. | Code | Status | What it means | What to do | | --- | --- | --- | --- | | [`api_key_missing`](https://business.onme.com/docs/reference#errors/api_key_missing) | 401 | The request had no `Authorization: Bearer` header. | Send `Authorization: Bearer omk-...` on every request. | | [`api_key_invalid`](https://business.onme.com/docs/reference#errors/api_key_invalid) | 401 | We don't recognize the key. | Check for a truncated or mistyped key, or create a new one under [Account → Developers](https://business.onme.com/account/developers). | | [`api_key_revoked`](https://business.onme.com/docs/reference#errors/api_key_revoked) | 401 | The key was valid but has been revoked. | Switch to a current key, or create one under [Account → Developers](https://business.onme.com/account/developers). | | [`api_key_environment_mismatch`](https://business.onme.com/docs/reference#errors/api_key_environment_mismatch) | 401 | The request reached the wrong environment for its key. You should not see this. | Call the documented base URL, not a deployment host. If you already do, quote the `request_id` to support. | | [`organization_suspended`](https://business.onme.com/docs/reference#errors/organization_suspended) | 403 | The organization that owns the key is suspended. | Email support@onme.com. | --- # Quickstart > Send your first On Me gift through the API in about five minutes, open it as your recipient would, then look it up. In this guide you send a $10 gift to yourself as a link, open it the way a recipient would, and then look it up through the API. You need a terminal, and optionally Node 18+ or Python 3 with `requests`. The Node samples use top-level `await`, so run them as ES modules, for example by saving them as `.mjs` files. > **Warning: This sends a real gift** Following this guide with a live key spends $10 of your organization's real balance on a real gift. The gift is yours to keep: claim it and spend it. To rehearse it instead, use a sandbox key — see [Testing in the sandbox](https://business.onme.com/docs/sandbox). 1. **Get your API key** If you don't have a key yet, an org admin can create one under [Account → Developers](https://business.onme.com/account/developers) (see [Get an API key](https://business.onme.com/docs/authentication#get-an-api-key)). Then export it in your terminal so the examples below can use it: ```bash export ONME_API_KEY=omk-... ``` 2. **Check your balance**: Your order is paid for from your organization's balance, so make sure it has at least $10. You can see it, and add funds, under [Billing](https://business.onme.com/billing) in the business portal. 3. **Pick a category** A category decides where the gift can be spent. List them all: **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 categories = await response.json() console.log(categories.data.map((category) => `${category.id}: ${category.name}`)) ``` **Python** ```python import os import requests response = requests.get( "https://api.onme.com/v1/categories", headers={"Authorization": f"Bearer {os.environ['ONME_API_KEY']}"}, ) categories = response.json() print([f"{category['id']}: {category['name']}" for category in categories["data"]]) ``` You get back a list. We've trimmed each category to the fields you need here: **200 OK** ```json { "object": "list", "data": [ { "id": "coffee", "name": "Coffee", "description": "For caffeine-addicts" }, { "id": "choice", "name": "Choice", "description": "For those who want to choose" } ], "has_more": false } ``` Copy the `id` of the one you like. The examples below use `coffee`. 4. **Send the gift** Create an order with one $10 gift. `amount` is in cents, so $10 is `1000`, the smallest gift you can send. The `link` delivery method means we don't email or text anyone; we give you a link instead. Every `POST /v1/orders` needs an `Idempotency-Key` header with a value that's unique to this order. If a request times out, retrying with the same key returns the original order instead of charging you twice. **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": 1000, "category": "coffee", "delivery": { "method": "link" }, "personalization": { "recipient_name": "You" } } ] }' ``` **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: 1000, category: 'coffee', delivery: { method: 'link' }, personalization: { recipient_name: 'You' }, }, ], }), }) const order = await response.json() console.log(order.order_id, 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": 1000, "category": "coffee", "delivery": {"method": "link"}, "personalization": {"recipient_name": "You"}, } ] }, ) order = response.json() print(order["order_id"], order["gifts"][0]["delivery"]["link"]) ``` A `201` means the order exists and your balance has been debited. With `status: "processed"`, as here, the gift exists too. `sender_name` shows your organization's name because you didn't set one: **201 Created** ```json { "order_id": "o-jfAQKlOHalPHMJkwMYZffJF_6j2OMWSu9svqSIvkLSw", "status": "processed", "gifts": [ { "gift_id": "g-gpsh60pP2JhgzroxWk4AyXfL237_DiKIJQK8ilq2u5g", "amount": 1000, "category": "coffee", "delivery": { "method": "link", "status": "sent", "link": "https://gift.onme.com/b6YSLdAA98fcSxpU" }, "personalization": { "recipient_name": "You", "sender_name": "Acme" } } ] } ``` In the rare case that `status` is `processing`, `gifts` can be empty; the gift is finished shortly after, and the lookup in the last step returns it. See [when the status is processing](https://business.onme.com/docs/creating-orders#when-the-status-is-processing). Hang on to the `order_id`; you need it in the last step. If you got an error instead, its `code` says what to fix, and nothing was charged. Look it up in the [error reference](https://business.onme.com/docs/reference#errors). 5. **Open your gift**: Copy `gifts[0].delivery.link` from the response and open it in your browser. You land on the gift's claim page, the same one a recipient sees when you share a link with them. Claim it if you like: it's a real gift. 6. **Look it up** Fetch the order by its ID, replacing the example ID with your own: **cURL** ```bash curl https://api.onme.com/v1/orders/o-jfAQKlOHalPHMJkwMYZffJF_6j2OMWSu9svqSIvkLSw \ -H "Authorization: Bearer $ONME_API_KEY" ``` **Node** ```js const orderID = 'o-jfAQKlOHalPHMJkwMYZffJF_6j2OMWSu9svqSIvkLSw' const response = await fetch(`https://api.onme.com/v1/orders/${orderID}`, { headers: { Authorization: `Bearer ${process.env.ONME_API_KEY}` }, }) const order = await response.json() console.log(order.gifts[0].delivery.status) ``` **Python** ```python import os import requests order_id = "o-jfAQKlOHalPHMJkwMYZffJF_6j2OMWSu9svqSIvkLSw" response = requests.get( f"https://api.onme.com/v1/orders/{order_id}", headers={"Authorization": f"Bearer {os.environ['ONME_API_KEY']}"}, ) order = response.json() print(order["gifts"][0]["delivery"]["status"]) ``` Because you opened the link, the gift's `delivery.status` has moved from `sent` to `opened`. If you claimed it, it reads `claimed`. ## What just happened - **Your balance paid for the order.** `POST /v1/orders` debited $10 from your organization's balance and, almost always, created the gift in the same request. There was no checkout and no card to charge. - **You delivered the gift yourself.** With the `link` method, we created the gift and handed you its claim link, and you chose where it went. The `email` and `phone` methods have us send it for you. - **The gift moved through its lifecycle.** It started as `sent`, became `opened` when you visited the link, and becomes `claimed` once someone claims it and gets a card for its category. [How a gift moves](https://business.onme.com/docs/overview#how-a-gift-moves) shows the whole journey. ## Next steps - [Creating orders](https://business.onme.com/docs/creating-orders): send several gifts at once, set the names recipients see, and handle every response. - [Idempotency and retries](https://business.onme.com/docs/idempotency): build a retry loop that can never charge you twice. - [Delivery methods](https://business.onme.com/docs/delivery-methods): send gifts by email or SMS, and fix a delivery that failed. --- # Testing in the sandbox > Rehearse every gift outcome without sending a real gift. The sandbox is a full copy of the API running against its own database. Orders you place there create real orders and real gifts, debit a real balance and return the same responses production does — but no card is charged, no message is sent, and no recipient is reached. You reach it with a sandbox key. There is one base URL for both environments and your key decides which one serves the request, so moving an integration to production means swapping the key and changing nothing else. > **Info: Getting a sandbox key** Switch the business portal to **Sandbox** mode, then create a key under **Account → Developers**. Sandbox keys are prefixed `omk-sandbox-`; live keys are prefixed `omk-`. ## Choosing what happens to a gift In production, what becomes of a gift is up to its recipient: they open it, or they don't; the delivery lands, or it bounces. You cannot rehearse any of that against a real person. So in the sandbox the **recipient address chooses the outcome**. Send a gift to one of these and you get that result immediately. The rest of your request is unchanged — same body, same headers, same code path. Only the recipient differs, which means the integration you test is the integration you ship. ```bash curl https://api.onme.com/v1/orders \ -H "Authorization: Bearer omk-sandbox-..." \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "gifts": [{ "category": "coffee", "amount": 2500, "delivery": { "method": "email", "email": "delivery-failed@sandbox.onme.com" } }] }' ``` That order comes back `processed` with one gift whose `delivery.status` is `failed` — the state you would otherwise have to wait for a real bounce to see, and the one that unlocks `PUT /v1/gifts/:id/delivery`. ## Driving a gift that is already created Triggers fire at order time. To move a gift that already exists — or one delivered by link, which has no recipient address to carry a trigger — use the sandbox endpoints. Open, claim and spend work on any gift in your sandbox. Deliver works only on gifts On Me sends by email or phone; a link gift returns `gift_delivery_not_simulatable`. | Endpoint | What it does | | --- | --- | | `POST /v1/sandbox/gifts/:id/deliver` | Marks delivery delivered or failed | | `POST /v1/sandbox/gifts/:id/open` | Marks the gift opened | | `POST /v1/sandbox/gifts/:id/claim` | Claims it onto a card | | `POST /v1/sandbox/gifts/:id/spend` | Records a first spend | Order is enforced the way it is in production: a spend before a claim returns `gift_not_claimed`, and claiming twice returns `gift_already_redeemed`. Claiming also back-fills the opened timestamp, because a gift that was claimed without being opened is a state production never produces. They are in the [API reference](https://business.onme.com/docs/reference) alongside everything else, tagged **Sandbox** and badged _Sandbox only_. > **Warning: These answer only for a sandbox key** `/v1/sandbox/...` is documented because it shares a base URL with the rest of the API, not because it exists in production. A live key on one of these paths gets `403 sandbox_endpoint_only`. Code that calls them is test scaffolding by construction — which is exactly why they are namespaced rather than mixed into `/v1/gifts`. ## Testing an order that takes a while Most orders finish before the response returns. Occasionally one does not: the order is paid for, some gifts are still being created, and the response says `processing`. Your integration has to poll until it settles. Send to `processing@sandbox.onme.com` and the sandbox reproduces that: the order reports `processing` with no gifts for about twenty seconds, then completes on its own. It finishes by itself rather than waiting for you to nudge it, so the loop you write here is the loop that runs in production. ## Testing failures Two triggers fail the whole order rather than the gift: - `insufficient-funds@sandbox.onme.com` returns `402 insufficient_balance` whatever your balance is - `order-error@sandbox.onme.com` returns `500 api_error`, for exercising retry handling Both are rejected before anything is created, so there is no order and no debit to clean up. ## Starting over **Account → Developers** has a reset control in sandbox mode. It deletes every sandbox order, gift and balance for your organization, including the balance ledger. Your API keys survive, and you will need to add funds again before you can order. Reach for it when your sandbox has drifted into a state you can no longer account for. Everything in it is reproducible — a few calls recreate your fixtures — so starting clean is usually quicker than working out how it got there. --- # 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. --- # Idempotency and retries > Retry any order request safely, and never send or charge a gift twice. Networks fail. A request can time out after we've created the order, and a retry without protection would send and charge every gift a second time. The `Idempotency-Key` header prevents that: send the same key again and you get the order it already created, not a new one. ## Why it's required `POST /v1/orders` moves money, so we require an `Idempotency-Key` header on every call. A request without one is rejected with [`parameter_invalid`](https://business.onme.com/docs/reference#errors/parameter_invalid) and `param: "Idempotency-Key"`, before anything else happens. With a key, the rules are simple: - The first request with a key creates the order. - Every later request with that key and the same body returns that same order. - There's no expiry window: a key that created an order returns that order whenever you send it. Keys are scoped to your organization, so they never collide with another organization's keys. No other endpoint takes a key. The `GET` endpoints only read, and `PUT /v1/gifts/{gift_id}/delivery` can't send a gift twice, because it only works while the gift's delivery has failed. ## Choosing a key A key is 1 to 255 printable ASCII characters. Use one of these: - **A random UUID v4**, generated once per order: `crypto.randomUUID()` in Node, `uuid.uuid4()` in Python, `uuidgen` in a shell. Save it with your record before you send the request, so a retry after a crash can reuse it. - **A value derived from your own order ID**, such as `order-${yourOrderId}`. A retry then produces the same key without storing anything extra. See [the recipe](https://business.onme.com/docs/creating-orders#idempotency-keys-from-your-own-order-ids). Whichever you choose: - **Use one key per distinct order.** If one of your records can lead to more than one On Me order, include what makes each one different, such as a date or attempt number. - **Keep personal data out.** Don't put emails, phone numbers or names in a key. - **Stay within printable ASCII.** Characters outside space (`0x20`) to tilde (`0x7E`), or a key over 255 characters, fail with `parameter_invalid`. ## What happens when you repeat a request | You send | You get | | --- | --- | | The same key and the same body as an order that was created | `200 OK` with that order, plus an `Idempotent-Replayed: true` header. Nothing new is created or charged. | | The same key with a different body | `409` [`idempotency_key_reused`](https://business.onme.com/docs/reference#errors/idempotency_key_reused). The original order is unchanged. | | The same key while the first request is still running | We wait up to about five seconds for it to finish and return its result. If it's still running, you get `409` [`idempotency_in_progress`](https://business.onme.com/docs/reference#errors/idempotency_in_progress); retry shortly. | | A key whose earlier requests all got error responses | The request is processed as new. Error responses never use up a key, because a key binds to an order only when we debit your balance. | "The same body" compares the request after validation, so the order of keys, the case of an email address and the formatting of a phone number (`+1 (415) 555-1234` versus `+14155551234`) don't matter. Any change to an amount, category, recipient or name does. Because errors never use up a key, you can fix a request that failed with `parameter_invalid` or `insufficient_balance` and resend it with the same key, even though the body changed. ## When to retry Every outcome falls into one of three cases: | What you got | What it means | What to do | | --- | --- | --- | | A `2xx` response | The order exists. `201` means this request created it; `200` means it's a replay. | Done. If `status` is `processing`, poll the order until it's `processed`; see [When the status is processing](https://business.onme.com/docs/creating-orders#when-the-status-is-processing). | | A response with an `error` object | Nothing happened: no order, no charge, no gifts. | Follow the code's retry rule in the [error reference](https://business.onme.com/docs/reference#errors). Codes marked **Retry: yes** (`idempotency_in_progress`, `resource_locked`, `rate_limit_exceeded` and `internal_error`) can be resent unchanged after `Retry-After`. **Retry: after fix** codes need you to fix the request, key, balance or account first. **Retry: no** codes can't succeed. | | No response, a timeout, or a response without an `error` object | The outcome is unknown. The order may or may not exist. | Retry with the same key and the same body. You get the order if it exists, or a new one if it doesn't, never two. | The third case is the one that matters most. A `502` or `504` from a proxy, a dropped connection or a client timeout doesn't tell you whether we created the order. Retrying with a new key in that situation is how duplicate gifts happen. ## A complete retry loop This function sends an order and retries it safely. It makes up to five attempts with the same key, using exponential backoff with jitter capped at 10 seconds and honouring `Retry-After`. It retries network errors, any response without an `error` object (the outcome is unknown), `409` with `idempotency_in_progress` or `resource_locked`, `429`, and `5xx`. It returns on success or on an error that retrying can't fix. **Node** ```js const RETRYABLE_409_CODES = new Set(['idempotency_in_progress', 'resource_locked']) const MAX_ATTEMPTS = 5 const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) function backoffMs(attempt, retryAfterHeader) { const exponential = Math.min(10_000, 500 * 2 ** (attempt - 1)) const jittered = exponential / 2 + Math.random() * (exponential / 2) const retryAfterMs = Number(retryAfterHeader) * 1000 || 0 return Math.max(jittered, retryAfterMs) } function isRetryable(status, code) { if (status === 429 || status >= 500) return true return status === 409 && RETRYABLE_409_CODES.has(code) } // Only an error in our envelope proves nothing happened; anything else is an unknown outcome. function onmeError(payload) { const error = payload?.error return typeof error?.type === 'string' && typeof error?.code === 'string' ? error : undefined } export async function createOrderWithRetry(body, key) { let lastProblem for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { let response try { 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': key, }, body: JSON.stringify(body), signal: AbortSignal.timeout(30_000), }) } catch (networkError) { // No response: the outcome is unknown, so retry with the same key. lastProblem = networkError if (attempt < MAX_ATTEMPTS) await sleep(backoffMs(attempt)) continue } const payload = await response.json().catch(() => null) if (response.ok) { return { ok: true, status: response.status, order: payload } } const error = onmeError(payload) // A response without our error object (such as a proxy's 403 or 408) is an unknown outcome. if (error && !isRetryable(response.status, error.code)) { // Nothing happened, and resending the same request can't succeed. return { ok: false, status: response.status, error } } lastProblem = error ?? new Error(`HTTP ${response.status} without an error body`) if (attempt < MAX_ATTEMPTS) { await sleep(backoffMs(attempt, response.headers.get('Retry-After'))) } } // Out of attempts. If the last failure had no error body, the order may exist: // retry later with the same key to find out. throw new Error(`Order not confirmed after ${MAX_ATTEMPTS} attempts; retry later with key ${key}`, { cause: lastProblem, }) } ``` **Python** ```python import os import random import time import requests RETRYABLE_409_CODES = {"idempotency_in_progress", "resource_locked"} MAX_ATTEMPTS = 5 def backoff_seconds(attempt, retry_after_header=None): exponential = min(10.0, 0.5 * 2 ** (attempt - 1)) jittered = exponential / 2 + random.random() * (exponential / 2) try: retry_after = float(retry_after_header) if retry_after_header else 0.0 except ValueError: retry_after = 0.0 return max(jittered, retry_after) def is_retryable(status, code): if status == 429 or status >= 500: return True return status == 409 and code in RETRYABLE_409_CODES # Only an error in our envelope proves nothing happened; anything else is an unknown outcome. def onme_error(payload): error = payload.get("error") if isinstance(payload, dict) else None if isinstance(error, dict) and isinstance(error.get("type"), str) and isinstance(error.get("code"), str): return error return None def create_order_with_retry(body, key): last_problem = None for attempt in range(1, MAX_ATTEMPTS + 1): try: response = requests.post( "https://api.onme.com/v1/orders", headers={ "Authorization": f"Bearer {os.environ['ONME_API_KEY']}", "Idempotency-Key": key, }, json=body, timeout=30, ) except requests.RequestException as network_error: # No response: the outcome is unknown, so retry with the same key. last_problem = network_error if attempt < MAX_ATTEMPTS: time.sleep(backoff_seconds(attempt)) continue try: payload = response.json() except ValueError: payload = None if response.ok: return {"ok": True, "status": response.status_code, "order": payload} error = onme_error(payload) # A response without our error object (such as a proxy's 403 or 408) is an unknown outcome. if error and not is_retryable(response.status_code, error.get("code")): # Nothing happened, and resending the same request can't succeed. return {"ok": False, "status": response.status_code, "error": error} last_problem = error or f"HTTP {response.status_code} without an error body" if attempt < MAX_ATTEMPTS: time.sleep(backoff_seconds(attempt, response.headers.get("Retry-After"))) # Out of attempts. If the last failure had no error body, the order may exist: # retry later with the same key to find out. raise RuntimeError( f"Order not confirmed after {MAX_ATTEMPTS} attempts; retry later with key {key}: {last_problem}" ) ``` Call it with a key you've already saved, so a crash in your own process can resume with the same key: **Node** ```js import { randomUUID } from 'node:crypto' const key = reward.idempotencyKey ?? (await saveIdempotencyKey(reward, randomUUID())) const result = await createOrderWithRetry(orderBody, key) if (result.ok) { await markSent(reward, result.order) } else { const { code, message, request_id } = result.error ?? {} console.error(`Order refused (HTTP ${result.status}): ${code} ${message} ${request_id}`) } ``` **Python** ```python import uuid key = reward.idempotency_key or save_idempotency_key(reward, str(uuid.uuid4())) result = create_order_with_retry(order_body, key) if result["ok"]: mark_sent(reward, result["order"]) else: error = result.get("error") or {} print(f"Order refused (HTTP {result['status']}): {error.get('code')} {error.get('message')} {error.get('request_id')}") ``` > **Tip: Out of attempts is not the same as failed** When the loop gives up after network errors or bodiless `5xx` responses, the order may exist. Keep the key and try again later, for example from a background job. The first successful response tells you what happened. ## Coming from another gifting API If you're migrating from another provider, you may already store an idempotency value per order. Tremendous calls it `external_id`, Tango Card calls it `externalRefID`, and Amazon Incentives calls it `creationRequestId`. All three send it as a field in the request body. On Me takes it as the `Idempotency-Key` header instead, and the body has no equivalent field. Put the value you already generate in the header: **Node** ```js headers: { Authorization: `Bearer ${process.env.ONME_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': existingExternalId, // the value you used to send as external_id }, ``` Two differences to keep in mind: - A field you add to our body for this purpose is ignored, so it won't protect you. Only the header counts. - Reusing a key with a different body is an error ([`idempotency_key_reused`](https://business.onme.com/docs/reference#errors/idempotency_key_reused)), not a silent replay of the old order. Make sure each value maps to exactly one order. --- # Delivery methods > Send gifts by email, SMS or a link you share yourself, track them, and fix a failed delivery. Every gift in an order has a `delivery` object that says how it reaches the recipient. There are three methods: `email` and `phone`, where we send the gift for you, and `link`, where you deliver it yourself. One order can mix all three. ## Email Set `method` to `email` and give the address: **Request** ```json "delivery": { "method": "email", "email": "sam@example.com" } ``` **What the recipient gets.** We email the gift right away. The sender name reads "The Acme team (via On Me)", using your `sender_name` or, if you didn't send one, your organization name, and the subject line says who sent the gift. The email links to the gift page, where the recipient opens and claims it. **What you get back.** The gift echoes the address, trimmed and lowercased, with `status` set to `sent`: **Response** ```json "delivery": { "method": "email", "status": "sent", "email": "sam@example.com" } ``` `sent` means we've accepted the gift for delivery. If the email later can't be delivered, the status changes to `failed`; see [Fixing a failed delivery](#fixing-a-failed-delivery). ## SMS Set `method` to `phone`, not `sms`, and give the number: **Request** ```json "delivery": { "method": "phone", "phone": "+1 (415) 555-1234" } ``` The number must start with `+` and a country code. Spaces, dashes, dots and parentheses are fine, and we store the number in E.164 format: | You send | We store | | --- | --- | | `+14155551234` | `+14155551234` | | `+1 (415) 555-1234` | `+14155551234` | | `+1-415-555-1234` | `+14155551234` | | `+1.415.555.1234` | `+14155551234` | | `(415) 555-1234` | Rejected: no country code | | `4155551234` | Rejected: no country code | A number that isn't valid for its country is rejected too. Either way you get [`parameter_invalid`](https://business.onme.com/docs/reference#errors/parameter_invalid) with a `param` such as `gifts[0].delivery.phone`, and the order isn't created. **What the recipient gets.** We text the gift right away. The message greets the recipient by `recipient_name` if you set one ("Hi Sam", otherwise "Hi there"), names the sender and the category, and includes the link to the gift page. It ends with a line saying who sent it, which includes the email address of your organization's billing contact, or of an admin if no billing contact is set. **What you get back.** The gift echoes the number in E.164: **Response** ```json "delivery": { "method": "phone", "status": "sent", "phone": "+14155551234" } ``` As with email, `sent` means accepted for delivery, and it changes to `failed` if the text can't be delivered. ## Link Set `method` to `link`. There's nothing else to send: **Request** ```json "delivery": { "method": "link" } ``` **What the recipient gets.** Nothing from us. We don't email or text anyone. You deliver the link through your own channel: your app, a chat message, an internal tool, a printed card. **What you get back.** The gift includes `delivery.link`, the URL of the gift page: **Response** ```json "delivery": { "method": "link", "status": "sent", "link": "https://gift.onme.com/Xq7mT2pLk9RwZ4vB" } ``` The recipient opens the link, sees the gift, and claims it, the same page an email or SMS recipient lands on. For a link gift, `sent` means the link is ready; a link gift never reaches `failed`. > **Warning: A link is as good as the gift** Anyone who has the link can claim the gift. Send it only to the recipient, over a channel you trust, and don't log it where others can read it. ## Delivery status Each gift's `delivery.status` tells you how far it has got: | Status | Meaning | | --- | --- | | `sent` | For email and SMS, we've accepted the gift for delivery. For a link, the link is ready to share. | | `opened` | The recipient opened the gift page. | | `claimed` | The recipient claimed the gift onto their On Me card. | | `failed` | We couldn't deliver the email or text. Only `email` and `phone` gifts can fail. | The status shows the furthest point a gift has reached. A gift that failed, was redirected and then claimed reads `claimed`. To track gifts, poll `GET /v1/gifts/{gift_id}` for a single gift, or page through `GET /v1/gifts` to check many at once. Webhooks are planned, so you won't need to poll forever. **cURL** ```bash curl https://api.onme.com/v1/gifts/g-XLotjYqCLoI9pgf0jKYl5gXsNzxkBANTiq1VyivdeRA \ -H "Authorization: Bearer $ONME_API_KEY" ``` **Node** ```js const response = await fetch(`https://api.onme.com/v1/gifts/${giftId}`, { headers: { Authorization: `Bearer ${process.env.ONME_API_KEY}` }, }) const gift = await response.json() console.log(gift.delivery.status) ``` **Python** ```python import os import requests gift = requests.get( f"https://api.onme.com/v1/gifts/{gift_id}", headers={"Authorization": f"Bearer {os.environ['ONME_API_KEY']}"}, timeout=30, ).json() print(gift["delivery"]["status"]) ``` ## Link expiry A gift link stops working a year after it was issued. You don't need to track that date. Fetch the gift with `GET /v1/gifts/{gift_id}` whenever you're about to share or re-share its link: if the link has expired and the gift hasn't been claimed, that response carries a fresh link, and the old one stops working. Only the single-gift endpoint refreshes links. `GET /v1/gifts` and `GET /v1/orders` return whatever link the gift currently has, which may have expired. Use them for status, and fetch the single gift before sending a link to anyone. ## Fixing a failed delivery When an email bounces or a text can't be delivered, the gift's status becomes `failed`. Redirect it to a corrected address or number: `PUT /v1/gifts/{gift_id}/delivery` ([API reference](https://business.onme.com/docs/reference#tag/gifts/PUT/v1/gifts/{gift_id}/delivery)) **cURL** ```bash curl -X PUT https://api.onme.com/v1/gifts/g-XLotjYqCLoI9pgf0jKYl5gXsNzxkBANTiq1VyivdeRA/delivery \ -H "Authorization: Bearer $ONME_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "method": "email", "email": "sam.lee@example.com" }' ``` **Node** ```js const response = await fetch(`https://api.onme.com/v1/gifts/${giftId}/delivery`, { method: 'PUT', headers: { Authorization: `Bearer ${process.env.ONME_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ method: 'email', email: 'sam.lee@example.com' }), }) const gift = await response.json() ``` **Python** ```python import os import requests gift = requests.put( f"https://api.onme.com/v1/gifts/{gift_id}/delivery", headers={"Authorization": f"Bearer {os.environ['ONME_API_KEY']}"}, json={"method": "email", "email": "sam.lee@example.com"}, timeout=30, ).json() ``` | Field | Type | Required | Description | | --- | --- | --- | --- | | `method` | string | Yes | (enum: email \| phone) | | `email` | string | No | When method is email. Address to re-send the gift to. (format: email) | | `phone` | string | No | When method is phone. Number to re-send the gift to, in international format. Stored in E.164. | The response is the updated gift, with the new destination and `status` back to `sent`. We send the gift to the new destination right away, and the link in the undelivered message stops working. You can redirect a gift when all of these are true: - It was sent by `email` or `phone`. Link gifts can't be redirected, and a gift can't be switched to `link`. - Its status is `failed`. A gift that was delivered can't be rerouted, even to fix a typo, so that no one can redirect a gift that already reached someone. - It hasn't been claimed. You can switch methods while you redirect, from email to SMS or the other way round. Each gift can be redirected up to 10 times. These errors mean the redirect was refused and nothing changed: | Code | When | | --- | --- | | [`gift_delivery_not_updatable`](https://business.onme.com/docs/reference#errors/gift_delivery_not_updatable) | The gift's delivery hasn't failed, or it's a link gift. | | [`gift_already_redeemed`](https://business.onme.com/docs/reference#errors/gift_already_redeemed) | The recipient has already claimed the gift. | | [`gift_resend_limit_reached`](https://business.onme.com/docs/reference#errors/gift_resend_limit_reached) | The gift has been redirected the maximum number of times. | | [`parameter_invalid`](https://business.onme.com/docs/reference#errors/parameter_invalid) | The new email or phone number is invalid. `param` names the field. | | [`resource_missing`](https://business.onme.com/docs/reference#errors/resource_missing) | No gift with that ID belongs to your organization. | --- # API reference > Every endpoint, request and response schema, and error code in the On Me API, with code samples for each request. Base URL: `https://api.onme.com` OpenAPI 3.1 spec: [openapi.json](https://business.onme.com/docs/openapi.json) ## Introduction Buy and send On Me gifts programmatically. New here? Start with the [quickstart](https://business.onme.com/docs/quickstart). Authenticate with a secret key as a bearer token: `Authorization: Bearer omk-...`. See [Authentication](https://business.onme.com/docs/authentication). There is one base URL for both environments, and your key decides which one answers: `omk-` reaches live, `omk-sandbox-` reaches the sandbox. Everything below works the same in each, so moving an integration to production is a key swap and nothing else. The sandbox additionally answers the **Sandbox** endpoints below, and lets you choose what becomes of a gift by the recipient address you send to — delivered, bounced, opened, claimed, spent, or a failed order. Operations badged _Mockable_ are the ones that address steers. See [Testing in the sandbox](https://business.onme.com/docs/sandbox). Successful responses return the resource itself, or `{ "object": "list", "data": [...], "has_more": false }` for lists. Errors return a single `error` object; see [Errors](https://business.onme.com/docs/reference#errors). `POST /v1/orders` requires an `Idempotency-Key` header. Retry a timed-out request with the same key and you get the original order, never a second charge. See [Idempotency and retries](https://business.onme.com/docs/idempotency). ## Errors ### Handling errors Every error response has the same shape: ```json { "error": { "type": "invalid_request_error", "code": "parameter_invalid", "message": "Invalid email address", "param": "gifts[0].delivery.email", "doc_url": "https://business.onme.com/docs/reference#errors/parameter_invalid", "request_id": "req-3kTq9x..." } } ``` - Branch on `type` and `code`. Never parse `message`; it is for humans and may change. - `param` names the offending field when there is one, as a path such as `gifts[0].amount`. - Log `request_id` (also sent as the `Request-Id` header on every response) and quote it when contacting support. An error in this shape means **nothing happened**: no order, no charge, no gifts. A timeout, a dropped connection or a gateway error without an `error` object means the outcome is unknown: retry with the same `Idempotency-Key`. ### Retrying Every code below says whether to retry it: | Retry | Meaning | | --- | --- | | `yes` | Send the same request again, unchanged, after a short backoff. We send `Retry-After` with these. | | `after_fix` | Fix what the error names (the request, your key, your balance or your account setup), then retry. For `POST /v1/orders`, keep the same `Idempotency-Key`. | | `no` | Sending it again cannot succeed. | Back off exponentially with jitter (for example 0.5s, 1s, 2s, capped at 10s) and honour `Retry-After` when present. ### Error codes #### api_key_missing **401** · `authentication_error` · Retry: after fix No API key was provided. Send it as a bearer token in the Authorization header. Send `Authorization: Bearer omk-...` on every request. #### api_key_invalid **401** · `authentication_error` · Retry: after fix The API key is not recognised. Check for a truncated or mistyped key. An org admin can create a new one under Account → Developers in the business portal. #### api_key_revoked **401** · `authentication_error` · Retry: after fix The API key was valid but has since been revoked. Switch to a current key. An org admin can create one under Account → Developers in the business portal. #### api_key_environment_mismatch **401** · `authentication_error` · Retry: after fix The request reached the wrong environment for its key. You should not see this: there is one base URL, and your key decides which deployment serves the request. It means the request bypassed that routing, or reached a deployment host directly. Call the documented base URL rather than a deployment host directly. If you already are, this is a fault on our side — quote the request_id. #### organization_suspended **403** · `authentication_error` · Retry: no The organization owning this API key is suspended. Contact support. #### resource_missing **404** · `invalid_request_error` · Retry: no No such resource. Also returned for a resource owned by another organization, so that IDs are not enumerable. #### parameter_invalid **422** · `invalid_request_error` · Retry: after fix A parameter failed validation, or a required one such as the Idempotency-Key header is missing. `param` identifies it. Fix the parameter named in `param` (for example `gifts[0].delivery.phone`) and send the request again. For `POST /v1/orders`, keep the same Idempotency-Key. #### cursor_invalid **422** · `invalid_request_error` · Retry: after fix The cursor in starting_after or ending_before does not refer to a resource you can access. #### request_body_invalid **400** · `invalid_request_error` · Retry: after fix The request body could not be parsed as JSON. #### request_body_too_large **413** · `invalid_request_error` · Retry: after fix The request body exceeds the 1MB limit. #### gift_already_redeemed **422** · `invalid_request_error` · Retry: no The gift has already been redeemed by its recipient. #### gift_already_delivered **422** · `invalid_request_error` · Retry: no The gift has already been delivered. #### gift_resend_limit_reached **422** · `invalid_request_error` · Retry: no The gift has reached its delivery-retry limit. #### gift_delivery_not_updatable **422** · `invalid_request_error` · Retry: no Delivery can only be redirected for a gift whose delivery has failed and that is not yet redeemed. #### idempotency_key_reused **409** · `invalid_request_error` · Retry: no This Idempotency-Key was already used for a request with a different body. Use a new key for a new order. Generate a new Idempotency-Key for a new order. Reuse a key only to retry the exact same request. #### organization_not_configured **422** · `invalid_request_error` · Retry: after fix The organization has no active member for orders to be attributed to. Add an active admin, or set a billing contact who is one, in the On Me business portal. In the business portal, make sure your organization has an active admin, or set a billing contact who is one. #### insufficient_balance **402** · `insufficient_balance_error` · Retry: after fix The organization balance cannot cover this order. Top up and retry; no order was created and nothing was charged. Add funds in the On Me business portal under Billing, then retry with the same Idempotency-Key. #### rate_limit_exceeded **429** · `rate_limit_error` · Retry: yes Too many requests. Retry after the interval given in the Retry-After header. Back off for the number of seconds in `Retry-After`, then retry. #### sandbox_missing **404** · `invalid_request_error` · Retry: after fix No sandbox environment exists for this organization, or the requested sandbox belongs to another organization. #### gift_not_claimed **422** · `invalid_request_error` · Retry: after fix The gift has not been claimed yet, and the requested operation requires a claimed gift. #### sandbox_endpoint_only **403** · `invalid_request_error` · Retry: no A `/v1/sandbox` endpoint was called with a live key. These endpoints fabricate delivery and redemption state, so they answer only in the sandbox. They are documented here because they share a base URL with everything else, not because they exist in production. Send the request with a sandbox key (`omk-sandbox-`). There is no live equivalent — in production these transitions are caused by the recipient. #### gift_delivery_not_simulatable **422** · `invalid_request_error` · Retry: after fix Sandbox only. Delivery can only be simulated for a gift On Me sends itself (email or phone); a link gift is delivered by you. #### resource_locked **409** · `api_error` · Retry: yes Another operation holds this resource. Nothing happened; retry after a short wait. #### idempotency_in_progress **409** · `api_error` · Retry: yes A request with this Idempotency-Key is still in progress. Retry after a short wait to get its result. Wait a second and retry with the same key; you will get the original result. #### internal_error **500** · `api_error` · Retry: yes An unexpected error occurred on our side. It is safe to retry after a short wait; quote the request_id if it persists. Retry after a short wait, with the same Idempotency-Key for `POST /v1/orders`. If it keeps happening, contact support with the `request_id`. ## Categories The gift catalog. ### `GET /v1/categories`: List categories Returns the full catalog in one response. Uses the same list envelope as the other collections, but is not paginated — the catalog is small and bounded, so `has_more` is always false. **Responses** - `200`: The full category catalog. Returns `CategoryList` (see Objects). - `401`: The API key is missing, invalid, or revoked. Returns `Error` (see Objects). - `403`: The organization is suspended. Returns `Error` (see Objects). - `429`: Rate limit exceeded. Retry after the interval in Retry-After. Returns `Error` (see Objects). - `default`: An unexpected error. Returns `Error` (see Objects). ## Orders Purchases of one or more gifts. ### `GET /v1/orders`: List orders Orders belonging to the calling organization, most recent first. Includes orders still `processing` as well as `processed` ones; orders that were never paid for are not returned. **Query parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `limit` | integer | No | Page size for the order list. (min 1, max 100, default: 10) | | `starting_after` | string | No | Return results after this resource ID. Mutually exclusive with ending_before. (non-empty, pattern: ^[A-Za-z0-9_-]+$) | | `ending_before` | string | No | Return results before this resource ID. Mutually exclusive with starting_after. (non-empty, pattern: ^[A-Za-z0-9_-]+$) | **Responses** - `200`: A page of orders. Returns `OrderList` (see Objects). - `401`: The API key is missing, invalid, or revoked. Returns `Error` (see Objects). - `403`: The organization is suspended. Returns `Error` (see Objects). - `422`: A query parameter or cursor is invalid. Returns `Error` (see Objects). - `429`: Rate limit exceeded. Retry after the interval in Retry-After. Returns `Error` (see Objects). - `default`: An unexpected error. Returns `Error` (see Objects). ### `POST /v1/orders`: Create an order **Mockable** Creates and funds an order of 1–10 gifts, charged to your organization balance in a single debit. Responds once every gift exists: 201 for a new order, and 200 when an `Idempotency-Key` replay returns the order that key already created. If we hit a problem after charging you, the order comes back `processing` instead; it is paid for and we will finish it. Every request must carry an `Idempotency-Key` header. See [Idempotency and retries](https://business.onme.com/docs/idempotency) and [Errors](https://business.onme.com/docs/reference#errors). **Mockable in the sandbox.** The recipient address you send to decides what happens: a gift that is delivered, bounces, is opened, claimed or spent, an order that stays `processing` for twenty seconds, or a whole-order 402 or 500. See [Testing in the sandbox](https://business.onme.com/docs/sandbox). **Headers** | Field | Type | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | string | Yes | Required. A unique key for this order, up to 255 printable ASCII characters. A retry with the same key and body returns the original order instead of charging again. (max 255 chars) | **Request body** (JSON) | 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) | **Responses** - `200`: An Idempotency-Key replay: the order this key already created. Returns `Order` (see Objects). - Header `Idempotent-Replayed`: Present on a replay. - `201`: The new order: `processed`, with its gifts, or `processing` if we hit a problem after charging you. Returns `Order` (see Objects). - `400`: The request body could not be parsed. Returns `Error` (see Objects). - `401`: The API key is missing, invalid, or revoked. Returns `Error` (see Objects). - `402`: The organization balance cannot cover this order. Nothing was created or charged. Returns `Error` (see Objects). - `403`: The organization is suspended. Returns `Error` (see Objects). - `409`: A request with this key is still in progress (`idempotency_in_progress`), this key was used with a different body (`idempotency_key_reused`), or another order for your organization was being charged at the same moment (`resource_locked`). Nothing was created or charged. Returns `Error` (see Objects). - `413`: The request body exceeds the 1MB limit. Returns `Error` (see Objects). - `422`: The Idempotency-Key header is missing or invalid, the request body failed validation, or the organization has no active member for orders to be attributed to. Returns `Error` (see Objects). - `429`: Rate limit exceeded. Retry after the interval in Retry-After. Returns `Error` (see Objects). - `500`: An unexpected error. Nothing was created or charged, so it is safe to retry. Returns `Error` (see Objects). - `default`: An unexpected error. Returns `Error` (see Objects). ### `GET /v1/orders/{order_id}`: Retrieve an order **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `order_id` | string | Yes | | **Responses** - `200`: The order. Returns `Order` (see Objects). - `401`: The API key is missing, invalid, or revoked. Returns `Error` (see Objects). - `403`: The organization is suspended. Returns `Error` (see Objects). - `404`: No such resource, or it belongs to another organization. Returns `Error` (see Objects). - `429`: Rate limit exceeded. Retry after the interval in Retry-After. Returns `Error` (see Objects). - `default`: An unexpected error. Returns `Error` (see Objects). ## Gifts Individual gifts and their delivery. ### `GET /v1/gifts`: List gifts Gifts belonging to the calling organization, most recent purchase first. Redemption links in a list response are not refreshed — retrieve a gift individually to be sure of a working link. **Query parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `limit` | integer | No | Page size for the gift list. (min 1, max 100, default: 10) | | `starting_after` | string | No | Return results after this resource ID. Mutually exclusive with ending_before. (non-empty, pattern: ^[A-Za-z0-9_-]+$) | | `ending_before` | string | No | Return results before this resource ID. Mutually exclusive with starting_after. (non-empty, pattern: ^[A-Za-z0-9_-]+$) | **Responses** - `200`: A page of gifts. Returns `GiftList` (see Objects). - `401`: The API key is missing, invalid, or revoked. Returns `Error` (see Objects). - `403`: The organization is suspended. Returns `Error` (see Objects). - `422`: A query parameter or cursor is invalid. Returns `Error` (see Objects). - `429`: Rate limit exceeded. Retry after the interval in Retry-After. Returns `Error` (see Objects). - `default`: An unexpected error. Returns `Error` (see Objects). ### `GET /v1/gifts/{gift_id}`: Retrieve a gift Retrieving a link-delivered gift whose redemption token has expired mints a fresh one, so the returned link always works. **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `gift_id` | string | Yes | | **Responses** - `200`: The gift. Returns `Gift` (see Objects). - `401`: The API key is missing, invalid, or revoked. Returns `Error` (see Objects). - `403`: The organization is suspended. Returns `Error` (see Objects). - `404`: No such resource, or it belongs to another organization. Returns `Error` (see Objects). - `429`: Rate limit exceeded. Retry after the interval in Retry-After. Returns `Error` (see Objects). - `default`: An unexpected error. Returns `Error` (see Objects). ### `PUT /v1/gifts/{gift_id}/delivery`: Redirect a failed delivery **Mockable** Re-sends a gift to a different address. Restricted to gifts whose delivery has FAILED and that are not yet redeemed; any other state returns 422. Redirecting a successfully delivered gift would let a caller reroute it, so this is a security constraint rather than a convenience. **Mockable in the sandbox.** The failed delivery this needs is otherwise unreachable on demand — order to `delivery-failed@sandbox.onme.com`, or call `POST /v1/sandbox/gifts/{gift_id}/deliver` with `{"outcome":"failed"}`, to get a gift in the one state it accepts. See [Testing in the sandbox](https://business.onme.com/docs/sandbox). **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `gift_id` | string | Yes | | **Request body** (JSON) | Field | Type | Required | Description | | --- | --- | --- | --- | | `method` | string | Yes | (enum: email \| phone) | | `email` | string | No | When method is email. Address to re-send the gift to. (format: email) | | `phone` | string | No | When method is phone. Number to re-send the gift to, in international format. Stored in E.164. | **Responses** - `200`: The updated gift. Returns `Gift` (see Objects). - `400`: The request body could not be parsed. Returns `Error` (see Objects). - `401`: The API key is missing, invalid, or revoked. Returns `Error` (see Objects). - `403`: The organization is suspended. Returns `Error` (see Objects). - `404`: No such resource, or it belongs to another organization. Returns `Error` (see Objects). - `413`: The request body exceeds the 1MB limit. Returns `Error` (see Objects). - `422`: The gift is not in a state whose delivery can be redirected, or the body failed validation. Returns `Error` (see Objects). - `429`: Rate limit exceeded. Retry after the interval in Retry-After. Returns `Error` (see Objects). - `default`: An unexpected error. Returns `Error` (see Objects). ## Sandbox Simulation endpoints, answered only for a sandbox key. They drive state that in production only a recipient can cause — delivery settling, opening, claiming, spending — plus a reset. See [Testing in the sandbox](https://business.onme.com/docs/sandbox). ### `POST /v1/sandbox/gifts/{gift_id}/deliver`: Simulate delivery **Sandbox only** Settles a pending email or phone delivery as `delivered` or `failed`. Use this on a gift that already exists. A link-delivered gift has no delivery for On Me to settle and returns 422 `gift_delivery_not_simulatable`. A `failed` outcome is what unlocks `PUT /v1/gifts/{gift_id}/delivery`. Sandbox only: a live key gets 403 `sandbox_endpoint_only`. In production this transition is caused by the recipient, and there is no way to ask for it. **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `gift_id` | string | Yes | | **Request body** (JSON) | Field | Type | Required | Description | | --- | --- | --- | --- | | `outcome` | string | No | What to settle the pending delivery as. Defaults to `delivered`, so an empty body is the happy path. (enum: delivered \| failed, default: delivered) | **Responses** - `200`: The gift, after the transition. Returns `Gift` (see Objects). - `400`: The request body could not be parsed. Returns `Error` (see Objects). - `401`: The API key is missing, invalid, or revoked. Returns `Error` (see Objects). - `403`: Called with a live key (`sandbox_endpoint_only`) — these endpoints exist only in the sandbox — or the organization is suspended. Returns `Error` (see Objects). - `404`: No such resource, or it belongs to another organization. Returns `Error` (see Objects). - `422`: Delivery cannot be simulated for this gift (`gift_delivery_not_simulatable` — the gift is delivered by link, so there is nothing for us to send), or the body failed validation. Returns `Error` (see Objects). - `429`: Rate limit exceeded. Retry after the interval in Retry-After. Returns `Error` (see Objects). - `default`: An unexpected error. Returns `Error` (see Objects). ### `POST /v1/sandbox/gifts/{gift_id}/open`: Simulate the recipient opening a gift **Sandbox only** Stamps the gift opened, as a recipient following their link would. Sandbox only: a live key gets 403 `sandbox_endpoint_only`. In production this transition is caused by the recipient, and there is no way to ask for it. **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `gift_id` | string | Yes | | **Responses** - `200`: The gift, after the transition. Returns `Gift` (see Objects). - `401`: The API key is missing, invalid, or revoked. Returns `Error` (see Objects). - `403`: Called with a live key (`sandbox_endpoint_only`) — these endpoints exist only in the sandbox — or the organization is suspended. Returns `Error` (see Objects). - `404`: No such resource, or it belongs to another organization. Returns `Error` (see Objects). - `429`: Rate limit exceeded. Retry after the interval in Retry-After. Returns `Error` (see Objects). - `default`: An unexpected error. Returns `Error` (see Objects). ### `POST /v1/sandbox/gifts/{gift_id}/claim`: Simulate the recipient claiming a gift **Sandbox only** Redeems the gift onto a card. Back-fills the opened timestamp if the gift was never opened, because a claimed-but-never-opened gift is a state production cannot produce and your integration should not have to handle. Sandbox only: a live key gets 403 `sandbox_endpoint_only`. In production this transition is caused by the recipient, and there is no way to ask for it. **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `gift_id` | string | Yes | | **Responses** - `200`: The gift, after the transition. Returns `Gift` (see Objects). - `401`: The API key is missing, invalid, or revoked. Returns `Error` (see Objects). - `403`: Called with a live key (`sandbox_endpoint_only`) — these endpoints exist only in the sandbox — or the organization is suspended. Returns `Error` (see Objects). - `404`: No such resource, or it belongs to another organization. Returns `Error` (see Objects). - `422`: The gift is already redeemed (`gift_already_redeemed`). Returns `Error` (see Objects). - `429`: Rate limit exceeded. Retry after the interval in Retry-After. Returns `Error` (see Objects). - `default`: An unexpected error. Returns `Error` (see Objects). ### `POST /v1/sandbox/gifts/{gift_id}/spend`: Simulate a first spend **Sandbox only** Records spend against the claimed gift’s card. Ordering is enforced as it is in production: spending before claiming returns 422 `gift_not_claimed`. Sandbox only: a live key gets 403 `sandbox_endpoint_only`. In production this transition is caused by the recipient, and there is no way to ask for it. **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `gift_id` | string | Yes | | **Responses** - `200`: The gift, after the transition. Returns `Gift` (see Objects). - `401`: The API key is missing, invalid, or revoked. Returns `Error` (see Objects). - `403`: Called with a live key (`sandbox_endpoint_only`) — these endpoints exist only in the sandbox — or the organization is suspended. Returns `Error` (see Objects). - `404`: No such resource, or it belongs to another organization. Returns `Error` (see Objects). - `422`: The gift has not been claimed yet (`gift_not_claimed`). Returns `Error` (see Objects). - `429`: Rate limit exceeded. Retry after the interval in Retry-After. Returns `Error` (see Objects). - `default`: An unexpected error. Returns `Error` (see Objects). ### `POST /v1/sandbox/reset`: Delete all sandbox data **Sandbox only** Deletes every order, gift, balance and balance ledger entry belonging to your organization in the sandbox. Your API keys survive; you will need to add funds again before you can order. Not reversible and not idempotency-protected. It is scoped to your own sandbox data, which is the only safety property it needs. The counts come back separately rather than summed so an automated setup step can assert on what actually went. Sandbox only: a live key gets 403 `sandbox_endpoint_only`. **Responses** - `200`: What was deleted. Returns `SandboxResetResult` (see Objects). - `401`: The API key is missing, invalid, or revoked. Returns `Error` (see Objects). - `403`: Called with a live key (`sandbox_endpoint_only`) — this endpoint exists only in the sandbox — or the organization is suspended. Returns `Error` (see Objects). - `429`: Rate limit exceeded. Retry after the interval in Retry-After. Returns `Error` (see Objects). - `default`: An unexpected error. Returns `Error` (see Objects). ## Meta Unauthenticated service endpoints. ### `GET /v1/health`: Health check Liveness probe. Requires no authentication. **Responses** - `200`: The API is reachable. - `429`: Rate limit exceeded. Retry after the interval in Retry-After. Returns `Error` (see Objects). - `default`: An unexpected error. Returns `Error` (see Objects). ### `GET /v1/openapi.json`: Retrieve this specification Returns this document. Requires no authentication, so a client can read the contract before it holds a key. **Responses** - `200`: The OpenAPI document. - `429`: Rate limit exceeded. Retry after the interval in Retry-After. Returns `Error` (see Objects). - `default`: An unexpected error. Returns `Error` (see Objects). ## Objects ### GiftDelivery | Field | Type | Required | Description | | --- | --- | --- | --- | | `method` | string | Yes | (enum: email \| phone \| link) | | `status` | string | Yes | (enum: sent \| failed \| opened \| claimed) | | `email` | string | No | Present only when method is "email". | | `phone` | string | No | Present only when method is "phone". E.164 format. | | `link` | string | No | Redemption URL. Present only when method is "link". Retrieving a single gift refreshes this if it has expired. | ### GiftPersonalization | Field | Type | Required | Description | | --- | --- | --- | --- | | `recipient_name` | string | No | | | `sender_name` | string | No | | ### Gift | Field | Type | Required | Description | | --- | --- | --- | --- | | `gift_id` | string | Yes | | | `amount` | integer | Yes | Gift value in cents. | | `category` | string | Yes | ID of the category this gift is for. | | `delivery` | object | Yes | | | `delivery.method` | string | Yes | (enum: email \| phone \| link) | | `delivery.status` | string | Yes | (enum: sent \| failed \| opened \| claimed) | | `delivery.email` | string | No | Present only when method is "email". | | `delivery.phone` | string | No | Present only when method is "phone". E.164 format. | | `delivery.link` | string | No | Redemption URL. Present only when method is "link". Retrieving a single gift refreshes this if it has expired. | | `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. | | `personalization.recipient_name` | string | No | | | `personalization.sender_name` | string | No | | ### GiftList | Field | Type | Required | Description | | --- | --- | --- | --- | | `object` | string | Yes | | | `data` | array of objects | Yes | | | `data[].gift_id` | string | Yes | | | `data[].amount` | integer | Yes | Gift value in cents. | | `data[].category` | string | Yes | ID of the category this gift is for. | | `data[].delivery` | object | Yes | | | `data[].delivery.method` | string | Yes | (enum: email \| phone \| link) | | `data[].delivery.status` | string | Yes | (enum: sent \| failed \| opened \| claimed) | | `data[].delivery.email` | string | No | Present only when method is "email". | | `data[].delivery.phone` | string | No | Present only when method is "phone". E.164 format. | | `data[].delivery.link` | string | No | Redemption URL. Present only when method is "link". Retrieving a single gift refreshes this if it has expired. | | `data[].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. | | `data[].personalization.recipient_name` | string | No | | | `data[].personalization.sender_name` | string | No | | | `has_more` | boolean | Yes | Whether more results exist after this page in the direction of travel. | ### Order | 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 | | ### OrderList | Field | Type | Required | Description | | --- | --- | --- | --- | | `object` | string | Yes | | | `data` | array of objects | Yes | | | `data[].order_id` | string | Yes | | | `data[].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) | | `data[].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. | | `data[].gifts[].gift_id` | string | Yes | | | `data[].gifts[].amount` | integer | Yes | Gift value in cents. | | `data[].gifts[].category` | string | Yes | ID of the category this gift is for. | | `data[].gifts[].delivery` | object | Yes | | | `data[].gifts[].delivery.method` | string | Yes | (enum: email \| phone \| link) | | `data[].gifts[].delivery.status` | string | Yes | (enum: sent \| failed \| opened \| claimed) | | `data[].gifts[].delivery.email` | string | No | Present only when method is "email". | | `data[].gifts[].delivery.phone` | string | No | Present only when method is "phone". E.164 format. | | `data[].gifts[].delivery.link` | string | No | Redemption URL. Present only when method is "link". Retrieving a single gift refreshes this if it has expired. | | `data[].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. | | `data[].gifts[].personalization.recipient_name` | string | No | | | `data[].gifts[].personalization.sender_name` | string | No | | | `has_more` | boolean | Yes | Whether more results exist after this page in the direction of travel. | ### Category | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | Yes | Stable identifier for the category. Pass this as `category` when creating a gift. | | `name` | string | Yes | | | `description` | string | Yes | Short description of who the category suits. | | `artwork` | string \| null | Yes | URL of the card artwork, or null if none is configured. | | `product_details_url` | string | Yes | Public category details page on onme.com. | ### CategoryList | Field | Type | Required | Description | | --- | --- | --- | --- | | `object` | string | Yes | | | `data` | array of objects | Yes | | | `data[].id` | string | Yes | Stable identifier for the category. Pass this as `category` when creating a gift. | | `data[].name` | string | Yes | | | `data[].description` | string | Yes | Short description of who the category suits. | | `data[].artwork` | string \| null | Yes | URL of the card artwork, or null if none is configured. | | `data[].product_details_url` | string | Yes | Public category details page on onme.com. | | `has_more` | boolean | Yes | Whether more results exist after this page in the direction of travel. | ### SandboxResetResult | Field | Type | Required | Description | | --- | --- | --- | --- | | `object` | string | Yes | | | `orders_deleted` | integer | Yes | | | `gifts_deleted` | integer | Yes | | | `balances_deleted` | integer | Yes | | | `ledger_entries_deleted` | integer | Yes | Balance ledger entries removed alongside the balances. Counted separately because the balance and its ledger are separate records, and a reset that cleared one but not the other would leave a sandbox that reconciles wrong. | ### Error | Field | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | Yes | | | `error.type` | string | Yes | Broad class of error. Clients should branch on this rather than on the HTTP status. (enum: invalid_request_error \| authentication_error \| insufficient_balance_error \| rate_limit_error \| api_error) | | `error.code` | string | Yes | Stable, machine-readable identifier for the specific error. Safe to branch on. (enum: api_key_missing \| api_key_invalid \| api_key_revoked \| api_key_environment_mismatch \| organization_suspended \| resource_missing \| parameter_invalid \| cursor_invalid \| request_body_invalid \| request_body_too_large \| gift_already_redeemed \| gift_already_delivered \| gift_resend_limit_reached \| gift_delivery_not_updatable \| idempotency_key_reused \| organization_not_configured \| insufficient_balance \| rate_limit_exceeded \| sandbox_missing \| gift_not_claimed \| sandbox_endpoint_only \| gift_delivery_not_simulatable \| resource_locked \| idempotency_in_progress \| internal_error) | | `error.message` | string | Yes | Human-readable explanation. Not stable — never branch on this. | | `error.param` | string | No | Path of the offending field, when the error concerns one. | | `error.doc_url` | string | No | Documentation for this error code. | | `error.request_id` | string | Yes | Identifier for this request. Present on every error. Quote it when contacting support. |