Creating orders
Everything about POST /v1/orders, from the request fields to retries, errors and the guarantees behind them.
For AI agents: this page is available as Markdown, and every guide plus the API reference is in llms-full.txt.
An order is one request that sends between 1 and 10 gifts. We debit the total from your organization balance once, create every gift, and start delivering them before we respond. This guide covers the request, the response, and what to do when something goes wrong.
The request
Here's a complete request for one $25 coffee gift sent by email:
IDEMPOTENCY_KEY=$(uuidgen)
curl https://api.onme.com/v1/orders \
-H "Authorization: Bearer $ONME_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{
"gifts": [
{
"amount": 2500,
"category": "coffee",
"delivery": { "method": "email", "email": "sam@example.com" },
"personalization": { "recipient_name": "Sam", "sender_name": "The Acme team" }
}
]
}'What each part does:
Authorization: Bearer $ONME_API_KEYidentifies your organization. See Authentication.Content-Type: application/jsonis required. Without it the body isn't read, and you getparameter_invalidwith noparam. Python'sjson=sets it for you.Idempotency-Keyis required on every order. Generate it once per order and store it before you send, so a retry reuses it. See Idempotency and retries.giftsis the list of gifts to create, 1 to 10 of them, all paid for in one debit.amountis the gift value in cents:2500is $25.00. It must be between1000and50000.categoryis a categoryidfromGET /v1/categories. It decides where the recipient can spend the card.deliverysays how the gift reaches the recipient:email,phone(SMS) orlink.personalizationis optional. It holds the names the recipient sees.
Request body
- giftsarray of objectsrequired1–10 items
The gifts to create, 1–10. The whole order is funded from your organization balance in one debit.
- gifts[].amountintegerrequiredmin 1000max 50000
Gift value in cents, between 1000 and 50000.
- gifts[].categorystringrequirednon-empty
ID of the category, as returned by GET /v1/categories.
- gifts[].deliveryone of: email | phone | linkrequired
- gifts[].delivery.methodstringrequiredenum: email | phone | link
- gifts[].delivery.emailstringformat: email
Recipient address. Required when method is "email".
- gifts[].delivery.phonestring
Recipient number in international format, such as +14155551234. Required when method is "phone". Spaces, dashes and parentheses are accepted; the number is stored in E.164.
- gifts[].personalizationobject
Optional names to show the recipient.
sender_namedefaults to your organization name;recipient_nameis shown only when you supply it. - gifts[].personalization.recipient_namestring1–200 chars
- gifts[].personalization.sender_namestring1–200 chars
Choosing a category
Each gift is a card for one category, such as coffee or dinner. The category decides where the card works: a coffee card can be spent at the coffee brands in that category, and nowhere else. The recipient doesn't pick a brand in advance; they spend the card at any brand the category covers. Some categories, such as choice, are a gift of choice instead: the recipient picks one of several categories when they claim the gift.
Fetch the catalog with GET /v1/categories. It returns every category in one response, with has_more always false:
curl https://api.onme.com/v1/categories \
-H "Authorization: Bearer $ONME_API_KEY"Each category has:
id: what you pass ascategoryin an order. IDs are short slugs such ascoffee, and some contain spaces, such asboard games. Pass them exactly as returned.nameanddescription: 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, ornullif 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:
2500for $25.00.25.5is rejected. - Each gift is $10 to $500, so
amountmust be between1000and50000. - 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_balanceand 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:
{
"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.
"delivery": { "method": "email", "email": "sam@example.com" }"delivery": { "method": "phone", "phone": "+1 415 555 1234" }"delivery": { "method": "link" }With email and phone, we send the gift to the recipient right away. With link, we send nothing: the response includes delivery.link, a URL you give the recipient yourself. Phone numbers need a country code and are stored in E.164 format, such as +14155551234.
Delivery methods covers what recipients receive, delivery statuses, link expiry and how to fix a failed delivery.
Personalization
personalization sets the names the recipient sees. Both fields are optional, and each is 1 to 200 characters after we trim surrounding whitespace.
sender_nameis 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_namegreets the recipient by name, such as "Hi Sam" in the text message. If you leave it out, we don't invent one.
"personalization": { "recipient_name": "Sam", "sender_name": "The Acme team" }The response echoes both. sender_name is always present there, set to your organization name when you didn't send one.
Recipes
Each recipe is a complete request. Swap in your own category IDs from GET /v1/categories.
One email gift
A $50 dinner gift for one person:
curl https://api.onme.com/v1/orders \
-H "Authorization: Bearer $ONME_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"gifts": [
{
"amount": 5000,
"category": "dinner",
"delivery": { "method": "email", "email": "jordan@example.com" },
"personalization": { "recipient_name": "Jordan" }
}
]
}'In production, generate the key before the request and save it with your record, so a retry sends the same key. The inline $(uuidgen) makes a new key every time the command runs.
A batch of ten
Ten $25 coffee gifts in one order, one debit of $250. This is the most an order can hold.
curl https://api.onme.com/v1/orders \
-H "Authorization: Bearer $ONME_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"gifts": [
{ "amount": 2500, "category": "coffee", "delivery": { "method": "email", "email": "ava@example.com" } },
{ "amount": 2500, "category": "coffee", "delivery": { "method": "email", "email": "ben@example.com" } },
{ "amount": 2500, "category": "coffee", "delivery": { "method": "email", "email": "chloe@example.com" } },
{ "amount": 2500, "category": "coffee", "delivery": { "method": "email", "email": "dev@example.com" } },
{ "amount": 2500, "category": "coffee", "delivery": { "method": "email", "email": "emma@example.com" } },
{ "amount": 2500, "category": "coffee", "delivery": { "method": "email", "email": "finn@example.com" } },
{ "amount": 2500, "category": "coffee", "delivery": { "method": "email", "email": "grace@example.com" } },
{ "amount": 2500, "category": "coffee", "delivery": { "method": "email", "email": "hugo@example.com" } },
{ "amount": 2500, "category": "coffee", "delivery": { "method": "email", "email": "isla@example.com" } },
{ "amount": 2500, "category": "coffee", "delivery": { "method": "email", "email": "jack@example.com" } }
]
}'If one address is invalid, the whole order is rejected with a param such as gifts[6].delivery.email. Fix that entry and resend. For more than ten recipients, send several orders, each with its own key.
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 https://api.onme.com/v1/orders \
-H "Authorization: Bearer $ONME_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"gifts": [
{
"amount": 2000,
"category": "coffee",
"delivery": { "method": "link" },
"personalization": { "recipient_name": "Riley" }
}
]
}'Anyone with the link can claim the gift, so treat it like cash. Links expire; see Link expiry.
Mixed delivery methods
One order can use different methods, amounts and categories for each gift:
curl https://api.onme.com/v1/orders \
-H "Authorization: Bearer $ONME_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"gifts": [
{
"amount": 2500,
"category": "coffee",
"delivery": { "method": "email", "email": "sam@example.com" }
},
{
"amount": 5000,
"category": "dinner",
"delivery": { "method": "phone", "phone": "+1 (415) 555-1234" },
"personalization": { "recipient_name": "Alex" }
},
{
"amount": 1000,
"category": "coffee",
"delivery": { "method": "link" }
}
]
}'This debits $85.00 once. Only the third gift comes back with a delivery.link. The second comes back with delivery.phone set to +14155551234.
Idempotency keys from your own order IDs
If your system already has an ID for each order, derive the key from it. A retry after a crash then produces the same key without any extra storage:
export function idempotencyKeyFor(yourOrderId) {
return `order-${yourOrderId}`
}
// reward is your own record, for example { id: '1234', email: 'sam@example.com', amountCents: 2500 }
export async function sendReward(reward) {
const response = await fetch('https://api.onme.com/v1/orders', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ONME_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKeyFor(reward.id),
},
body: JSON.stringify({
gifts: [
{
amount: reward.amountCents,
category: 'coffee',
delivery: { method: 'email', email: reward.email },
},
],
}),
})
return response.json()
}Reading the response
A new order comes back as 201 Created with the order and all of its gifts:
{
"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:
{
"order_id": "o-zerANhhHtZFvz39EFRa0K0GWpooV-GhnyNgY0s-Btt4",
"status": "processed",
"gifts": [
{
"gift_id": "g-XLotjYqCLoI9pgf0jKYl5gXsNzxkBANTiq1VyivdeRA",
"amount": 2500,
"category": "coffee",
"delivery": { "method": "email", "status": "opened", "email": "sam@example.com" },
"personalization": { "sender_name": "Acme" }
},
{
"gift_id": "g-p6wJKWU-3Lwpa7_v1w5_t2rmfYR7jEjCAtTDa3aqjOw",
"amount": 5000,
"category": "dinner",
"delivery": { "method": "phone", "status": "sent", "phone": "+14155551234" },
"personalization": { "recipient_name": "Alex", "sender_name": "Acme" }
},
{
"gift_id": "g-j9zTmu2Tuq26cc2F6Yisb6C9beQfFd9dhCShe6GXhFc",
"amount": 1000,
"category": "coffee",
"delivery": { "method": "link", "status": "sent", "link": "https://gift.onme.com/Xq7mT2pLk9RwZ4vB" },
"personalization": { "sender_name": "Acme" }
}
]
}A replay shows the order as it is now, so delivery statuses may have moved on since the first response. Treat 200 and 201 the same way: both mean the order exists.
Gifts come back in the same order you sent them, so gifts[i] in the response is the gift you sent at gifts[i] in the request. Map them back to your own records by index, and store each gift_id to look the gift up later with GET /v1/gifts/{gift_id}.
Order object
- order_idstringrequired
- statusstringrequiredenum: processing | processed
Whether every gift has been created.
processedmeans they all exist.processingmeans the order is paid for and some gifts are still being created; it happens only if we hit a problem after charging you, and the order finishes on its own. Retrying the original request with the same Idempotency-Key returns it. - giftsarray of objectsrequired
The order's gifts, in the order you submitted them, so the Nth gift here is the Nth gift you sent.
- gifts[].gift_idstringrequired
- gifts[].amountintegerrequired
Gift value in cents.
- gifts[].categorystringrequired
ID of the category this gift is for.
- gifts[].deliveryobjectrequired
- gifts[].delivery.methodstringrequiredenum: email | phone | link
- gifts[].delivery.statusstringrequiredenum: sent | failed | opened | claimed
- gifts[].delivery.emailstring
Present only when method is "email".
- gifts[].delivery.phonestring
Present only when method is "phone". E.164 format.
- gifts[].delivery.linkstring
Redemption URL. Present only when method is "link". Retrieving a single gift refreshes this if it has expired.
- gifts[].personalizationobject
What the recipient sees.
sender_nameis always present, falling back to your organization name when you do not supply one;recipient_nameappears only when you do. - gifts[].personalization.recipient_namestring
- gifts[].personalization.sender_namestring
When the status is processing
status is almost always processed, which means every gift exists. Rarely, it's processing. That means we debited your balance but hit a problem while creating the gifts. The order is paid for, and we finish it on our side; you don't need to do anything to complete it.
While an order is processing, gifts may be empty or incomplete, so don't map gifts by index yet. Instead:
- Store the
order_id. - Poll
GET /v1/orders/{order_id}with backoff (for example after 5, 10, 20 and 40 seconds, then every few minutes) untilstatusisprocessed. - Read the gifts from that response.
Retrying the original POST with the same key and body also works: it returns the order, and nudges it to finish. Never create a new order to replace a processing one. That sends and charges the gifts twice.
curl https://api.onme.com/v1/orders/o-zerANhhHtZFvz39EFRa0K0GWpooV-GhnyNgY0s-Btt4 \
-H "Authorization: Bearer $ONME_API_KEY"Handling errors
An error response has an error object with a code you can branch on. It always means nothing happened: no order, no charge, no gifts. These are the codes POST /v1/orders returns, besides the authentication errors:
| Code | HTTP | What happened | What to do |
|---|---|---|---|
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 | 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 | 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 | 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 | 409 | Another operation briefly held a resource this order needs. | Wait for Retry-After and resend with the same key. |
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 | 400 | The body isn't valid JSON. | Fix the JSON and resend. The same key is fine. |
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 | 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 | 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.
Guarantees
These rules hold for every call to POST /v1/orders, and you can build on them:
- Every order has an idempotency key. A request without an
Idempotency-Keyheader is rejected before anything happens. - A 2xx means the order exists.
processedmeans every gift has been created.processingmeans we've been paid and will finish creating them. - An error response means nothing happened. No order, no debit, no gifts. Retrying with the same key is safe.
- No response, or an error without our
errorobject, 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. - A key belongs to one order. Reusing a key with a different body is rejected with
idempotency_key_reused. - 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.