-
Notifications
You must be signed in to change notification settings - Fork 0
Endpoints
The gateway is self-describing: MCP's tools/list returns every tool it offers,
with descriptions and input schemas. At the time of writing that is 62 tools, of
which ddREST covers the 38 that make up ordinary browse-cart-order use.
bun run list-toolsThat prints what the gateway currently advertises and flags anything ddREST calls that it does not — the check to run when adding a route or after DoorDash ships a change.
| Method | Path | Tool |
|---|---|---|
| GET | /v1/restaurants |
doordash_find_restaurants |
| GET | /v1/nearby-stores |
internal_find_nearby_stores |
| GET | /v1/offers |
internal_get_nearby_offers |
| GET | /v1/stores/{store_id} |
internal_get_store_info |
| GET | /v1/stores/{store_id}/menu |
doordash_get_restaurant_menu |
| GET | /v1/stores/{store_id}/items |
internal_find_items_in_store |
| GET | /v1/stores/{store_id}/items/{item_id} |
internal_get_item_details |
| GET | /v1/stores/{store_id}/menus/{menu_id}/items/{item_id} |
doordash_get_food_item |
| GET | /v1/stores/{store_id}/deals |
internal_get_store_deals |
| GET | /v1/stores/{store_id}/promotions |
internal_list_eligible_cart_promotions |
| GET | /v1/stores/{store_id}/promotions/{campaign_id}/items |
internal_get_promo_eligible_items |
| POST | /v1/product-lists |
doordash_create_product_list |
| GET | /v1/carts |
doordash_list_active_carts |
| POST | /v1/carts/items |
doordash_add_to_cart |
| POST | /v1/carts/{cart_uuid}/items |
doordash_add_to_cart |
| GET | /v1/carts/{cart_uuid} |
doordash_get_cart |
| DELETE | /v1/carts/{cart_uuid} |
doordash_clear_cart |
| PATCH | /v1/carts/{cart_uuid}/items/{cart_item_id} |
internal_update_cart_item |
| DELETE | /v1/carts/{cart_uuid}/items/{cart_item_id} |
doordash_remove_cart_item |
| PUT | /v1/carts/{cart_uuid}/fulfillment |
doordash_update_delivery_option |
| GET | /v1/carts/{cart_uuid}/promotions |
internal_get_applied_cart_promotions |
| POST | /v1/carts/{cart_uuid}/promotions |
internal_apply_cart_promotion |
| DELETE | /v1/carts/{cart_uuid}/promotions/{promo_code} |
internal_remove_cart_promotion |
| POST | /v1/carts/{cart_uuid}/preview |
internal_preview_order |
| POST | /v1/carts/{cart_uuid}/order |
internal_submit_order |
| GET | /v1/carts/{cart_uuid}/checkout-url |
doordash_get_checkout_url |
| GET | /v1/carts/{cart_uuid}/suggestions |
internal_get_mic_carousel |
| GET | /v1/orders |
internal_get_order_history |
| GET | /v1/orders/{order_uuid}/receipt |
internal_get_order_receipt |
| GET | /v1/orders/{order_uuid}/status |
internal_get_order_status |
| GET | /v1/orders/{order_uuid}/status/stream |
internal_get_order_status (polled) |
| POST | /v1/orders/{order_uuid}/reorder |
internal_reorder |
| GET | /v1/me |
doordash_get_user_info |
| GET | /v1/addresses |
doordash_list_delivery_addresses |
| GET | /v1/addresses/search |
doordash_address_autocomplete |
| POST | /v1/addresses |
doordash_select_address |
| PUT | /v1/addresses/current |
doordash_set_delivery_address |
| PUT | /v1/addresses/{address_link_id}/instructions |
internal_set_delivery_instructions |
| PUT | /v1/addresses/{address_link_id}/label |
internal_set_address_label |
| GET | /v1/payment-methods |
doordash_get_payment_info |
Two ids on a cart item are easy to confuse. The cart-line id identifies
this line of this cart and is what PATCH and DELETE take; the menu item
id identifies the product. Saved addresses have the same split: address_id
is the place, address_link_id is this account's link to it and is what carries
the label and the Dasher instructions.
PATCH .../items/{cart_item_id} with quantity: 0 and DELETE on the same
path do the same thing.
POST /v1/carts/{cart_uuid}/order places a real order and charges the account.
tip_amount_cents is required rather than defaulted, so a tip is always
deliberate. See Ordering safely.
Ids are opaque and you rarely have the one you need to hand, so every id in a
path also accepts a shorthand that says what you meant. Each one costs one extra
upstream lookup; passing the real id costs nothing. Whatever a shorthand
resolved to comes back in an X-Resolved-* response header, since the body does
not reliably say.
| Where | Accepts | Resolves to |
|---|---|---|
{cart_uuid} |
latest |
The most recently updated active cart |
{cart_uuid} |
store:<store_id> |
The active cart at that store |
{order_uuid} |
latest |
Your most recent order in the last 90 days |
{store_id} |
name:<store name> |
A store of that name near your default address |
address_id |
default |
Whichever address DoorDash marks as the account default |
address_id |
a label, e.g. home
|
The saved address with that label, matched case-insensitively |
menu_id on add-to-cart |
(omit it) | The store's menu id, read off the store |
curl -s "http://localhost:8787/v1/orders/latest/status" -H "authorization: Bearer dds2.…"
curl -s "http://localhost:8787/v1/carts/latest" -H "authorization: Bearer dds2.…"
curl -s "http://localhost:8787/v1/stores/name:Chipotle/menu" -H "authorization: Bearer dds2.…"A literal id is always tried first, so a real cart whose uuid happened to be
latest still wins over the keyword. Ambiguity is refused rather than resolved
arbitrarily: name: matching two nearby branches, or a label on two addresses,
is a 400 listing the candidates — picking one silently is how you order from
the wrong side of town.
POST /v1/carts/{cart_uuid}/order supports two things worth using every time.
Idempotency-Key (header). A lost response otherwise leaves you unable to
tell whether you were charged, and both options are bad. Send a unique value and
a repeat of the same request returns the first response — marked
Idempotency-Replayed: true — instead of placing a second order. Keys are
scoped to your session and honoured for 24 hours. Reusing one with a different
body is a 409 idempotency_conflict rather than a silent replay.
confirm_total_cents (body). State the pre-tip total you expect, from
quote.net_total_before_tip.unit_amount on the preview response. The cart is
re-priced first and the order refused with 412 total_mismatch if it has moved
past confirm_total_tolerance_cents (default 0). Nothing is ordered on a
mismatch.
curl -sX POST "http://localhost:8787/v1/carts/latest/order" \
-H "authorization: Bearer dds2.…" \
-H "content-type: application/json" \
-H "idempotency-key: $(uuidgen)" \
-d '{"tip_amount_cents": 500, "confirm_total_cents": 3410}'Setting READ_ONLY=true refuses every state-changing request outright, for an
instance handed to an agent or exposed beyond the account holder. Browsing keeps
working; logging in and pairing stay allowed, since blocking those would not be
safer, only useless.
GET /v1/orders/{order_uuid}/status/stream is the status endpoint as Server-Sent
Events, so a client waits instead of polling.
curl -N "http://localhost:8787/v1/orders/latest/status/stream" -H "authorization: Bearer dds2.…"Events are status (the full payload, on the first poll and on every change
afterwards), error (one poll failed; the stream continues) and end (why it
stopped: terminal, timeout or client). It is deliberately quiet — a stream
open across a 40-minute delivery emits a handful of events, not one every tick.
It closes itself once the order reaches a terminal status.
ORDER_STREAM_INTERVAL_SECONDS (default 15, floor 5) and
ORDER_STREAM_MAX_SECONDS (default 1800) tune it. Reconnect if you still care
after a stream ends on timeout.
Anywhere coordinates are accepted — GET /v1/restaurants,
GET /v1/nearby-stores, GET /v1/offers and GET /v1/stores/{id}/deals — you
can pass an address_id from
GET /v1/addresses instead, and ddREST reads the coordinates off the saved
address for you:
curl -s "http://localhost:8787/v1/restaurants?query=pizza&address_id=addr-home" \
-H "authorization: Bearer dds2.…"That costs one extra upstream lookup, so passing latitude/longitude
directly stays the cheaper path. Nothing is cached — an address you just added
would otherwise be invisible until a TTL elapsed.
Pass address_id=default for whichever address DoorDash marks as the account
default, so the common case needs no lookup at all:
curl -s "http://localhost:8787/v1/restaurants?query=pizza&address_id=default" \
-H "authorization: Bearer dds2.…"The id is the address_id field, not address_link_id.
Sending address_id and coordinates is refused rather than picking a winner,
since the two can disagree and you would have no way to tell which was used. An
unknown id returns address_not_found listing the addresses that do exist —
ids are opaque numbers, so each comes with its printable address:
{
"error": "address_not_found",
"message": "No saved address has the id \"9999\".",
"addresses": "/v1/addresses",
"known_addresses": [
{ "id": "1611178960", "address": "Thompson Hotels, 21 E Bellevue Pl, Chicago, IL 60611, USA" }
]
}A saved address carrying no coordinates returns address_missing_coordinates
rather than quietly falling back to the configured default.
Every optional argument the gateway advertises is exposed, except two groups that do nothing useful here:
-
Guest-session arguments (
session_id,session_nonce,is_guest) — these switch identity away from your authenticated account, or ask for an AES-encrypted response ddREST has no key for. -
Widget display arguments (
delivery_address_label,store_name) — these only change labels inside DoorDash's own interactive widget. They have no effect on what the API returns, so exposing them would just be noise.
/docs has the full list; the ones worth knowing about:
| Route | Parameter | Why it matters |
|---|---|---|
/v1/restaurants |
radius |
Miles. The schema documents a default of 3, but a live search with it unset came back reporting search_radius: 8 — so the default is not fixed and may vary by location. Set it explicitly when the reach matters. |
/v1/restaurants |
desired_restaurant_name, item_name
|
Narrow to a named restaurant or a specific dish. |
/v1/stores/{id}/menu |
include_extras |
Every item's options inline. Much larger response. |
/v1/stores/{id}/items |
disable_ads |
Suppress sponsored placements. Worth setting if you take only the first result, since a sponsored placement is not ranked. |
/v1/stores/{id}/items |
limit, snap_eligible_only
|
Results per item; SNAP/EBT filter. |
/v1/carts/{uuid}, add-to-cart |
include_pricing |
Subtotal, taxes and fees inline. An estimate — the preview endpoint is what an order is priced against. |
/v1/carts/{uuid}/preview and /order
|
address_id |
Deliver this order to a saved address without changing the account default. Sent upstream as delivery_address_id. |
/v1/nearby-stores |
use_store_ranker |
Experimental upstream ranking instead of soonest-ETA. |
Boolean query parameters accept true/false, 1/0, yes/no, and reject
anything else rather than guessing — ?disable_ads=maybe is a 400, not a
silent true.
Every result-count parameter is spelled limit, whatever the upstream tool
calls it (max_stores, max_carts, max_results, max_orders). The
translation happens server-side.
The auth and pairing routes are not tool-backed:
| Method | Path | Purpose |
|---|---|---|
| POST | /v1/auth/login/start |
Begin the paste-back login |
| POST | /v1/auth/login/complete |
Finish it and get a session |
| GET | /v1/auth/session |
Inspect the current session |
| POST | /v1/auth/logout |
Revoke it |
| POST | /v1/auth/pair/request |
Device: ask for a pairing code |
| POST | /v1/auth/pair/token |
Device: poll for the session |
| GET/POST | /v1/auth/pair |
Human: the approval pages (HTML) |
| POST | /v1/auth/pair/verify |
Human: look up a code (JSON) |
| POST | /v1/auth/pair/complete |
Human: approve it (JSON) |
| POST | /v1/auth/pair/deny |
Human: refuse it (JSON) |
Most MCP tools accept an intent string, and per dd-cli's own help text
DoorDash "may review this data for research and product-improvement purposes".
This API generates it server-side, per operation, and forwards no end-user text.
Callers cannot set or influence it. Every string sent is in one auditable place:
src/mcp/tools.ts.
Where a tool does accept it, it is optional, which the gateway's own schemas confirm — they describe it as "logged for product analytics and monitoring only — has no effect on the results returned". ddREST sends it anyway, because a generated one-line description of the operation is a smaller disclosure than an empty field is a benefit, and it keeps behaviour close to dd-cli's. Nothing would break if it were dropped.
Twelve of the 38 declare no intent at all, and ddREST sends none for them
rather than an argument the gateway never asked for:
doordash_update_delivery_option internal_get_nearby_offers
internal_update_cart_item internal_get_store_deals
internal_get_applied_cart_promotions internal_set_delivery_instructions
internal_get_promo_eligible_items internal_set_address_label
internal_get_mic_carousel doordash_select_address
doordash_address_autocomplete doordash_get_user_info
{ "error": "session_expired", "message": "…", "login_start": "/v1/auth/login/start" }error is a stable machine-readable code. Notable ones: session_missing,
session_invalid, session_expired, csrf_origin_rejected,
login_ticket_expired, state_mismatch, token_exchange_failed,
doordash_unauthorized, doordash_forbidden, upstream_error,
cart_not_found, order_not_found, store_not_found, menu_not_found,
read_only, total_mismatch, idempotency_conflict.
A 403 doordash_forbidden with private_beta_gating: true means the account
authenticated fine but is not an approved consumer-MCP tester.
Errors say what would work. A wrong or unresolvable id comes back with the
things that do exist, so it can be corrected without a second call or a trip to
these docs — known_addresses on an address, known_carts on a cart, matches
on an ambiguous store name, and a pointer at the endpoint the id comes from.
success: false is an error, not a 200. DoorDash reports a semantic failure
— cart gone, store closed, item unavailable — inside the body of an otherwise
successful response. ddREST turns that into a 502 doordash_tool_error carrying
the upstream message, with the whole original payload under upstream_result so
nothing is lost. Set STRICT_TOOL_ERRORS=false to go back to passing it
through, if DoorDash ever starts reporting success: false in a benign case.
429 responses carry Retry-After. So does the RFC 8628 slow_down during
device pairing, where the header matches the new interval in the body.
Tool responses are passed through unvalidated, but they are documented. The
gateway advertises an outputSchema per tool alongside its inputs, so /docs
shows the real response shape for most tools. The
other six declare a bare {additionalProperties: true} and fall back to the
generic pass-through result:
doordash_get_restaurant_menu internal_get_item_details
doordash_list_active_carts doordash_get_checkout_url
doordash_list_delivery_addresses doordash_get_payment_info
Documented is not enforced. Nothing validates a response against its schema — DoorDash can add a field at any time and it must still pass through untouched.
The schemas are committed rather than fetched at boot, so the API document is
identical for everyone, needs no account, and renders /docs without a network
call. Refresh them after a DoorDash change:
bun run list-tools --dump && bun run gen-schemasThat rewrites src/schemas/results.generated.ts,
hoisting each tool's $defs into shared components so their $refs resolve
inside the OpenAPI document.