Place Wonder orders
from your own app.
One key, one call to price a cart, one call to confirm it. Your accounts, your cards, your chains — nobody else can see or spend them.
Building with an AI?
Copy this and paste it into Claude, ChatGPT or Cursor. It contains everything needed to write a working integration.
Quickstart
What a real integration looks like end to end. This runs the actual shape of the calls, so you can watch the two steps happen.
Get your key from Discord with /api. It is sent by DM and
shown once.
# 1. price a cart — nothing is charged yet
curl https://yonderdrop.com/v1/checkouts \
-H "Authorization: Bearer yd_live_..." \
-H "Content-Type: application/json" \
-d '{"cart_code":"ABC123","address":"742 Example Ave, Springfield, IL"}'
# → nothing charged yet. This is what comes back:
{
"ok": true,
"id": "chk_8f2a", // use this to confirm or cancel
"status": "pending",
"restaurant": "Bella Vista Kitchen",
"items": [
{ "name": "Chicken Burrito Bowl", "quantity": 1, "subtotal": "$20.80" }
],
"price": {
"subtotal": 20.80, // the food
"discount": -15.00, // applied for you
"service": 2.99,
"tax": 0.78,
"tip": 0.00,
"credit": -0.00, // account credit spent on this order
"total": 9.57 // what the card is charged
},
"account": "buyer-04@…", // who it orders as
"card": "•••• 4242",
"expires_in": 300 // seconds before the hold is released
}
# 2. confirm it — this is the one that spends a checkout
curl https://yonderdrop.com/v1/checkouts/chk_.../confirm \
-H "Authorization: Bearer yd_live_..." \
-H "Idempotency-Key: your-own-id"
# → {"ok":true,"order_id":"...","tracking":"https://yonderdrop.com/track/X7K2M9"}
/confirm. A prepared
checkout can be inspected or cancelled for free, and if anything fails
before the order reaches Wonder you are never charged for it.
Authentication
Every request carries your key as a bearer token.
Authorization: Bearer yd_live_xxxxxxxxxxxxxxxxxxxxxxxx
/api again; the old key stops working immediately.
Anyone holding it can order on your plan and spend your cards.
How an order works
Two steps, on purpose. The first one prices the cart and costs nothing. The second is the only thing in the whole API that spends money. Between them you can change anything, or walk away.
POST /v1/checkouts → pending priced, held 15 min, FREE
│
├── PATCH /v1/checkouts/{id} change tip, notes, card… still free
├── POST /v1/checkouts/{id}/name rename the account, still free
├── POST /v1/checkouts/{id}/reprice ask again, renew the hold
│
├── POST /v1/checkouts/{id}/cancel → cancelled nothing charged, ever
├── (do nothing for 5 minutes) → expired nothing charged, ever
│
└── POST /v1/checkouts/{id}/confirm → placed CHARGED. order is live
↓
GET /v1/orders/{id} → delivered
Stopping an order before it goes through
A checkout is not an order. Pricing one charges nothing, reserves nothing on your card and spends nothing from your daily plan — it only holds a lane for you for fifteen minutes. There are two ways to back out, and both cost exactly nothing:
# say so, and the lane is freed immediately for your next order
curl -X POST https://yonderdrop.com/v1/checkouts/chk_8f2a/cancel \
-H "Authorization: Bearer yd_live_..."
# → {"ok":true,"id":"chk_8f2a","status":"cancelled"}
Or do nothing at all. After fifteen minutes the hold lapses on its own, the lane goes back, and the checkout stops existing. You never have to clean up after yourself — cancelling is just faster, and gets your lane back for the next order sooner.
/confirm, that is a real
order for real food. It cannot be cancelled through this API. If you
need to stop it after that point, it is a conversation with the
restaurant, not an API call. So: price freely, adjust freely, cancel
freely — and treat confirm as the point of no return.What "charged" actually means
Confirm does two separate things. It spends one checkout from your daily plan, and it puts a real order on a real card. If the order fails before it ever reaches the restaurant, your checkout is given straight back — you are not billed a slot for something that never happened.
If it fails after submission, the response says
"outcome_known": false. That means the food may genuinely be
on its way and we will not pretend otherwise, so nothing is refunded and
you should check before trying again. Anything that reports
"outcome_known": true provably never landed.
Every option
Only cart_code is ever required. Everything else has a
sensible default, and anything you leave out falls back to your saved
settings first, then to how the cart itself was built.
| Field | Type | Default | What it does |
|---|---|---|---|
cart_code | string | required | The six-character code from the extension or bookmarklet. |
address | string | from the cart | Where it goes. If the cart was grabbed with an address saved, this is optional. |
address2 | string | from the cart | The apartment, suite, floor or unit — e.g. "Apt 4B". Also accepted as apt. It survives a later address change, so re-pointing an order does not send it to the building's front door. |
tip | number | 0 | Dollars for the driver, e.g. 3.50. Not cents. |
notes | string | none | Instructions for the driver, e.g. "Ring the bell twice". Max 200 characters. |
dropoff | string | MEET_AT_DOOR | How it is handed over: MEET_AT_DOOR, LEAVE_AT_DOOR or MEET_OUTSIDE. |
fulfillment | string | from the cart | DELIVERY or PICKUP. Pickup needs no address and no dropoff. |
card | string | your pool | "number,mm/yy,cvv,zip". Used once for this order and never stored — nothing is added to your pool and nothing is kept afterwards. |
chain | string | best-funded free one | Which chain to order on, e.g. "1". Left out, the chain whose next account holds the most credit is used — Wonder takes that balance off the order, so it is the cheapest one available. |
use_credit | boolean | false | Scan every account and buy with whichever holds the most credit. |
jig | boolean | true | Varies the apartment/unit line so repeat orders to one address are not identical. On by default — send false to turn it off. |
So a full order with a tip, driver notes and a hand-off preference is one call:
curl https://yonderdrop.com/v1/checkouts \
-H "Authorization: Bearer yd_live_..." \
-H "Content-Type: application/json" \
-d '{
"cart_code": "ABC123",
"address": "742 Example Ave, Springfield, IL",
"address2": "Apt 4B",
"tip": 5.00,
"notes": "Ring the bell twice, gate code 4417",
"dropoff": "LEAVE_AT_DOOR"
}'
Using one card for one order
Pass card and that order is paid with it — nothing goes
into your pool, nothing is stored, and your pinned card is untouched.
Useful when a customer's order should not come off your own card, or
when you are testing.
-d '{
"cart_code": "ABC123",
"address": "742 Example Ave, Springfield, IL",
"card": "4242424242424242,09/28,414,10001"
}'
Leave it out and it draws from your pool instead — the pinned card
first, then whichever is at the front. If a card is refused you get
CARD_DECLINED before anything is ordered; swap it with
PATCH on the same checkout rather than starting over.
You do not have to carry card numbers around to retry. Send
{"card": "pool"} and the next unclaimed card in your pool
is drawn for you, exactly as the Discord panel's "try another card"
does. An exhausted pool answers NO_CARD, and says whether
it is empty or simply all on other open checkouts.
curl -X PATCH https://yonderdrop.com/v1/checkouts/chk_8f2a41c07b93 \
-H "Authorization: Bearer yd_live_..." \
-d '{"card": "pool"}'
A fraud decline works the same way. The first
FORTER_DECLINE on an account burns that card but keeps the
checkout open, so a different card can be sent and confirmed again. If a
second card is refused on the same account, the account itself is
the problem: it is retired from the chain and the checkout closes, so
build a new one on another chain.
Pickup is the same call with no address at all:
-d '{"cart_code": "ABC123", "fulfillment": "PICKUP", "tip": 2.00}'
Changing it after pricing
Priced it and want something different? PATCH the checkout
before you confirm. Send only what changes — the rest stays as it was,
and the checkout is re-priced and returned.
curl -X PATCH https://yonderdrop.com/v1/checkouts/chk_8f2a \
-H "Authorization: Bearer yd_live_..." \
-H "Content-Type: application/json" \
-d '{"tip": 8.00, "notes": "Leave with the doorman"}'
# → the whole checkout back, plus what actually changed:
# "changed": ["tip", "notes"], "price": { ... "total": 12.57 }
| You can change | Notes |
|---|---|
tip | Any amount from 0 up. |
notes | Send "" to clear them. |
dropoff | Same three values as above. |
fulfillment | Switch between delivery and pickup. |
card | Swap the card after a decline. Keeps your place in the queue — do not rebuild the checkout for this. |
address | Somewhere else entirely. Re-prices against the new one. |
PATCH /v1/settings takes tip,
dropoff, fulfillment, notes,
jig and chain as your defaults. Anything sent
with a checkout still wins — a default only fills a silence.Endpoints
Errors
Every failure has a stable code, a sentence written for a human, and a flag telling you whether trying again could help.
{
"ok": false,
"code": "CHECKOUT_FAILED",
"error": "Bella Vista Kitchen is open but does not serve this address",
"retryable": true
}
| Code | HTTP | Means | Retry? |
|---|---|---|---|
ADDRESS_REJECTED | 400 | Wonder would not accept that address | yes |
ADDRESS_REQUIRED | 400 | delivery asked for with no address anywhere | no |
ALREADY_PLACED | 409 | that order is on its way and cannot be changed or cancelled | no |
AMBIGUOUS_CARD | 400 | more than one of your cards ends in those four | no |
API_UNREACHABLE | 502 | the ordering API is not answering | yes |
BAD_ADDRESS | 400 | the address you sent was empty | no |
BAD_CARD | 400 | the card could not be parsed | no |
BAD_CART | 400 | that cart could not be read — re-grab it | re-grab |
BAD_COUNT | 400 | count must be a whole number from 1 to 10 | no |
BAD_DROPOFF | 400 | dropoff is one of the three named values | no |
BAD_FULFILLMENT | 400 | fulfillment is DELIVERY or PICKUP | no |
BAD_JSON | 400 | the body is not a JSON object | no |
BAD_SETTING | 400 | that setting is the wrong type or not a valid value | no |
BAD_TIER | 400 | tier is starter, pro or unlimited | no |
BAD_TIP | 400 | tip is not a number of dollars, or is above the ceiling | no |
CARD_DECLINED | 400 | the card was refused while pricing. Nothing was charged | another card |
CARD_REJECTED | 400 | the swapped card was refused | another card |
CARD_REQUIRED | 400 | no card in the body of a cards call | no |
CART_REQUIRED | 400 | no cart_code was sent | no |
CHAIN_BUSY | 409 | that one chain is still settling | yes |
CHAIN_IN_USE | 409 | an order is being placed on that chain right now | yes |
CANNOT_PRICE | 400 | this cart cannot be ordered as configured — the message says what to change, e.g. pickup is not offered at that address | no, change something first |
BAD_CHAIN | 400 | chain must be a string of letters and digits — leave it out entirely to generate into your pool | no |
CHAIN_STALLED | 409 | the rung above has not completed | no |
CHECKOUT_FAILED | 400 | pricing failed — read error for which | yes |
CHECKOUT_UNAVAILABLE | 502 | the payment provider is not answering | yes |
INTERNAL | 500 | our fault | yes |
IN_PROGRESS | 409 | something else is already using that checkout | yes |
NOTHING_TO_CHANGE | 400 | the PATCH body changed nothing | no |
NOT_FOUND | 404 | nothing of yours has that id | no |
NO_CARD | 400 | no card sent and your pool is empty, or every card is on another open checkout | add a card |
FORTER_DECLINE | 400/502 | the fraud gate refused it — first time the checkout stays open for another card, second card retires the account | another card, then another chain |
CREDIT_NOT_SUPPORTED | 409 | a credit-funded order — the current checkout flow does not spend account credit on its own; order on a referral chain instead | no |
NAME_UNSUPPORTED | 409 | rename on a checkout that is not on the mobile flow — the only surface with a name-change endpoint | no |
NAME_FAILED | 400 | Wonder refused the rename | yes |
BAD_NAME | 400 | no name in the body — send {"name":"John Smith"} | no |
BAD_ADDRESS | 400 | could not resolve a delivery address with coordinates | no |
NO_CHAIN_FOR_ACCOUNT | 400 | the account holding the most credit is on no chain, so there is no referral to order under | no |
ACCOUNT_NOT_ON_CHAIN | 400 | that account is not a rung on its own chain | no |
NO_CHAINS | 400 | you have no chains yet | after /v1/accounts |
NO_CHAIN_READY | 409 | every chain is mid-order or settling | yes |
NO_CREDIT_READ | 400 | credit could not be read on any account | yes |
NO_KEY | 404 | you have no API key | no |
NO_REFERRAL | 400 | no unused account below the buyer, so the order would credit nobody | after /v1/accounts |
NO_SESSION | 400 | that account has no saved session, so it cannot order | no |
NO_SESSIONS | 400 | no accounts with a saved session to scan for credit | no |
NO_SUCH_CARD | 404 | no card of yours ends in those four digits | no |
NO_SUCH_CHAIN | 404 | that chain is not one of yours | no |
NO_SUCH_CHECKOUT | 404 | gone, expired after 5 minutes, or never yours | no |
NO_SUCH_JOB | 404 | no job of yours with that id — they are kept an hour | no |
NO_SUCH_ORDER | 404 | no order of yours with that id | no |
NO_SUCH_ROUTE | 404 | no such endpoint at that method and path | no |
PICKUP_HAS_NO_ADDRESS | 400 | this checkout is pickup — switch it to delivery first | no |
QUOTA_EXHAUSTED | 402 | today's checkouts are spent; resets_at says when | tomorrow |
RATE_LIMITED | 429 | too many calls — wait retry_after seconds | yes |
REF_REQUIRED | 400 | /v1/track needs an id to look up | no |
TIMEOUT | 504 | we gave up waiting on something upstream | yes |
TOO_MANY_HELD | 409 | five checkouts already open | after one closes |
TOO_MANY_JOBS | 409 | three generation jobs already running | yes |
UNAUTHORIZED | 401 | the key is missing, wrong or revoked | no |
UNKNOWN_SETTING | 400 | that is not a setting | no |
USE_CARD_PIN_ENDPOINT | 400 | pin cards with /v1/cards/pin, not settings | no |
outcome_known.
When it is false the order may still have reached Wonder
and the food may be on its way, so nothing is refunded and you should
check before retrying. When it is true the order provably
never landed and your checkout is already back.Plans & limits
| Plan | Price | Checkouts / day |
|---|---|---|
| Free | — | 2 |
| Starter | $25 | 6 |
| Pro | $50 | 30 |
| Unlimited | $115 | no cap |
A checkout is spent when an order is placed. Anything that fails, or that you cancel, costs nothing. The day resets at midnight.
Rate limits
Separate from the daily plan. The plan caps how many orders you place; these cap how fast you may call, and they are priced by what a call actually costs us. Every limit is counted per key.
| Bucket | Rate | Burst | Covers |
|---|---|---|---|
| Reads | 120 / min | 40 | plan, chains, cards, orders, jobs, tracking, settings |
| Writes | 40 / min | 12 | adding cards, settings, chain locks, cancels, adjusting a checkout |
| Pricing | 20 / min | 6 | POST /v1/checkouts and reprice — each one prices a real cart |
| Placing | 12 / min | 4 | /confirm — the expensive, irreversible one |
| Generating | 6 / min | 3 | POST /v1/accounts — each account costs an SMS |
Burst is how many you may fire at once before the rate starts to matter — it refills continuously, so a steady caller never notices it.
A limited request answers 429 with a Retry-After
header and the same figure in retry_after, in seconds:
{
"ok": false,
"code": "RATE_LIMITED",
"error": "Too many read requests. Try again in 4s.",
"retryable": true,
"retry_after": 4.2
}
Every successful response also carries
X-RateLimit-Remaining and X-RateLimit-Limit, so
you can slow down before you are told to.
Tracking
Every placed order returns a link you can hand straight to your customer. It shows the driver, the ETA, every item with its photo and the drop-off photo, and never shows a price or an address you did not give it. The page updates itself until the food lands.
https://yonderdrop.com/track/X7K2M9
Try it
Every endpoint, answering in the exact shape the real one answers in. The data below is invented — nobody's key, nobody's cart — but the fields, the error codes and the failure cases are the ones the server actually returns, so what you build against this works against the real thing.
https://yonderdrop.com/v1 with your own key for the real ones.