start_purchase
Start a purchase when the user asks to pay with Envoy or has chosen Envoy as the default for this checkout.
POST https://api.envoy.money/tools/start_purchase
Start a purchase when the user asks to pay with Envoy or has chosen Envoy as the default for this checkout. Never ask for card details. Retry with the same key; a new key is a new purchase. Send the full cart exactly as the user asked, the delivery address, each item's image and the merchant_logo: the user sees them on the approval screen. Only items bought; no gifts. Never split a cart or lower a total. Envoy decides who checks out: read checkout_by in each reply. agent: you get a one-time card. envoy: Envoy checks out; never open the payment step. Envoy never shares a browser live-view link.
| Title | Service | Read only | Destructive | Idempotent | External sites |
|---|---|---|---|---|---|
| Start a purchase | purchase | false | true | true | true |
Send a Central Auth bearer with agent scope. A key-bound connection also sends Envoy-Installation-Proof over this exact request. JSON bodies are limited to 64 KB; a larger body returns HTTP 413 with INVALID_INPUT.
Input
| Field | Required | Type | Description and rules |
|---|---|---|---|
idempotency_key | Yes | string | One key per intent; retry with the same key. A new key is a new purchase. minLength 16; maxLength 64; pattern ^(?:[A-Za-oq-z0-9_-]|p[A-Za-tv-z0-9_-]|pu[A-Za-qs-z0-9_-]|pur[A-Za-z0-9-])[A-Za-z0-9_-]*$ |
merchant_url | Yes | string | Merchant checkout or product URL; the merchant name is taken from its domain. Starts with https://; a bare domain or http:// is upgraded to https://. pattern ^(?:[Hh][Tt][Tt][Pp][Ss]?://)?[^\s/?#:@]+\.[^\s/?#:@]+(?:[/?#]\S*)?$ |
merchant_logo | No | string | Merchant logo or brand image URL, as shown on the page. Starts with https://; a bare domain or http:// is upgraded to https://. pattern ^(?:[Hh][Tt][Tt][Pp][Ss]?://)?[^\s/?#:@]+\.[^\s/?#:@]+(?:[/?#]\S*)?$ |
total | Yes | string | Checkout total in INR, with delivery, tax and fees. Rupees, up to 2 decimals; number also accepted, "₹" and thousands commas are stripped. maxLength 20; pattern ^[0-9]+(?:\.[0-9]{1,2})?$ |
items | Yes | array of object | What to buy, one entry per product. Only items being bought: no free gifts or ₹0 lines. minItems 1; maxItems 20 |
items[].name | Yes | string | Item name, including size, colour or variant. minLength 1; maxLength 200 |
items[].quantity | No | integer | Quantity; default 1. Numeric string also accepted. minimum 1; maximum 99; default 1 |
items[].url | No | string | Item page URL. Starts with https://; a bare domain or http:// is upgraded to https://. pattern ^(?:[Hh][Tt][Tt][Pp][Ss]?://)?[^\s/?#:@]+\.[^\s/?#:@]+(?:[/?#]\S*)?$ |
items[].image | No | string | Direct URL of the product image shown on the cart or product page. Used only to show the user what they are approving. Starts with https://; a bare domain or http:// is upgraded to https://. pattern ^(?:[Hh][Tt][Tt][Pp][Ss]?://)?[^\s/?#:@]+\.[^\s/?#:@]+(?:[/?#]\S*)?$ |
items[].product_id | No | string | A search_products product's product_id, on every item or none. pattern ^ofr_[a-f0-9]{32}$ |
items[].price | No | string | Line price in rupees, quantity included. |
user_request | Yes | string | The user's own words for this purchase, including any notes. minLength 1; maxLength 300 |
address_id | No | string | Saved address ID from get_account with include addresses. Preferred over address. Do not also send address. minLength 1; maxLength 64 |
address | No | string | Delivery address on one line, with recipient name, phone and 6-digit PIN code. Prefer address_id from get_account. Do not also send address_id. minLength 1; maxLength 500 |
max_total | No | string | Highest total the user accepts; defaults to total. The card's hard limit: set it above total if fees may still be added. Rupees, up to 2 decimals; number also accepted, "₹" and thousands commas are stripped. maxLength 20; pattern ^[0-9]+(?:\.[0-9]{1,2})?$ |
product_id | No | string | For a product from search_products, pass its product_id, merchant_url and total exactly; Envoy buys it from Elixir and the user approves in the app. pattern ^ofr_[a-f0-9]{32}$ |
rail | No | "gift_card" | "operated_checkout" | gift_card: buy a gift card of a merchant list_gift_card_merchants lists, for this total. operated_checkout: Envoy places the order itself or refuses; no card is released. Omit for a one-time card. Not with retry_of or product_id. |
order_title | No | string | Display only, as shown, like price_lines and facts; Envoy drops what it cannot show. Order title. |
price_lines | No | array of object | Bill lines adding up to total. |
price_lines[].code | No | string | item_total|delivery|taxes_fees|packaging|discount |
price_lines[].amount | No | string | Rupees, positive. |
facts | No | array of object | Order facts. |
facts[].code | No | string | deliver_to|arrives|sold_by|restaurant|check_in|check_out|cancellation|booked_for |
facts[].value | No | string | — |
retry_of | No | string | Retry a card purchase you reported failed on Envoy's browser checkout: its purchase ID or original idempotency key. Same merchant site, items and total. Envoy finishes it with that card if it can still use it, otherwise as a new purchase on the user's own card. Envoy may also accept a purchase that failed because its card could not be issued. minLength 16; maxLength 64; pattern ^(?:pur_[a-f0-9]{32}|(?:[A-Za-oq-z0-9_-]|p[A-Za-tv-z0-9_-]|pu[A-Za-qs-z0-9_-]|pur[A-Za-z0-9-])[A-Za-z0-9_-]*)$ |
Unknown fields are rejected. The exact input schema is on the schemas page.
Synthetic request
{
"idempotency_key": "synthetic-intent-0001",
"merchant_url": "https://shop.example.test",
"merchant_logo": "https://shop.example.test/logo.png",
"total": "2499.00",
"items": [
{
"name": "Headphones",
"image": "https://shop.example.test/headphones.jpg"
}
],
"user_request": "Buy the headphones"
}Success data
Every reply uses the shared envelope: ok, the purchase, next, user_action and error. Follow next; outputs may gain fields, so ignore ones you do not recognize.
This tool declares no data payload: a success returns the purchase in purchase and what to do next in next.
Synthetic result
Example synthetic-waiting-for-approval (synthetic data):
{
"ok": true,
"contract": "envoy@2026-10",
"tool": "start_purchase",
"request_id": "req_synthetic",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"server_time": "2026-09-29T12:00:00Z",
"replayed": false,
"purchase": {
"purchase_id": "pur_5b1e9c7a3f2d5a8e9c0b1d2e3f4a5b6c",
"service": "purchase",
"idempotency_key": "synthetic-intent-0001",
"revision": 1,
"terminal": false,
"state": "waiting_for_user",
"user_task": "approve",
"reason_code": null,
"status_text": "Approve this purchase in the Envoy app.",
"rail": "otu_card",
"rail_reason": "RAIL_OTU_PREFERRED",
"checkout_by": "agent",
"merchant": {
"name": "Example Store",
"url": "https://shop.example.test"
},
"amount": {
"minor": 249900,
"currency": "INR",
"display": "₹2,499.00"
},
"description": "Headphones",
"details": {
"complete": true,
"missing": []
},
"question": null,
"follow_up": null,
"approval": {
"state": "pending",
"method": "passkey",
"decide_by": "2026-09-29T12:15:00Z",
"approval_url": null
},
"payment_ready": {
"available": false,
"kind": "card",
"expires_at": null
},
"money": {
"status": "not_started",
"paid": null,
"merchant_order_ref": null
},
"receipt": null,
"chain": {
"supersedes": null,
"superseded_by": null
},
"agent": {
"agent_id": "origin:https://host.example.test",
"display_name": "Synthetic Host",
"verification": "origin"
},
"created_at": "2026-09-29T12:00:00Z",
"updated_at": "2026-09-29T12:00:00Z"
},
"data": null,
"next": {
"action": "call_tool",
"tool": "get_purchase_status",
"args": {
"purchase": "pur_5b1e9c7a3f2d5a8e9c0b1d2e3f4a5b6c",
"after_revision": 1,
"wait_seconds": 45
},
"instruction": "Tell the user the user_action message once; they cannot see tool replies. There is no link, id or code to give them. Call get_purchase_status with next.args exactly to check progress. Follow the returned next action until the state changes or next.by passes.",
"after_seconds": 0,
"by": "2026-09-29T12:15:00Z"
},
"user_action": {
"task": "approve",
"url": null,
"by": "2026-09-29T12:15:00Z",
"message": "Approve this purchase in the Envoy app. If your assistant hasn't continued after you approve, reply 'done'."
},
"follow_ups": [],
"error": null
}Errors
A failure has ok: false, changes nothing, and carries error plus one next. Branch on error.code and next, not on the HTTP status. The contract declares every code below for every tool; which one a call returns depends on the request and the purchase state.
| Code | HTTP status | Meaning | What the agent does |
|---|---|---|---|
INVALID_INPUT | 400 | Fix the supplied input. | Fix every field listed in error.fields, then call the same tool once more. |
UNAUTHENTICATED | 401 | Reconnect through Central Auth. | Ask the user to reconnect Envoy, then retry this call. |
NOT_ALLOWED | 403 | The operation is not allowed. | This connection may not do that. Tell the user; do not retry. |
SERVICE_NOT_ENABLED | 403 | The service is not enabled. | This Envoy service is not enabled for the user. Tell the user; do not retry. |
NOT_FOUND | 404 | Purchase absent or owned by another user. | No purchase matches that reference. Check the purchase id from this task; do not start a new purchase to recover. |
IDEMPOTENCY_CONFLICT | 409 | The same key has different input. | Retry with the original inputs unchanged, or use a new idempotency_key only for a genuinely new purchase. |
ACTIVE_PURCHASE_EXISTS | 409 | An active purchase already exists. | Another purchase is in progress. Show the user the user_action message and stop this attempt. |
STATE_CONFLICT | 409 | The operation conflicts with the current state. | This conflicts with the current state. Tell the user; do not retry. |
NO_RAIL | 422 | No rail can serve this purchase; nothing was created. | Envoy cannot pay this purchase. Tell the user why using error.message; do not retry or split it. |
RATE_LIMITED | 429 | Wait before retrying. | Wait next.after_seconds, then retry the same call unchanged. |
UNAVAILABLE | 503 | The service is unavailable; follow retryable and next. | Wait next.after_seconds, then retry the same call unchanged; never pay elsewhere meanwhile. |
CLIENT_UPDATE_REQUIRED | 426 | Update the client. | This client is too old. Tell the user to update it. |