# Changelog
Source: https://www.zinc.com/docs/changelog
Product updates and improvements to Zinc API
Agents can now manage their own webhooks and check their own balance, with no dashboard login required. Plus the retailer's own reason when an order is cancelled, and a round of fixes.
### Webhooks via the API
Until now, a webhook URL could only be set in the dashboard — which an agent paying through [`POST /agent/orders`](/docs/v2/api-reference/agent/create-order) doesn't have. Three new endpoints close the gap:
* `PUT /webhooks/endpoint` — set your webhook URL. A signing secret is minted the first time you call it.
* `GET /webhooks/endpoint` — retrieve the current URL and signing secret.
* `DELETE /webhooks/endpoint` — clear the URL. The secret is retained, so re-enabling later needs no change to your verification code.
Private and internal addresses are rejected. Events, payloads, and signature verification are unchanged — see [Webhooks](/docs/v2/api-reference/introduction/webhooks).
### Check Your Balance
`GET /wallet/me` returns your [wallet](/docs/v2/wallet) balance, so you can check funds before placing an order rather than finding out from a `402`. It's now in the OpenAPI spec and generated SDKs.
### Retailer Cancellation Reasons
When a retailer cancels an order after placement, its own explanation now comes through instead of a generic message. Look for it in the item's `cancellation_reason`, in the refund note, and on the [`order.cancelled`](/docs/v2/api-reference/introduction/webhooks) webhook as `extra_data.retailer_reason`.
### Bug Fixes
* **Duplicate agent payments:** A retried `POST /agent/orders` with the same [`idempotency_key`](/docs/v2/api-reference/introduction/idempotency) now returns `409` before any payment settles. Payer addresses match case-insensitively.
* **Invoiced accounts:** Customers billed by invoice are no longer rejected for insufficient wallet funds.
* **Payment errors on Zinc-managed accounts:** Now reported as `retailer_unavailable` rather than as a decline on your payment method.
* **Rate limits:** API key traffic no longer counts against the owner's dashboard session.
* **Sandbox price breakdowns:** [Sandbox](/docs/v2/api-reference/introduction/sandbox) orders now return a real `price_components` instead of `null`.
Zinc now orders internationally wherever our retailers do, and the sandbox emits tracking checkpoints you can build against. Plus a refreshed [zinc.com](https://zinc.com).
### Wider International Coverage
You can now order to every country a retailer serves, and stores outside our catalog accept any address we can validate rather than US addresses alone. Per-retailer coverage is listed as `supported_countries` on [`GET /retailers`](/docs/v2/api-reference/retailers/list-retailers).
### Sandbox Tracking Checkpoints
[Sandbox](/docs/v2/api-reference/introduction/sandbox) orders now emit tracking checkpoints as they move — label created, in transit, delivered — in the same shape a live shipment returns, so you can build and test checkpoint handling before your first live order. Item statuses follow live orders too, sitting at `ordered` between placement and shipment.
### `max_age` Minimum
`max_age` must now be at least **31 seconds** on [`GET /products/{id}`](/docs/v2/api-reference/products/get-product), [`GET /products/{id}/offers`](/docs/v2/api-reference/products/get-product-offers), [`POST /agent/products/details`](/docs/v2/api-reference/agent/product-details), and [`POST /agent/products/offers`](/docs/v2/api-reference/agent/product-offers). Smaller values return a parameter error naming the constraint, and a `newer_than` timestamp is clamped to the same floor.
### Refreshed Site
[zinc.com](https://zinc.com) got a redesign, and [Zinc for Agents](https://agent.zinc.com) now has a page of its own.
Zinc has an MCP server! Point an AI assistant at it and it can search products, place orders, follow tracking, and top up the wallet — with a person confirming anything that spends money. Plus Solana as a payment rail for agents.
### The Zinc MCP Server
Connect an assistant to `https://mcp.zinc.com/mcp` and the whole ordering loop is available to it — no client to write. Nine tools across three domains:
* **Orders** — `list-orders`, `get-order`, `get-order-tracking`, `prepare-order`, `place-order`
* **Wallet** — `get-wallet-balance`, `prepare-topup`, `add-funds`
* **Search** — `search-products`
**Anything that spends money takes two steps.** The assistant prepares the order or top-up; you confirm it on a card only you can approve.
Orders, tracking, search results, and your balance come back as interactive cards rather than walls of text, in clients that support them.
### Pay with Solana
Agents can now pay in USDC on **Solana**, on both [x402 and MPP](/docs/v2/mpp). Every settlement is recorded by its transaction hash and each hash pays for exactly one thing.
### Idempotent Wallet Top-Ups
`POST /wallet/top-up` now takes an optional [`idempotency_key`](/docs/v2/api-reference/introduction/idempotency). Pass one and a retried deposit won't charge you twice.
### Improvements
* **Standard rate limit headers:** Responses now carry `RateLimit` and `RateLimit-Policy` per the IETF draft spec, on `429` and `402` too, so agent tooling can self-throttle without parsing our `X-RateLimit-` headers. The legacy headers are unchanged.
* **Return label webhook:** A new [`return.label_uploaded`](/docs/v2/api-reference/introduction/webhooks) event fires when a customer-visible return label is attached to a return, with a signed `label_url` you can hand straight to your customer.
* **Shipment emails:** You could already rewrite the headline and body copy in the emails we send your customers when an order ships — now you can hide either outright.
* **Bulk upload notifications:** Opt into an email when a bulk upload finishes, sent once every order in the batch reaches a terminal state.
### Bug Fixes
* **Multi-package orders:** Retailers that send one tracking email per package now record every package, not just the first.
An agent can now mint its own sandbox and place a test order without a signup in the loop. Plus Etsy, gift messages, and per-item price breakdowns.
### Agent Sandboxes
`POST /sandbox/keys` mints a provisional user and a `zn_test_` [sandbox](/docs/v2/api-reference/introduction/sandbox) key with no account required. The response comes back with a runnable example order and the ordered next steps — enough for an agent to place a test order on the first try. Rate limited to 5 mints per IP per day.
When the agent's user is ready to keep the work, the response's `claim_url` hands it over: signing in there merges the sandbox's order history and API key into that account, and the agent's key keeps working afterward. Claiming always requires an explicit confirmation naming the account doing the absorbing.
### Etsy
Etsy search and product details are live, joining the retailers you can query through the [product endpoints](/docs/v2/api-reference/products/search). Prices are returned in the listing's native currency, with a `currency_code` alongside them.
### Gift Messages
[`POST /orders`](/docs/v2/api-reference/orders/create-order) now takes an optional `gift_message` alongside `is_gift`, and Zinc types it into the retailer's own gift-message box at checkout — up to 240 characters, multi-line and emoji included.
Gift fulfillment is also a hard guarantee now: an `is_gift` order is only placed once the retailer's gift option is confirmed selected, and fails as `gift_option_unavailable` if it can't be.
### Per-Item Pricing and Merchant Order IDs
* **Per-item pricing:** Price breakdowns now include `cart_items`, a line item per product, so a multi-item order reconciles product by product instead of only in total. In the spec as `OrderPriceComponents.cart_items`.
* **Merchant order IDs:** [`GET /orders`](/docs/v2/api-reference/orders/list-orders) returns `merchant_order_ids` and accepts a `merchant_order_id` filter. The [Orders](https://app.zinc.com/orders) table gains the column and a `merchant_order_id:` search term.
### Improvements
* **Undeliverable packages:** A new `UNDELIVERABLE` tracking status marks packages the carrier or retailer has given up on — return-to-sender and the like — so they're distinguishable from packages still in transit.
### Bug Fixes
* **Headless Shopify storefronts:** Stores that serve their storefront separately from their Shopify backend — `ruggable.com` among them — now resolve instead of returning `invalid_shop`.
Zinc's product data is no longer limited to the retailers in our catalog. Point the product endpoints at **any** Shopify store — there are millions — and search it directly.
### Every Shopify Store, Searchable
**SKIMS**. **Gymshark**. **Fashion Nova**. **Bombas**. **Kylie Cosmetics**. Roughly a tenth of US ecommerce runs on Shopify, and until this week none of it was reachable unless we'd added the brand by hand. Now you just pass the store's domain as the `retailer`:
* [`GET /products/search?retailer=`](/docs/v2/api-reference/products/search) — predictive search across the storefront's catalog
* [`GET /products/{handle}?retailer=`](/docs/v2/api-reference/products/get-product) — per-variant pricing and availability, read from the store's own product API
Same request and response shapes you already use for supported retailers — no new client code, no catalog entry, no waiting on us.
Products are addressed by **BSIN** rather than the store's numeric SKU, so an item keeps one identifier across calls. Details still return the numeric `sku` too, for joining back to a search response.
A domain that isn't reachable — or isn't a Shopify store — comes back as a structured negative result rather than a generic failure. `invalid_shop` returns a `400` and `product_not_found` a `404`, each with an error code you can branch on.
### Find Orders by Metadata
The `metadata` you attach when creating an order is now searchable — in the API **and** in the dashboard.
From the [Orders](https://app.zinc.com/orders) page, type a `metadata:` term straight into the search bar:
```
metadata:po_number=ABC123
```
That's every order against that PO number, no export and no API call. The same filter is on [`GET /orders`](/docs/v2/api-reference/orders/list-orders) as `metadata_key` and `metadata_value`.
### Bug Fixes
* **Bank transfers and auto top-up:** ACH deposits now report `pending` while they settle instead of counting as failures — no repeat charges or false failure notifications during the 1–4 day settlement window.
* **MPP refunds:** Refunds now fire on every terminal path rather than one, cancelling an unbilled pending order returns the agent's prepayment, each refund posts a ledger entry so balances don't drift, and `order.placed` and its confirmation email no longer go missing.
* **Duplicate tracking numbers:** Legacy-pipeline orders no longer get a second ZPY number that could never pick up checkpoints.
* **Tracking emails:** More retailers' tracking emails now link themselves to the right order instead of needing manual assignment.
* **Order confirmation details:** Fixed orders completing with `merchant_order_id` as `UNKNOWN` and a zeroed price breakdown.
* **More reliable checkout:** Collapsed cart item lists are expanded before validation, orders that stall on a cart page are cut short, and quantity selectors are read correctly.
This week: Zinc can email your end customer as their order ships, and give them a tracking page of their own — no forwarding, and no templates to build on your side.
### Customer Email Notifications
Orders now accept an optional `customer_notifications` block with an `email`, and Zinc handles the rest:
* **A shipment email to your customer** when the order ships, with the estimated delivery date (in the subject line too), carrier, tracking number, an item summary, and the destination. Anything a retailer doesn't give us is left out rather than rendered blank.
* **A tracking page for that order**, linked from the email and served on `track.zinc.com`. It's scoped to the one order and shows no account details.
* **One-click unsubscribe**, per [RFC 8058](https://www.rfc-editor.org/rfc/rfc8058.html). Opting out applies to that order only, so the same address still hears about its other orders. Bounces and spam complaints suppress future sends on their own.
The add-on is **\$0.25 per order** on top of the base order fee, billed the same way the rest of your usage is — prepaid wallet, metered invoicing, or per-order agent payments.
Rolling out: `customer_notifications` is live on the API, but sending is still gated while we finish provisioning the sending domain. Until then the opt-in is accepted and ignored — nothing is emailed and the \$0.25 add-on isn't charged. We'll note it here when it's switched on.
### Bug Fixes
* **Dates and totals in the dashboard:** Order dates and numbers were formatted against the server's timezone instead of yours, showing the wrong local time and throwing a rendering error on every [Orders](https://app.zinc.com/orders) load.
This week we're shipping an official npm SDK, generated from the live API spec so it can't fall behind. Plus x402 on the agent data endpoints, and a batch of fixes to agent payments and Connect.
### npm SDK
Zinc now has an official JavaScript SDK, published as [`zinc`](https://www.npmjs.com/package/zinc) on npm and generated directly from the production API spec:
```bash theme={null}
npm i zinc
```
Versions are CalVer and track the API version itself (e.g. `2026.7.17`), publishing automatically whenever the API version bumps — so the SDK never drifts from the API it describes.
### x402 on the Agent Data Endpoints
All four agent data endpoints now accept **x402** alongside [MPP](/docs/v2/mpp):
* [`POST /agent/search`](/docs/v2/api-reference/agent/search) — cross-retailer search
* [`POST /agent/products/search`](/docs/v2/api-reference/agent/product-search) — per-retailer product search
* [`POST /agent/products/offers`](/docs/v2/api-reference/agent/product-offers) — offers for a product
* [`POST /agent/products/details`](/docs/v2/api-reference/agent/product-details) — product details
Every `402` now carries both challenges — `WWW-Authenticate` for MPP and `PAYMENT-REQUIRED` for x402 — so a client can pay with whichever rail it speaks.
### Improvements
* **Discounts and fees on order totals:** `PriceComponents` and `OrderPriceComponents` now include `discount` and `fees`, so a breakdown always foots to the charged total.
* **Tracking for Shopify stores:** Shopify orders that ship without a carrier tracking number now report status and an estimated delivery date.
### Bug Fixes
* **Refunds on failed data calls:** A paid `/agent` data call that fails is now refunded automatically instead of charging you for nothing, on both MPP and x402.
* **Agent orders validated before charging:** Structural checks now run before any charge, so an invalid order is rejected rather than charged.
* **MPP credential parsing:** The Shared Payment Token parser now reads the canonical `{"spt": ""}` payload emitted by `link-cli` and `mppx`.
* **Connect holds:** Holds are now released on every terminal failure path, including cancellations and retried placements.
* **Order timeline:** Retried and reversed placements now show the correct final state.
* **Free gift-with-purchase items:** Promotional `$0.00` items that retailers add to the cart no longer fail orders with `invalid_product_url`.
* **`max_price` and estimated shipping:** Orders are no longer rejected up front over shipping that isn't charged at checkout.
A cleanup week: we spent it squashing bugs and tightening up existing behavior across webhooks, returns, and the ordering pipeline. One new bit of visibility on the order page, and a lot of things that just work more reliably now.
### Webhook Deliveries on the Order Page
Every webhook delivery for an order now shows up right on its detail page, so you can see exactly what we sent without leaving the order. Each attempt lists its status, HTTP response code, event type, timestamp, and error details — and you can **resend** any delivery straight from the same view.
### Bug Fixes
* **Exactly-once lifecycle webhooks:** Fixed duplicate `order.started`, `order.placed`, and `order.failed` events. `order.started` now fires at the correct in-progress transition for every queue type, and `order.placed` / `order.failed` fire only on the first terminal transition — re-posted results and worker retries no longer re-notify.
* **One return per item:** Each order item can now be included in only one return request, preventing duplicate returns from being opened against the same item.
* **More reliable checkout:** A batch of fixes across several retailers: better address selection on accounts with a full address book, correct handling of quantity selectors, and better delivery-date parsing so valid orders aren't incorrectly blocked.
* **Fewer spurious order failures:** Improved reliability in the ordering pipeline to eliminate a class of transient failures.
### Pay with x402
AI agents can now pay for orders via **x402**, joining Tempo and Stripe as a supported rail on [MPP](/docs/v2/mpp). Every `402` challenge on `/agent/orders` now advertises x402 alongside the others.
Need just one rail? Pass an optional `?method=` parameter (`x402`, `stripe`, or `tempo`) to return a single `402` challenge — many HTTP clients mishandle the repeated `WWW-Authenticate` headers a multi-rail `402` returns. Omit it to advertise everything; an unknown value returns `400` with the available methods.
### Wire Funding
You can now fund your wallet by wire transfer, with deposits settling same-day (T+0). Find your wire details and memo code in [Settings](https://app.zinc.com/settings), and each deposit comes with a downloadable receipt.
### Carrier Delivery Estimates
Order reads now include an `estimated_delivery_date` on each `tracking_numbers` entry, parsed from the carrier's own estimate (USPS, UPS, FedEx). Amazon Logistics (`TBA…`) and other direct-delivery shipments now also get **Shipped** and **Delivered** checkpoints instead of empty tracking histories.
### Improvements
* **Auto top-up hardening:** Auto top-up now disables itself after 3 consecutive failed charges (firing a new `wallet.auto_topup.disabled` webhook), locks per payment method so a slow ACH deposit can't block a card top-up, and no longer enqueues duplicate top-ups on burst charges.
* **Clearer address errors:** `invalid_shipping_address` now names the specific missing component (e.g. a unit number), and orders missing only a unit number — where Google verified the building — are now accepted.
* **Tracking badge dropdown:** The dashboard tracking badge now copies the carrier or Zinc ZPY number and links to the carrier and 17track.
### Bug Fixes
* **Transaction status in Billing:** Failed top-up attempts in [Billing](https://app.zinc.com/billing) now show a status badge, struck-through amount, and decline reason, instead of looking like successful deposits.
This week: carrier-independent tracking numbers, an order timeline endpoint, on-demand wallet top-ups, an estimated-delivery webhook, and a redesigned dashboard.
### Zinc Tracking Numbers
Every shipment now gets a Zinc-issued **ZPY tracking number** at creation: one carrier-independent number per shipment, whatever carrier delivers it.
* Returned as `zinc_tracking_number` and `zinc_tracking_url` on order reads and in `order.tracking`, `order.shipped`, and `order.delivered` webhooks
* Publicly trackable via [17track](https://www.17track.net/en/carriers/zinc)
### Order Timeline
A new `GET /orders/{id}/timeline` endpoint returns an order's milestones (created, placed, tracking, shipped, delivered, plus failed and cancelled) as one ordered list, merging placement logs and carrier tracking state. Works on historical orders, no changes required.
### Redesigned Dashboard
The dashboard got a consistency pass and clearer structure: top-level pages show your data, Settings is where you configure things.
* **Tracking and Returns pages:** New views with stat bands, status filters, and search
* **Billing and Webhooks as data pages:** [Billing](https://app.zinc.com/billing) shows balance, deposits, and a filterable transaction history; [Webhooks](https://app.zinc.com/webhooks) shows the delivery log with success-rate stats
* **Reorganized Settings:** One config hub for account, notifications, payments, Connect, webhooks, API keys, and managed accounts
* **Stats at a glance:** Every page now leads with a stat band of sparkline cards and period-over-period deltas, so you can see how your account is trending
### Improvements
* **On-demand wallet top-up:** Orders no longer fail with a `402` when auto top-up is configured. On a shortfall, Zinc charges your saved payment method, rechecks the balance, and only returns a `402` if it still can't cover the order
* **Estimated delivery webhook:** A new `order.estimated_delivery_updated` webhook fires when a revised delivery ETA is detected for Amazon orders
* **Free-shipping filter:** Product search accepts an optional `free_shipping` param to return only free-shipping items, on both [`/products/search`](/docs/v2/api-reference/products/search) and the MPP agent variant
### Bug Fixes
* **ACH deposits labeled correctly:** ACH (`us_bank_account`) top-ups are no longer mislabeled as card charges; deposit and fee rows now show the real payment type
* **Clearer fulfillment flags:** Public `PublicRetailer` fields renamed: `zinc_managed` → `no_account_needed`, `supports_accounts` → `use_your_account`. The `supported` field was removed since the public catalog only lists published retailers
This week is all about visibility into your orders: full carrier tracking timelines on every order read, a persisted webhook delivery log, and a new `order.shipped` event, plus pay-per-call data endpoints for AI agents.
### Carrier Tracking Checkpoints
Order reads now return the full per-scan tracking timeline, so a single call gives you every carrier checkpoint.
* `GET /orders/{id}` always returns the complete `checkpoints` timeline (most recent first) and an authoritative `status` on each tracking number
* `GET /orders` includes `status` on every tracking number; add `?include=tracking_events` to opt into the full checkpoint timeline, so list payloads stay lean by default
### Webhook Delivery Log
A new [Webhooks](https://app.zinc.com/webhooks) dashboard page brings logs and configuration together in one place. Every delivery is now persisted, so you can see exactly what we sent, when, and how your endpoint responded.
### `order.shipped` Webhook
A new `order.shipped` webhook event fires when an order ships. Pair it with `order.placed`, `order.delivered`, and `order.cancelled` to follow the full order lifecycle without polling.
### Data Endpoints for Agents
AI agents can now pull product data from Zinc with per-call [MPP](/docs/v2/mpp) payments (\$0.01 each), no Zinc API key required. Any request that isn't paid receives a `402` payment challenge before parameter validation, so discovery probes are never misrouted.
* `GET /agent/search` — cross-retailer search (beta)
* `GET /agent/products/search` — per-retailer product search
* `GET /agent/products/offers` — offers for a product
* `GET /agent/products/details` — product details
Payments under \$0.50 offer only micropayment-capable methods (crypto / Tempo); card payments aren't available at that price point.
### Improvements
* **Offer controls in CSV bulk upload:** `is_gift`, `handling_days_max`, `condition_in`, and `condition_not_in` now work in [CSV bulk uploads](/docs/v2/api-reference/orders/create-bulk-upload), matching the single-order API
* **Richer validation errors:** Validation failures now return a typed `field_errors` breakdown (`field`, `code`, `message`, `received`, `expected`)
* **Clearer address rejections:** US and Canada state codes are normalized at order creation, with `address_validation_reasons` explaining why an address was rejected
* **One-click v1 migration:** Migrating from v1 now brings your credentials over and backfills 90 days of order history in one step
This week brings two new order constraints, plus [Stripe Connect](/docs/v2/connect) and account-settings improvements.
### Order Constraints
Two new ways to control which retailer offer an order is allowed to buy:
* **Filter by condition** — `condition_in` / `condition_not_in` restrict purchases to the item conditions you allow or exclude (`New`, `Refurbished`, `UsedLikeNew`, `UsedVeryGood`, `UsedGood`, `UsedAcceptable`)
* **Maximum delivery time** — `max_shipping_days` caps how long an order may take to arrive; Zinc only buys an offer estimated to deliver within that window
### Improvements
* **Connect charge on the order** — [`GET /orders/{id}`](/docs/v2/api-reference/orders/get-order) now includes a `connect` object with the charge breakdown and state, so there's no separate call (`null` for wallet orders)
* **Self-service account settings** — Change your account email and unlink OAuth sign-in providers (e.g. Google) directly from [settings](https://app.zinc.com/settings)
* **More resilient search** — `/search` no longer returns intermittent `502`s when our upstream provider is flaky
* **Ordering and tracking** — Fixed issues that could leave orders stuck in progress and made polling more tolerant of retailer rate limits
This week we're launching Zinc Connect, a new way to have your customers pay for orders directly, plus automatic gift options at checkout.
### Zinc Connect
Worried about the cash flow of keeping money in Zinc? With Zinc Connect, your customers can now pay for orders directly through Stripe. Just tell us your margin: we charge your customer, complete the order, and send you your cut in real time via Stripe Connect. A guided onboarding flow gets you set up end to end.
### Gift Options at Checkout
Orders are now automatically marked as gifts during checkout when the retailer offers a free "this is a gift" option. We only use the free gift flag, never paid gift wrap or gift messages, so your order totals stay unchanged.
### Improvements
* **Rate limit headers** — By request, API responses can now include the standard rate limit headers (`X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`) so your integration can pace itself
* **v1 to v2 migration assistance** — Moving from v1? We can now bring your historical orders, retailer credentials, and webhook URLs over to v2 for you — reach out to [support@zinc.com](mailto:support@zinc.com) to get started
This week we're launching bulk order upload in the dashboard — place an entire spreadsheet of orders in one go.
### Bulk Order Upload
You can now place many orders at once by dragging a CSV into the [dashboard](https://app.zinc.com/orders). Each row is one order — product URLs, quantities, a shipping address, and a max price — and Zinc validates the whole file up front, shows you a preview, and only places orders once you confirm. Every row runs through the same validation, routing, and billing as a single order, so bulk orders behave exactly like the ones you create individually.
As the batch runs you get live status for each row, and when it's done you can download a results CSV with the Zinc order ID, total, and tracking for every order. It's a faster, self-serve replacement for the legacy email-based file exchange.
And keep an eye out next week — we've been working on a new way to pay for orders that doesn't involve pre-funding a wallet. More soon.
This week we're opening up cross-retailer product search in beta, a redesigned product search experience in the dashboard, and smarter handling of partial cancellations and gift-card orders.
### [Cross-Retailer Search](/docs/v2/api-reference/search/cross-retailer) (Beta)
The new `GET /search` endpoint returns a single ranked list of buyable products across every supported retailer — Amazon, Walmart, Target, Best Buy, Home Depot, Lowe's, Costco, eBay, Wayfair, Macy's, and more — in one call. Each result's `url` is the orderable retailer URL, so you can pass it straight to [Create order](/docs/v2/api-reference/orders/create-order).
`/search` is in active development and may change before launch. We'd love your feedback — [book a 30-minute customer interview](https://cal.com/zinc-ian/customer-interview) to help shape it, and we'll send you a \$50 Amazon gift card as thanks.
### Redesigned Product Search
The dashboard's Products page is now a cross-retailer [**Search**](https://app.zinc.com/search) experience. Instead of picking one retailer at a time, you search across all of them at once, with results streaming in behind skeleton loading states. New product cards show brand-colored retailer tags and click-to-copy product IDs.
### Improvements
* **Partial cancellations** — Cancelling a single sub-order no longer cancels the entire order. Cancellations are now handled at the item level with proportional refunds, fixing an over-refunding issue
* **Gift-card orders** — Orders fully covered by a gift card or store credit (a \$0.00 total) are no longer blocked by the max-price safety check, so they process correctly
This week we're shipping fiat payments for AI agents via the Machine Payments Protocol, return lifecycle webhooks, and USPS tracking.
### Fiat Payments for Agents
Agents paying for orders via the [Machine Payments Protocol](/docs/v2/mpp) can now settle in USD using Stripe Payment Tokens (SPT) — no stablecoins, no crypto wallet required. When MPP [launched in March](#2026-03-27) agents had to pay in stablecoins on Tempo; with SPT, any agent that can hold a Stripe payment method can place orders on Zinc using regular fiat.
### Return Lifecycle [Webhooks](/docs/v2/api-reference/introduction/webhooks#return-events)
Four new webhook events cover the full return lifecycle — `return.created`, `return.approved`, `return.denied`, and `return.credited` — so you can react to return status changes without polling. Each payload includes a new `return_id` field alongside `order_id`, making it easy to route events to the right return.
### USPS Tracking
USPS joins FedEx, UPS, and our existing carriers as a directly-polled tracking source. Orders shipped via USPS now get the same accurate checkpoints, faster delivery confirmations, and full delivery history as everything else.
### Improvements
* **Returns in sandbox** — The [returns API](/docs/v2/api-reference/returns/create-return) now routes through sandbox in test mode, so you can exercise the full return flow without touching production
This week we're shipping a new `order.cancelled` webhook event, a meaningful round of improvements to returns, and reliability fixes across the ordering and wallet flows.
### `order.cancelled` Webhook
A new [`order.cancelled`](/docs/v2/api-reference/introduction/webhooks) webhook event fires when a retailer cancels an order after placement. The payload includes the cancellation reason and source so you can react without polling — your wallet is refunded automatically at the same time.
### Returns Improvements
Building on the [returns API and dashboard](#2026-04-03) we launched in March, returns now feel like a complete workflow:
* **Resolution notes** — When a return is closed, the notes from our review are now visible on the return
* **Filter orders by return status** — The orders dashboard now lets you filter by **open** or **closed** returns for at-a-glance visibility
### Improvements
* **Faster retailer-cancellation detection** — Retailer-side cancellations are now detected and refunded more quickly
* **Wallet top-up safeguards** — Additional protections around concurrent top-up requests
* **Pre-placement balance check** — Wallet balance is now verified against the retailer's final total (including tax and shipping) before the order is placed
This week we're launching FedEx and UPS tracking, a new `order.delivered` webhook event, and a redesigned status page.
### FedEx and UPS Tracking
Zinc now polls FedEx and UPS directly for delivery status, alongside our existing carrier coverage. You'll see more accurate checkpoints, faster delivery confirmations, and a full delivery history for every shipment.
### `order.delivered` Webhook
A new [`order.delivered`](/docs/v2/api-reference/introduction/webhooks) webhook event fires when every package on an order has been delivered. Pair it with `order.placed` and `order.failed` to close the loop on an order without polling.
### Status Page
The new [status page](https://zinc.com/status) gives you an at-a-glance view of platform health across five service tiers — Ordering, Tracking, Product Data, API, and Dashboard — with 90 days of history for each. Every tier is rolled up from real production health checks, so what you see is what we're seeing.
### Improvements
* **Per-item delivery status** — The orders dashboard now shows delivery status for each line item (delivered, shipped, ordered, processing, cancelled), so partial shipments are easier to track at a glance
A lighter week of feature work, focused on durability and scale. Order volume has grown a lot over the past few months, and we want Zinc to feel rock-solid at any size — so this week is about hardening the ordering pipeline and dashboard to be more resilient under that demand, with fewer transient failures and faster recovery when something does go wrong.
### Ordering Reliability
A handful of improvements to our ordering pipeline to keep success rates high as volume grows:
* **Faster tracking updates** — Order tracking is now polled hourly for up to 14 days after placement, so status changes land in your dashboard sooner
* **Smarter retries** — Transient retailer errors now retry with unique trial IDs to avoid duplicate-detection conflicts
* **More precise error mapping** — Out-of-stock products now surface as `product_unavailable` instead of a generic failure, with clearer messaging across the board
### Improvements
* **Full order history** — The orders dashboard now shows your complete order history with infinite scroll pagination, instead of being capped at the 50 most recent orders
* **Passkey warning** — A reminder to disable passkeys on retailer accounts is now shown when adding or editing a managed account
This week we're launching a redesigned billing page and automatic wallet credits from bank transfers.
### Billing Page
The new [billing page](https://app.zinc.com/billing) gives you a complete picture of your spending and deposits in one place:
* **Per-order cost breakdown** — Product cost and Zinc fee broken out for every order
* **Deposit history** — Every deposit with a direct link to its Stripe-hosted receipt for easier accounting
### Automatic Wallet Credits from Bank Transfers
ACH and wire transfers to your virtual Stripe account now credit your Zinc wallet automatically. No more emailing support to top up — send the transfer and your balance updates as soon as it arrives. Reversals are handled too.
### Improvements
* **Retailer-initiated cancellations** — Orders cancelled by the retailer are now tracked under their own status and automatically requeued for a fresh attempt when we detect a cancellation email
* **Full tracking numbers** — Tracking numbers in the dashboard are no longer truncated at 12 characters
* **Consistent timestamps** — All timestamps in the dashboard now render in your browser's timezone with consistent formatting
This week we're launching Teams — a shared view of orders across your teammates — plus email deliverability improvements and a faster orders dashboard.
### Teams
If your team places orders through Zinc, you can now see every teammate's orders in a single shared view, so you don't have to hop between accounts to find an order someone else placed.
Teams is rolling out gradually. To get set up, reach out to [support@zinc.com](mailto:support@zinc.com) and we'll create your organization and invite your teammates.
### Email Improvements
We've made order emails more useful:
* **Price breakdowns in receipts** — Order emails now include subtotal, shipping, tax, and total
* **Order placement emails are now opt-in** — Disabled by default due to deliverability issues; you can turn them back on from your email notification preferences
### Improvements
* **Faster orders dashboard** — Added pagination and tuned backend queries so the orders page stays snappy no matter how many orders you've placed
* **More reliable sign-in** — Fixed an issue where transient auth provider timeouts could briefly strip admin access; sessions now fall back to cached roles when the provider is slow
This week we're launching Amazon Prime support, plus email notification preferences and payment reliability improvements.
### Amazon Prime Support
Zinc now supports Prime shipping on Amazon orders. If you're interested in enabling this for your account, reach out to [support@zinc.com](mailto:support@zinc.com).
### Email Notification Preferences
You can now customize which email notifications you receive — including order placed and order failed alerts — directly from the dashboard settings.
### Improvements
* **Duplicate transaction prevention** — Resolved a rare issue that could cause duplicate charges during payment processing
* **Tracking improvements** — Better tracking number selection, with added support for znlogic tracking links
With returns and product data now available in v2, Zinc has reached full feature parity with the legacy v1 API. If you're still on v1, now's the time to [migrate](/docs/v2/migrating-from-v1) — v2 is the future.
### Return Request API
Zinc now supports returns through a new set of API endpoints. Create return requests, list them, and check their status — all programmatically.
* [`POST /returns`](/docs/v2/api-reference/returns/create-return) — create a return request for any completed order
* [`GET /returns`](/docs/v2/api-reference/returns/list-returns) — list all return requests
* [`GET /returns/{id}`](/docs/v2/api-reference/returns/get-return) — get the status of a specific return
Returns can also be managed from the new Return Management section in the dashboard.
### Product Data API
New product data endpoints are now available under `/products`, giving you programmatic access to product search, offers, and details.
* [**Product search**](/docs/v2/api-reference/products/search) across supported retailers
* [**Offer data**](/docs/v2/api-reference/products/get-product-offers) with pricing and availability
* [**Product details**](/docs/v2/api-reference/products/get-product) by URL or identifier
### Improvements
* **Timezone handling** — dates in the dashboard now display in your local timezone
This week we're launching support for the Machine Payments Protocol — enabling AI agents to pay for Zinc orders per-transaction, with no accounts or API keys required.
### Machine Payments Protocol (MPP)
Zinc now supports the [Machine Payments Protocol](https://mpp.dev/) (MPP) — an open standard for machine-to-machine payments over HTTP. Instead of requiring API keys, subscriptions, or upfront enrollment, MPP lets AI agents pay for each order individually as part of the same HTTP request that places it.
Here's how it works: an agent calls Zinc's order endpoint, receives an HTTP 402 payment challenge, fulfills the payment, and gets the order confirmation back — all in a single exchange. No checkout forms, no stored credentials, no human in the loop.
* **Stablecoin payments via Tempo** — Agents can pay per-transaction using stablecoins on [Tempo](https://tempo.xyz), with fast settlement and predictable costs
* **Stripe Payment Tokens** — Support for Stripe-based fiat payments via SPT coming soon
* **Automatic refunds** — If an order fails, the payment is automatically refunded
* **Partial refunds** — When the final order price is less than the agent's `max_price`, the difference is refunded
Try it out in the [Agent Playground](https://agent.zinc.com), or check out the [MPP guide](/docs/v2/mpp) and [create order endpoint](/docs/v2/api-reference/agent/create-order) to get started.
### Bug Fixes
* **Legacy v1 API fix** — Resolved an issue where quantity was hardcoded to 1 in legacy v1 API order requests
This week brings API key rotation, expanded retailer information, and improved order reliability — plus a preview of what's coming next.
### Coming Soon: Machine Payments Protocol (MPP)
We're building native support for the [Machine Payments Protocol](https://mpp.dev/) — a fully-agentic purchasing protocol that lets AI agents pay for orders programmatically. MPP support will go live early next week with Tempo and Stripe as payment methods, starting with Amazon and Zinc retailers. Stay tuned.
### API Key Rotation
You can now rotate your API keys directly from the credentials dashboard. Key rotation lets you cycle credentials without downtime — generate a new key, migrate your integrations, then revoke the old one.
### Expanded Retailer Information
The retailer status API now includes a `supported_countries` field, making it easier to programmatically check which countries each retailer can ship to.
### Improvements
* **Merchant order IDs in order results** — `merchant_order_id` is now included in order responses for easier cross-referencing with retailer records
* **Smarter order retries** — Retried orders now correctly preserve variant information, retailer credentials, PO numbers, and metadata from the original order
* **Webhook reliability** — Fixed sandbox webhook processing issues
* **Better international address autocomplete** — Improved address suggestions for international orders
* **More accurate order pricing** — Better extraction of order price details when payment currency differs from the listing currency
This week brings Great Britain shipping, smarter international currency handling, estimated delivery dates, and improved order reliability.
### Great Britain Shipping
Zinc now supports shipping to Great Britain, continuing our expansion of international address coverage. GB addresses are fully validated during order creation.
### International Currency Support
Orders placed on international retailers now automatically handle foreign currency conversion. Non-USD order totals are converted to USD at charge time with a configurable FX markup (3% default) applied to conversion rates as a buffer against exchange rate fluctuations. This means you can place orders on retailers in EUR, AUD, GBP, and other currencies without worrying about currency handling — Zinc takes care of it.
### Estimated Delivery Dates
When provided by the retailer, estimated delivery dates are now included in your order details.
### Improvements
* **More reliable merchant order IDs** — Fixed handling of edge cases where merchant order IDs aren't immediately available after checkout
* **Enhanced error handling** — More resilient parsing and retry logic for smoother order completion
This week brings Home Depot support, estimated delivery dates, international currency handling, and a new retailer status page.
### Home Depot Support
Zinc now supports placing orders on **Home Depot**, expanding our retailer coverage to one of the largest home improvement retailers.
### Estimated Delivery Dates
If provided by the retailer, Zinc will now extract estimated delivery dates during the checkout process and include them in your order details.
### International Currency Support
Orders placed on retailers that use non-USD currencies are now automatically converted to USD at charge time. This makes it easier to place orders on international retailers without worrying about currency handling.
### Retailer Status Page
We've added a user-facing [retailer status page](https://app.zinc.com/retailers) so you can monitor which retailers are currently supported and their operational status.
### Improvements
* **Better rural address support** — Fixed validation failures for rural and highway addresses where USPS data is limited
* **Clearer validation errors** — Enhanced error messages for invalid addresses
* **Max price display** — The order detail page in the dashboard now shows the max price set for each order
* **Direct product links** — Order details now include direct links to the product URL
* **Editable addresses** — Address fields are now editable in the create order flyout after selection
This week brings Canadian shipping support, saved payment selection for managed accounts, and improved tracking reliability.
### Canadian Shipping Addresses
Zinc now supports shipping to Canada, continuing our expansion of international address coverage. Canadian addresses are fully validated during order creation.
### Saved Payment Selection
When using managed accounts, you can now select which saved payment method to use for an order. If you have multiple cards on file with a retailer, you can specify exactly which one Zinc should use during checkout.
### LaserShip Tracking Support
Orders shipped via LaserShip now include tracking numbers, giving you visibility into deliveries from this regional carrier.
### Improvements
* **Better tracking extraction** — Improved tracking number and verification code extraction with fewer false positives
* **Smarter email filtering** — Enhanced filtering of emails before extracting tracking and verification data
* **More accurate merchant order IDs** — Improved extraction of merchant order IDs from retailer confirmations
This week brings expanded retailer support with account-less checkout, international shipping to new regions, and improved order reliability.
### Account-less Checkout for Walmart, Target & Wayfair
Until now, Zinc-managed checkout accounts were only available for Amazon. This week, we're bringing the same experience to **Walmart**, **Target**, and **Wayfair** — meaning you can place orders on these retailers without creating or managing your own accounts. Just provide a shipping address and payment method, and Zinc handles the rest using our managed credentials.
This is a major step forward: retailers that previously required you to bring your own account credentials are now accessible out of the box. And this is just the beginning — we'll be rolling out managed checkout to more retailers soon.
### International Shipping: New Zealand & Australia
Zinc now supports shipping to New Zealand and Australia, continuing our expansion of international address coverage. Addresses for both countries are fully validated during order creation.
### Payment Verification for Managed Accounts
Managed accounts now support retailer-specific payment verification. If a retailer requires additional payment confirmation during checkout, Zinc stores and uses the appropriate verification hints automatically.
### SMS Verification Code Support
Our checkout system now supports OTP retrieval from forwarded SMS verification codes, in addition to email-based codes. This improves success rates for retailers that use SMS-based authentication.
### Improvements
* **Smarter cart validation** — Fixed checkout failures caused by cart information no longer being displayed on the page
* **Better error codes** — Added a specific `payment_verification_required` error code for clearer debugging
This week's highlight is the launch of our Universal Checkout Skill for AI agents, plus auto wallet top-up and better order visibility.
### Universal Checkout Skill
Zinc is now available as an [Agent Skill](https://agentskills.io) — a new way for AI agents to place orders through natural language. Install the [Universal Checkout Skill](/docs/v2/agent-skills/overview) into [OpenClaw](https://openclaw.ai), Claude Code, or Gemini CLI, and your agent can search for products, place orders, and check statuses — all from a single conversation.
* **Natural language ordering** — Ask your agent to buy a product by URL, and it handles the rest
* **Search-to-purchase** — Pair with Brave Search to find and order products in one workflow
* **Confirmation built in** — The agent always confirms before spending real money
[Learn more →](/docs/v2/agent-skills/overview)
### Auto Wallet Top-up
Never run out of funds mid-order. You can now configure automatic wallet top-ups that trigger whenever your balance drops below a threshold. Set your minimum balance and top-up amount from the new wallet settings page, and Zinc handles the rest.
### Order Progress Timeline
Order details now include a visual timeline showing each step of the purchasing flow. See exactly where your order is and what's already been completed.
### Improvements
* **Email forwarding indicators** — A new badge shows when forwarded verification emails have been detected for your managed accounts
* **Better Amazon tracking** — Improved package tracking URL extraction from Amazon order emails
* **International address fixes** — Fixed address validation for countries that don't require a state or province field
This week's highlight is a brand new retailer status page, plus stronger account security and better order visibility.
### Retailer Status Page
Zinc now has a dedicated status page with nightly integration test results for our top retailers. We're committed to full transparency into what's working and what isn't — and we'll be expanding coverage to more retailers over time.
### TOTP Support for Retailer Credentials
Managed retail accounts now support Time-based One-Time Passwords (TOTP). If your retailer account uses two-factor authentication with an authenticator app, you can provide your TOTP secret and Zinc will automatically generate codes during login — no more manual verification steps.
### Improved Order Progress
Orders now report more granular progress updates as they move through each step of the purchasing flow, giving you better real-time visibility into order status.
This week we're focused on reliability and expanding Zinc's reach with international shipping support and B2B features.
### International Order Support
Zinc now supports shipping to Germany, with more countries coming soon. International addresses are validated during order creation, and we've updated our date and currency formatting to handle international locales correctly.
* **Country configuration** — Each retailer now has configurable supported shipping countries using ISO 3166-1 alpha-2 codes
* **Address validation** — International addresses are validated before order creation
* **Flexible formats** — Postal codes work with both `zip_code` and `postal_code` fields, and state/province fields are now optional for countries that don't require them
### Purchase Order Support
For B2B customers, you can now include a purchase order number with your orders. Just pass an optional `po_number` field and we'll automatically detect PO number fields during checkout and fill them in.
### Product Variant Fixes
Fixed an issue where variant verification was failing for products that have pre-selected variants on the page. Variants should now work correctly across all supported retailers.
### Launch Week 1
**January 20-24** — Zinc's first launch week. Five days. Five releases.
We're rapidly expanding Zinc 2.0's feature-set, vastly improving devex, and showing off some impressive demos to inspire you on what you can build with Zinc.
Pay attention to your inboxes and our socials for Launch Week 1!
***
### Account Selection in Orders
The order form now includes a dropdown to choose between Zinc-managed accounts and your own retailer credentials. You can see exactly which account will be used before placing an order.
* **Visual account picker** — Select from your saved retailer accounts directly in the order form
* **Curl command preview** — See the exact API call that will be made, with one-click copy
### Easier Gmail Setup
Setting up email forwarding for verification codes just got simpler. A new "Add Gmail Filter" button generates the exact filter you need with one click.
### Smarter Retry Logic
We've improved how failed orders are retried to be more intelligent about when retries make sense.
* **Order-level tracking** — Retry attempts are now tracked at the order level for better visibility
* **Skip hopeless retries** — Orders that fail due to out-of-stock products, inaccessible items, or unavailable guest checkout no longer waste time retrying
This week we're introducing managed retail accounts and smarter payment handling. These features give you more control over how orders are placed and make credential management significantly easier.
### Managed Retail Accounts
You can now use managed retail accounts to place orders with your own retailer credentials. This gives you more control over order placement and unlocks features that require being logged in.
* **Amazon account support** — Orders placed with Amazon credentials now use our enhanced processing system for better reliability
* **Forwarding email addresses** — Each managed account automatically gets a dedicated email address for verification codes and tracking notifications
* **Easy credential reference** — Use short IDs to quickly identify and reference your saved retail credentials
* **Explicit credential selection** — Specify exactly which credentials to use with the `retailer_credentials_id` parameter
### Smarter Payment Handling
When using managed accounts, Zinc now automatically uses your saved payment method if you don't provide one explicitly. This means less redundant data in your API calls.
* **Automatic payment detection** — Orders with managed accounts can use saved payment methods without passing payment details each time
* **Flexible payment modes** — Switch seamlessly between saved and explicit payment methods based on your needs
* **Clear error messages** — Get helpful `payment_method_required` errors when a saved method is needed but doesn't exist
### Streamlined Onboarding
We've simplified the getting-started experience to help you start testing faster.
* **Skip optional steps** — Skip non-essential onboarding steps and dive straight into testing
* **Faster setup** — Streamlined flow gets you to your first test order more quickly
### Better Error Tracking
Enhanced error reporting throughout the platform helps you debug issues faster with more specific error types and clearer messages.
A lighter week with focused improvements to order search and price clarity, plus continued infrastructure work.
### Better Order Search
You can now search for orders by order ID, making it easier to find specific orders in your dashboard.
### Clearer Price Warnings
When an order exceeds your maximum price threshold, you'll now see a clear "Max Price Exceeded" warning instead of a generic failure message. This makes it easier to understand why an order didn't complete.
### Infrastructure Improvements
We've been working on backend improvements to support future features:
* **Multi-worker architecture** — Building distributed systems for better scalability
* **Enhanced authentication** — Improving support for retailer accounts that require login
* **Better error tracking** — Expanding our monitoring to catch and fix issues faster
This week brought intelligent product variant interpretation and expanded financial management capabilities. The highlight: we can now automatically understand and parse product variants from plaintext strings.
### Product Variant Interpretation
The biggest update this week is our ability to intelligently interpret product variants from plaintext strings. Instead of requiring structured data, you can now provide variant information as natural text and we'll automatically parse it into the correct format.
* **Intelligent parsing** — Automatically interpret size, color, and style options from plaintext strings
* **Flexible input** — No need to pre-structure variant data, just provide the information naturally
* **Clear label/value pairs** — Properly formatted variants for accurate order placement
### Better Test Order Flow
We've streamlined the in-app test order experience with rich previews and variant management.
* **URL preview cards** — See product images and details before placing test orders using Open Graph metadata
* **Variant support** — Test orders now fully support product variants with the new interpretation system
* **Mobile-optimized** — Fully responsive order interface that works seamlessly on mobile devices
### Improvements
* **Better error messages** — Clearer feedback when payment or order issues occur
* **Fixed retry logic** — Order retry now works correctly and only triggers for failed orders
We finally released the latest version of Zinc. This initial release brings dramatically expanded retailer support, along with key order management features.
### V2 Beta Launch
Zinc 2.0 beta is now available with a modern API and dashboard. This release dramatically expands our retailer coverage.
* **More Retailers:** Dramatically expanded retailer support across the world's top online stores
* **Modern API:** Clean, consistent API design for better developer experience
Some v1 features (account automation, tracking, returns) are coming soon. See [migrating from v1](/docs/v2/migrating-from-v1) for details.
### Order Management
You can now cancel orders directly from the dashboard and retry failed orders through the API. This gives you more control over your order lifecycle without needing to contact support.
* **Cancel orders** — Stop orders before they're fulfilled when plans change (available in dashboard)
* **Retry failed orders** — Automatically retry orders that failed due to temporary issues (available via API)
### Amazon Integration
For customers who need Amazon's full feature set during the v2 transition, orders can now be automatically proxied through our v1 API. This ensures continuity while we complete v2 feature parity.
### Platform Improvements
We've also made several improvements to the overall platform experience:
* **Better order display** — Improved how order details are shown in the dashboard
* **Payment notifications** — Clearer notifications when payment methods need attention
This week brought improvements to wallet management and ordering reliability. We're making it easier to manage your account balance and ensuring orders succeed more consistently.
### Wallet Management
You can now add funds directly to your wallet using your saved payment methods. Previously, you had to contact support to top up your account — now you can do it yourself in seconds.
We've also added detailed descriptions to all wallet transactions, so you can see exactly what each charge was for (API fees, order costs, etc.).
### More Reliable Orders
Orders now automatically retry when they encounter temporary issues, making the overall process more reliable. We've also made several improvements:
* **Smarter price checking** — We now validate prices only at the final checkout step, reducing premature order failures
* **More accurate pricing** — Enhanced price extraction logic handles varied retailer formats more reliably
# Introduction
Source: https://www.zinc.com/docs/index
Zinc lets you search, buy, and return items from top online retailers with a single API.
Get up and running with Zinc API in minutes.
Learn about Zinc, our mission, and how our API powers e-commerce automation.
Connect Claude, ChatGPT, or Cursor to your Zinc account and shop from chat.
Mint a test key with no account and place a sandbox order in two minutes.
Explore all endpoints, request/response formats, and integration details.
Join our Discord community for support, updates, and discussion.
# Quickstart
Source: https://www.zinc.com/docs/quickstart
Get started with Zinc API in minutes.
Sign up here to get started. You will need to deposit funds into your Zinc account to use the API.
After signing up, visit your Zinc dashboard to find your client token.
Here's h ow to place an order using `curl`:
```bash theme={null}
export ZINC_API_KEY=;
curl -X POST https://api.zinc.com/orders \
-H "Authorization: Bearer $ZINC_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"products": [{
"url": "https://www.zinc.com/shop/stickers",
"quantity": 1
}],
"max_price": 100,
"shipping_address": {
"first_name": "...",
"last_name": "...",
"address_line1": "...",
"address_line2": null,
"city": "...",
"state": "...",
"postal_code": "...",
"phone_number": "..."
}
}';
```
Replace \ with your actual token. Never share your client token publicly.
If successful, you'll get a successful JSON response with the request data. The order will now show
up in your dashboard.
***
Need help? Contact support or check out the [What is Zinc?](/docs/what-is-zinc) page for more info.
# Going Live
Source: https://www.zinc.com/docs/v2/agent-sandbox/going-live
Claim a sandbox into a real Zinc account without breaking the agent that built it, then swap in a live key.
Going live is two separate things, and it helps to keep them apart:
1. **Claiming** — a human adopts the sandbox into their Zinc account. The agent's key survives.
2. **Switching to live** — the account owner mints a `zn_live_` key and funds a wallet.
You can do the first without the second. Claiming preserves work; it doesn't spend money or enable real orders.
## Claiming
The mint response included a `claim_url`. Give it to the human you work for:
> "I've built and tested the ordering flow against a Zinc sandbox. To adopt it into your account, sign in here: `https://app.zinc.com/claim/...`"
When they sign in there, the sandbox folds into their Zinc account:
| | Result |
| ------------------- | ------------------------------------------------------ |
| Sandbox orders | Repointed to their account — the test history survives |
| The agent's API key | **Reassigned, not revoked** |
Not revoking the key is the entire point. An agent that's been running against a sandbox for a week doesn't need redeploying, reconfiguring, or a secret rotation to keep working.
Claiming is always a merge into the signed-in account — signing in creates the account if it didn't exist, so there's always something to merge into.
The claim token is **single-use** and lives only in that URL. Only its hash is stored, so the URL can't be reproduced — if it's lost, mint a new sandbox and hand over the new one.
### Knowing when it happened
An agent that hands off a URL can't see what the human did next. Poll for it:
```bash theme={null}
curl https://api.zinc.com/sandbox/status \
-H "Authorization: Bearer $ZINC_KEY"
```
| `claimed` | Meaning |
| --------- | ---------------------------------------------------------------------------- |
| `false` | Still provisional — nobody has claimed it. Keep waiting, or re-send the URL. |
| `true` | Claimed. `claimed_by_email` names the account it now belongs to. |
Every response carries a plain-language `hint`, so an agent can act on the answer without special-casing each shape.
## Switching to live
**A claimed key is still a sandbox key.** It reaches sandbox data only, claimed or not. Claiming moves ownership; it does not grant the ability to spend.
To place real orders, the account owner:
Deposit a balance in the [dashboard](https://app.zinc.com). Each order draws its cost plus the account's order fee. See [Wallet](/docs/v2/wallet) for the fee schedule, or [Stripe Connect](/docs/v2/connect) and [MPP](/docs/v2/mpp) for the other payment rails.
A `zn_live_` key, from the same dashboard.
The API surface is identical. Change the key, keep the code.
## What changes when you switch
The request and response shapes don't change. The environment around them does:
| | Sandbox | Live |
| ----------------------- | --------------------------- | ---------------------------------------------------- |
| Money | Nothing is charged | Every order draws from a wallet |
| Balance checks | Skipped | Enforced — an underfunded order is rejected |
| URL reachability | Skipped | Enforced |
| Address verification | Skipped | Enforced against external validation |
| Retailer URL validation | Skipped | Enforced |
| Timing | Compressed — minutes | Real — orders take 5–10 minutes, delivery takes days |
| Failures | Only the ones you asked for | Whatever the retailer does |
Because the sandbox skips those validations, your first live order can fail in ways no test did — a rejected address, an unreachable URL, a real out-of-stock. Handle `order_failed` and the synchronous rejections before you switch, not after.
## Before your first live order
* **Handle both failure timings.** Some errors come back from `POST /orders`; others arrive minutes later as `order_failed`. See [Error Handling](/docs/v2/api-reference/introduction/error-handling).
* **Set `max_price` to cover the real total.** Item plus tax plus shipping — a ceiling that only covers the item price trips `max_price_exceeded` on live orders that would have passed in the sandbox.
* **Re-register webhooks.** Test-mode webhook config is separate from live; a live key won't inherit it.
* **Send an idempotency key.** On live orders, a retry without one is a second order. See [Idempotency](/docs/v2/api-reference/introduction/idempotency).
* **Start small.** One cheap real order end to end before you turn on volume.
## Reference
`POST /sandbox/claim` — request and response by field.
`GET /sandbox/status` — every response shape.
# Agent Sandbox
Source: https://www.zinc.com/docs/v2/agent-sandbox/overview
Let an agent get a working Zinc key with no signup, place a test order, and graduate to a real account without changing its code.
The **agent sandbox** removes the one step an autonomous agent can't take: signing up.
An agent that knows nothing but the Zinc hostname can mint its own sandbox key in a single unauthenticated call, place a test order, and watch it deliver — no account, no dashboard, no human. When it's time to go live, a human claims the sandbox into their real Zinc account and **the agent's key keeps working**.
```bash theme={null}
curl -X POST https://api.zinc.com/sandbox/keys
```
That's the whole prerequisite. The response carries a `zn_test_` key, a complete example order body, and a URL a human can use later to adopt the sandbox.
## Why it works this way
Every other path to an API key assumes a person: a signup form, an email confirmation, a dashboard visit. An agent evaluating whether it *can* buy something has none of those, and asking it to stop and wait for a human is what kills the integration before it starts.
So the funnel is inverted. The agent proves the integration works first, against sandbox data that costs nothing and risks nothing. The human shows up only at the end, to decide whether to adopt it — and adopting it doesn't invalidate anything the agent already built.
The mint response doubles as documentation. Agents read the bytes they get back, not a docs site, so every field they need to proceed — the key, a valid order body, the ordered next steps — is in the response itself.
## The shape of it
One unauthenticated `POST`. Returns a sandbox key, an example order, and a claim URL.
Place sandbox orders, watch the real lifecycle on a compressed clock, and rehearse every failure mode against magic product URLs.
Hand the claim URL to the human you work for. The sandbox — order history and key — folds into their Zinc account.
They fund a wallet and mint a live key. The agent swaps one string. Nothing else changes.
## What a sandbox key can and cannot do
| | |
| ---------- | -------------------------------------------------------------------------------------------------- |
| **Can** | Place and read sandbox orders, register test-mode webhooks, reproduce every documented error shape |
| **Cannot** | Touch live mode, spend money, or place a real order — claimed or not |
A minted account is **provisional**. It has no wallet and needs none: sandbox orders skip funding entirely. It isn't a signup either — no email required, nothing sent, no live account created.
Sandbox keys **expire after 7 days without use**, and everything they created is sandbox data that's deleted with them. Any use resets the clock, so persist the key across runs rather than minting a fresh one each time.
## Who this is for
* **Agent builders** — get to a working order in the first session, decide later whether to open an account.
* **Platform and framework authors** — ship a Zinc integration your users can try with zero configuration.
* **Anyone evaluating Zinc** — exercise the real API surface, including the failure paths, before talking to us.
If you already have a Zinc account, you don't need any of this: mint a `zn_test_` key in the [dashboard](https://app.zinc.com) and read [Sandbox & Testing](/docs/v2/api-reference/introduction/sandbox) instead. The agent sandbox exists for the case where there's no account yet.
## Where the docs live
| | |
| -------------------------------------------------------------------- | ------------------------------------------------------------- |
| [**Quickstart**](/docs/v2/agent-sandbox/quickstart) | The walkthrough — mint, order, watch, rehearse failures |
| [**Going Live**](/docs/v2/agent-sandbox/going-live) | Claiming a sandbox and graduating to a live key |
| [**API Reference**](/docs/v2/api-reference/sandbox/overview) | The four `/sandbox/*` routes, request and response by field |
| [`GET /sandbox/quickstart`](https://api.zinc.com/sandbox/quickstart) | The same walkthrough as plain markdown, for the agent to read |
## Next steps
Zero to a delivered sandbox order in about two minutes.
Claim the sandbox and swap in a live key.
# Agent Sandbox Quickstart
Source: https://www.zinc.com/docs/v2/agent-sandbox/quickstart
Go from nothing to a delivered sandbox order in about two minutes — no account, no signup, no configuration.
No account needed. Four steps, about two minutes.
Pointing an agent at this instead of reading it yourself? The same walkthrough is served as plain markdown at [`GET /sandbox/quickstart`](https://api.zinc.com/sandbox/quickstart), written to be executed rather than read.
```bash theme={null}
curl -X POST https://api.zinc.com/sandbox/keys
```
No authentication. An empty body works; both fields are optional:
```bash theme={null}
curl -X POST https://api.zinc.com/sandbox/keys \
-H "Content-Type: application/json" \
-d '{
"name": "acme-shopping-agent dev testing",
"email": "operator@example.com"
}'
```
* `name` becomes the key's label, so a human claiming it later can tell what it was for.
* `email` is who should be told about claiming this sandbox. Never required, never used for anything else.
The response is self-guiding:
```json theme={null}
{
"api_key": "zn_test_...",
"expires_policy": "This key expires after 7 days without use. Everything it creates is sandbox data and is deleted with it.",
"quickstart_url": "https://api.zinc.com/sandbox/quickstart",
"example_order": {
"products": [ ... ],
"shipping_address": { ... },
"max_price": 2500
},
"claim_url": "https://app.zinc.com/claim/...",
"next": [
"POST /orders with this key ...",
"..."
]
}
```
Save the `claim_url`. It's returned **once** — only its hash is stored, so it can't be regenerated. Losing it means minting a new sandbox.
Send the key as a Bearer token. Test keys route to the sandbox automatically — no extra headers, no mode flag:
```bash theme={null}
export ZINC_KEY=zn_test_...
curl -X POST https://api.zinc.com/orders \
-H "Authorization: Bearer $ZINC_KEY" \
-H "Content-Type: application/json" \
-d '{
"products": [
{"url": "https://zinc.com/shop/products/test-success", "quantity": 1}
],
"shipping_address": {
"first_name": "Sandbox", "last_name": "Agent",
"address_line1": "101 Market St", "city": "San Francisco",
"state": "CA", "postal_code": "94105",
"phone_number": "4155552671", "country": "US"
},
"max_price": 2500
}'
```
That body is exactly what the mint response hands back as `example_order` — it can be sent unchanged.
`max_price` is in **cents** and is required on every order. It's the ceiling you authorize for the total, including tax and shipping. Leave `idempotency_key` out unless you're generating a fresh UUID for it; one is generated for you when omitted.
```bash theme={null}
curl https://api.zinc.com/orders/ \
-H "Authorization: Bearer $ZINC_KEY"
```
Sandbox orders run the real lifecycle on a compressed clock:
| When | What changes |
| --------------- | --------------------------------------------------------------------------------------------------------------------- |
| \~30–120s | `status` becomes `order_placed` — that **is** the terminal status of a successful order (`order_failed` if it didn't) |
| \~1–3 min later | A tracking number attaches, and `tracking_numbers[].status` advances through `shipped` to `delivered` |
Prefer not to poll? Register a webhook URL — test mode has its own webhook config, separate from live:
```bash theme={null}
curl -X POST https://api.zinc.com/users \
-H "Authorization: Bearer $ZINC_KEY" \
-H "Content-Type: application/json" \
-d '{"webhook_url": "https://your-server.example/webhooks/zinc"}'
```
Swap the product slug to reproduce each error shape. This is the part worth spending time on — a happy-path integration meets its first real failure in production.
Some fail **at creation**, some fail **after acceptance**:
| Product slug | Fails |
| --------------------------- | ---------------------------------------- |
| `test-success` | Nothing — places, ships, delivers |
| `test-invalid-address` | At creation |
| `test-url-unreachable` | At creation |
| `test-insufficient-funds` | At creation |
| `test-price-exceeded` | After placement — watch the order status |
| `test-out-of-stock` | After placement |
| `test-invalid-variant` | After placement |
| `test-shipping-unavailable` | After placement |
All slugs live under `https://zinc.com/shop/products/`. Any other product URL — including a real Amazon one — simply succeeds in the sandbox. See [Sandbox & Testing](/docs/v2/api-reference/introduction/sandbox#test-products) for the error payloads each one produces.
## Practical notes
* **Persist the key.** It's good for 7 days of inactivity and any use resets the clock. An agent that stores its key needs the mint endpoint roughly once.
* **Don't loop on `429`.** The mint endpoint is capped per IP per day. A `429` means *reuse the key you already have*, not retry — wait out `Retry-After` if you genuinely need a new one.
* **`503` is retryable.** The mint path fails closed when the sandbox or its rate limiter is unavailable, rather than running uncapped. Back off and try again.
* **Sandbox validations are looser than live.** Balance checks, URL reachability, and address verification are all bypassed. See [Skipped Validations](/docs/v2/api-reference/introduction/sandbox#skipped-validations) before your first real order.
## Next step
Claim the sandbox into a real account and swap in a live key.
# Universal Checkout Skill
Source: https://www.zinc.com/docs/v2/agent-skills/overview
Let AI agents discover, buy, track, and return products with the Zinc API.
The **Universal Checkout Skill** is an [Agent Skill](https://agentskills.io) that lets AI agents shop through the [Zinc API](https://zinc.com) — search for products, place orders, track them, and handle returns across Amazon, Walmart, Target, Best Buy, eBay, and 50+ other US retailers, all through natural language.
## What is an Agent Skill?
An Agent Skill is a declarative specification (a `SKILL.md` file) that teaches AI agents how to interact with an API. There's no executable code — the skill provides instructions, endpoint definitions, and safety guidelines that the agent follows.
Agent Skills use **progressive disclosure**: at startup only the skill name and description are loaded. The full instructions are read into context only when the agent detects a matching task, keeping your agent fast and focused.
## One skill, or one per retailer
The skill comes in two flavors, both from the same [`zincio/skills`](https://github.com/zincio/skills) catalog:
* **`universal-checkout`** — the full lifecycle across every supported retailer. Install this for general use.
* **Per-retailer skills** (`amazon-checkout`, `walmart-checkout`, `target-checkout`, …) — the same lifecycle retargeted for a single store. Handy when an agent only ever buys from one retailer.
Installing several near-identical per-retailer skills in one agent can make
skill triggering ambiguous. If you buy across retailers, install
**`universal-checkout`** rather than stacking retailer skills.
## What Can It Do?
| Capability | Description |
| ------------ | ---------------------------------------------------------------------------------------- |
| **Discover** | Search across retailers (`GET /search`) and compare best-price offers (Amazon & Walmart) |
| **Buy** | Place orders by product URL, with variants, condition filters, and a `max_price` ceiling |
| **Track** | Check status and the full carrier checkpoint timeline for an order |
| **Cancel** | Cancel an order while it's still queued |
| **Return** | Open a return against a placed order and get prepaid label URLs |
Placing an order or opening a return spends real money. The agent always
confirms with you before submitting.
## Two ways to pay
The skill supports either authentication method — pick based on how the user wants to pay:
| Method | When to use |
| ----------------------------------- | ------------------------------------------------------------------------------------------- |
| **API key** (`ZINC_API_KEY`) | Pre-registered users with a funded Zinc account |
| **MPP** (Machine Payments Protocol) | No Zinc account — pay per request via **Stripe** (cards/wallets) or **Tempo** (stablecoins) |
With MPP, the same `/agent/*` endpoints handle both ordering and metered data calls (\$0.01/call), so an agent can discover and buy with no account at all. Try it without code at [agent.zinc.com](https://agent.zinc.com).
## Supported Platforms
Works with any agent platform that supports the [Agent Skills](https://agentskills.io) standard, including:
* [Claude Code](https://claude.ai/code)
* [OpenClaw](https://openclaw.ai)
* [Cursor](https://cursor.com)
* [Gemini CLI](https://github.com/google-gemini/gemini-cli)
* VS Code, GitHub Copilot, and [many others](https://agentskills.io/home)
## Prerequisites
You'll need **one** of:
1. A **Zinc API key** ([sign up at app.zinc.com](https://app.zinc.com)) with funds deposited, **or**
2. An **MPP payment method** — a funded Tempo wallet key (`TEMPO_PRIVATE_KEY`) or a Stripe payment method. No Zinc account required.
## Using a chat client instead?
If your agent is a chat client — Claude, ChatGPT, Cursor — the [**Zinc MCP server**](/docs/v2/mcp/overview) covers the same ground with no install and no API key: paste one URL, sign in with your Zinc account, and each step renders as an interactive card you click to confirm. The skill remains the better fit for coding agents, headless automation, and buying with no Zinc account at all over [MPP](/docs/v2/mpp).
## Next Steps
Install and configure the skill for your platform.
Discover products, place orders, track them, and handle returns.
# Install the Skill
Source: https://www.zinc.com/docs/v2/agent-skills/setup
Install and configure the Universal Checkout Skill so your AI agent can buy and track orders through the Zinc API.
## Installation
Skills live in the [`zincio/skills`](https://github.com/zincio/skills) catalog and install with the [`skills`](https://github.com/vercel-labs/skills) CLI.
Install the universal skill (covers every retailer):
```bash theme={null}
npx skills add zincio/skills --skill universal-checkout
```
Or install a single retailer:
```bash theme={null}
npx skills add zincio/skills --skill amazon-checkout
```
List everything in the catalog:
```bash theme={null}
npx skills add zincio/skills --list
```
The CLI works with Claude Code, Cursor, OpenClaw, Gemini CLI, and other Agent Skills–compatible platforms.
Clone the catalog and point your agent at the `skills/` directory:
```bash theme={null}
git clone https://github.com/zincio/skills.git
```
Compatible agents automatically discover skills in the workspace `skills/` folder.
OpenClaw loads skills from the workspace (`./skills/`), user (`~/.openclaw/skills/`), and bundled locations. Use the `skills` CLI above, or clone a single skill into a skills directory. OpenClaw hot-reloads when `SKILL.md` changes — no restart needed.
Additional skill directories can be configured via `skills.load.extraDirs`.
## Configuration
Set up **one** authentication method, depending on how you want to pay.
For a pre-registered Zinc account, set the `ZINC_API_KEY` environment variable before starting your agent:
```bash theme={null}
export ZINC_API_KEY=your-api-key
```
Get your key from the Zinc dashboard and deposit funds to place orders.
With [Machine Payments Protocol](/docs/v2/mpp) you pay per request — no Zinc account needed. The skill uses the `/agent/*` endpoints, and the MPP client handles the 402 → pay → retry flow automatically. Pay via:
* **Tempo stablecoins** — set `TEMPO_PRIVATE_KEY` to a funded wallet key.
* **Stripe** — cards/wallets via Stripe Link (Shared Payment Tokens).
Install a client library so your agent can complete payments: `pip install pympp` (Python) or `npm install mppx viem` (TypeScript). Try it without code at [agent.zinc.com](https://agent.zinc.com).
### OpenClaw configuration file
If you're using OpenClaw, you can set the API key through the config file instead of an environment variable:
```json ~/.openclaw/openclaw.json theme={null}
{
"skills": {
"entries": {
"universal-checkout": {
"enabled": true,
"env": {
"ZINC_API_KEY": "your-api-key"
}
}
}
}
}
```
## Verifying the Installation
Start your agent and ask:
> "List my recent Zinc orders."
If the skill loaded correctly, the agent makes a `GET /orders` request and returns your order history. An authentication error usually means `ZINC_API_KEY` isn't set (or, on MPP, that no payment method is configured).
## Updating
Re-run the install command to pull the latest version:
```bash theme={null}
npx skills add zincio/skills --skill universal-checkout
```
# Using the Skill
Source: https://www.zinc.com/docs/v2/agent-skills/usage
Use the Universal Checkout Skill to discover products, place and track orders, and handle returns with your AI agent via the Zinc API.
Once the Universal Checkout Skill is [installed and configured](/docs/v2/agent-skills/setup), you can drive the full shopping lifecycle through natural language. This guide covers the key workflows.
## Finding Products
Don't have a product URL? The skill searches across retailers for you via the Zinc API — just ask:
> "Search for a highly-rated French press coffee maker under \$40."
> "Find me a USB-C hub with at least 3 ports."
The agent calls `GET /search`, which returns directly **orderable URLs** across Amazon, Walmart, Target, Best Buy, Home Depot, and more. Pick one and continue to checkout in the same conversation. For Amazon and Walmart, the agent can also compare best-price offers before ordering.
No setup or separate search tool required — product discovery is built into
the skill. On the MPP rail, search and product-data calls are metered at
\$0.01 each.
## Placing an Order
To place an order, provide your agent with:
* A **product URL** from a supported retailer (or a search query, above)
* A **shipping address**
* A **maximum price** — the total ceiling **including tax and shipping**
**Example prompts:**
> "Search for a French press coffee maker under \$40, then order the best option. Ship it to Jane Doe, 123 Main St, San Francisco, CA 94105."
> "Buy this: [https://www.amazon.com/dp/B0EXAMPLE](https://www.amazon.com/dp/B0EXAMPLE) — ship to Jane Doe, 123 Main St, San Francisco, CA 94105. Don't spend more than \$50."
The agent constructs the request and **asks for confirmation before submitting**, since placing an order spends real money.
`max_price` is the **total** ceiling — item price **plus tax and shipping**.
Below a retailer's free-shipping threshold, shipping is added to the total, so
leave room for it or the order trips `max_price_exceeded`.
### Variants, quantity, and condition
* **Variants** — "Find a Nike Dri-FIT t-shirt in size Large and color Blue, then order it."
* **Multiple products / quantity** — "Order 2 packs of AA batteries and 1 USB-C cable together. Ship to…"
* **Condition** — "Order this, but a used-like-new copy is fine if it's cheaper." (the agent allows used/refurbished conditions to find a cheaper offer)
## Checking Order Status
Orders process asynchronously and typically take 5–10 minutes. Ask:
> "What's the status of my last order?"
> "Where's order ``? Any tracking yet?"
The agent returns the status plus tracking — including the carrier, tracking number, current shipment `status`, and the full **checkpoint timeline** of carrier scans.
### Order Statuses
| Status | Meaning |
| ----------------------- | --------------------------------------- |
| `pending` | Order received, not yet processing |
| `in_progress` | Order is being placed with the retailer |
| `order_placed` | Order completed successfully |
| `order_failed` | Order could not be completed |
| `cancelled` | Cancelled by you |
| `cancelled_by_retailer` | Cancelled by the retailer |
`order_placed`, `order_failed`, `cancelled`, and `cancelled_by_retailer` are
terminal. If the status is `pending` or `in_progress`, check again in a few
minutes.
## Cancelling an Order
> "Cancel order ``."
An order can be cancelled only while it's still `pending` (queued). Once it's `in_progress` or complete, it can't be cancelled.
## Returns
Open a return against a placed order:
> "Return the coffee maker from order `` — it arrived damaged."
The agent submits the return with a reason and gives you back the prepaid **shipping label URLs**. Return status moves `open` → `approved` | `denied` | `credited`.
## Listing Orders
> "List my recent Zinc orders."
Returns a summary with IDs, statuses, and timestamps. Tracking `status` is included by default.
## Error Handling
If something fails, the agent reports the error. Common ones:
| Error | What to Do |
| -------------------------- | ------------------------------------------------------------------------------------------- |
| `max_price_exceeded` | Total (incl. tax + shipping) exceeds your max price. Raise the limit or pick another item. |
| `product_out_of_stock` | The product is unavailable. Try again later or choose an alternative. |
| `invalid_shipping_address` | Check the address fields (state must be 2-letter, country ISO alpha-2). |
| `insufficient_funds` | Deposit more funds in your [Zinc account](https://app.zinc.com), or check your MPP balance. |
| `product_variant_required` | The product has variants (size, color, etc.) that must be specified. |
| `retailer_unavailable` | The retailer is temporarily unavailable. Try again later. |
For the complete list, see the [Error Handling](/docs/v2/api-reference/introduction/error-handling) reference.
## Tips
* **Set `max_price` to cover the full cost** — item + tax + shipping — not just the item.
* **Use direct product URLs** (or let the agent search), not category or search-results pages.
* **Check status after a few minutes** — orders are asynchronous.
* **Reading is always safe.** Search, status checks, and listing never spend money. Only placing an order or opening a return needs confirmation.
# Create MPP Order
Source: https://www.zinc.com/docs/v2/api-reference/agent/create-order
versions/latest.json POST /agent/orders
Place an order and pay per request via the Machine Payments Protocol (MPP) — no Zinc account required.
Place an order using the [Machine Payments Protocol (MPP)](https://mpp.dev) — no Zinc account required. Payment is made upfront via MPP, supporting multiple payment methods including Tempo stablecoins and Stripe.
## How It Works
1. **Send Order Request** - Submit an order to `/agent/orders` with product and shipping details
2. **Payment Challenge** - If no valid payment credential is provided, the API returns HTTP `402` with payment challenges for all configured methods
3. **Submit Payment** - Include a valid MPP payment credential in the `Authorization` header
4. **Order Processing** - Once payment is confirmed, the order is queued for processing
This endpoint uses the same `OrderCreate` request body as the standard [Create Order](/docs/v2/api-reference/orders/create-order) endpoint. The only difference is how authentication and payment are handled. The same limits apply — notably **at most 10 products per order**, checked before the agent is charged.
## Payment is the gate
Payment — not request validation — is the gate on this endpoint. The body is parsed leniently so that an unpaid request reaches the `402` payment challenge instead of being rejected first by schema validation. This means an automated MPP discovery probe can send **no body, an empty body, or a partial body** and still receive the `402` challenge describing how to pay.
The body is only validated strictly once a valid payment credential is present. After paying, send a complete, valid `OrderCreate` body — an incomplete one will then be rejected with a `422`.
## Payment Methods
MPP supports multiple payment methods via the HTTP 402 challenge-credential flow:
* **Stripe** - Cards and wallets
* **Tempo** - Stablecoins
When a request is made without a valid credential, the response includes `WWW-Authenticate` headers describing the available payment challenges. Your MPP client uses these to complete payment and resubmit the request.
## 402 Payment Required
If no valid payment credential is provided, the API returns `402 Payment Required` with `WWW-Authenticate` headers describing the available payment methods.
### Response Headers
One header per supported payment method with challenge parameters (per RFC 9110 §11.6.1). Your MPP client uses these to complete payment and resubmit the request.
See the [MPP guide](/docs/v2/mpp) for a full walkthrough of integrating MPP with Zinc.
# Agent Product Search
Source: https://www.zinc.com/docs/v2/api-reference/agent/product-search
versions/latest.json POST /agent/products/search
Search a retailer's catalog for AI agents, paid per call via the Machine Payments Protocol (MPP).
`POST /agent/products/search` searches a single retailer (`amazon` or `walmart`) and is the agent-native, MPP-paid counterpart to the authenticated [Search Products](/docs/v2/api-reference/products/search) endpoint. Use it when you want to search one retailer at a time; use [Agent Search](/docs/v2/api-reference/agent/search) when you want breadth across every retailer in one call.
No Zinc account is required. Each call is paid per request via the [Machine Payments Protocol (MPP)](/docs/v2/mpp).
## Pricing
| | |
| ----------- | ---------------------------------------- |
| **Price** | \$0.01 per call (fixed) |
| **Payment** | MPP (HTTP 402 challenge-credential flow) |
| **Account** | Not required |
## 402 Payment Required
If no valid payment credential is provided, the API returns `402 Payment Required` with one `WWW-Authenticate` header per payable method (per RFC 9110 §11.6.1). Your MPP client uses these to complete payment and resubmit the request. See the [MPP guide](/docs/v2/mpp) for details.
# Agent Search
Source: https://www.zinc.com/docs/v2/api-reference/agent/search
versions/latest.json POST /agent/search
Search every supported retailer in one call for AI agents, paid per request via the Machine Payments Protocol (MPP).
**Beta** — the response shape may change without notice while this endpoint is in active development.
`POST /agent/search` is the agent-native counterpart to [Cross-Retailer Search](/docs/v2/api-reference/search/cross-retailer). It returns a single ranked list of buyable products across every supported retailer, and each result's `url` can be passed straight to [Create MPP Order](/docs/v2/api-reference/agent/create-order) — no opaque sku id, the URL is the contract.
No Zinc account is required. Like the other `/agent/*` data endpoints, this call is paid per request via the [Machine Payments Protocol (MPP)](/docs/v2/mpp): if no valid payment credential is provided, the API returns `402 Payment Required` with a `WWW-Authenticate` challenge for each configured payment method.
## Pricing
| | |
| ----------- | ---------------------------------------- |
| **Price** | \$0.01 per call (fixed) |
| **Payment** | MPP (HTTP 402 challenge-credential flow) |
| **Account** | Not required |
Use `/agent/search` to discover orderable URLs, then feed the chosen `url` directly into [`POST /agent/orders`](/docs/v2/api-reference/agent/create-order) to buy it — no account, no API key.
## 402 Payment Required
If no valid payment credential is provided, the API returns `402 Payment Required` with one `WWW-Authenticate` header per payable method (per RFC 9110 §11.6.1). Your MPP client uses these to complete payment and resubmit the request. See the [MPP guide](/docs/v2/mpp) for a full walkthrough.
# Retailer Credentials
Source: https://www.zinc.com/docs/v2/api-reference/configuration/managed-accounts
Configure retailer account credentials (managed accounts) so Zinc can place and process orders on your behalf.
Managed accounts (aka retailer credentials) allow you to use your own retailer
accounts (e.g., Amazon) for order processing. This gives you control over the accounts
used to place orders and can help with order limits and account management.
## Overview
When you create retailer credentials, Zinc uses those accounts to log in and place orders on your behalf. Credentials are encrypted and stored securely.
If you don't configure retailer credentials, Zinc will use default internal accounts
to process your orders.
## Order Locking
To prevent conflicts and ensure order integrity, managed accounts are locked during order processing. Only one order can be processed at a time per managed account.
If you submit multiple orders simultaneously using the same managed account, they will be queued and processed sequentially. This prevents issues like duplicate cart items or checkout conflicts that could occur if multiple orders were placed concurrently on the same retailer account.
## Two-Factor Authentication (TOTP)
If your retailer account has two-factor authentication enabled, you must provide the TOTP secret key when creating or updating credentials.
Using 2FA is the most secure and reliable method for avoiding account verification issues during order processing.
### Finding Your Amazon TOTP Key
To find your Amazon TOTP secret key:
1. Go to Amazon's Login & Security settings
2. Enable Two-Factor Authentication
3. When shown the QR code, click **"Can't scan the barcode?"**
4. Copy the displayed secret key (64 characters)
The TOTP key is the 64-character secret key, NOT the 6-digit time-based code that changes every 30 seconds.
## Email Forwarding
Retailers like Amazon may send verification codes via email during login. To handle these
automatically, you can forward emails from your retailer account to a special Zinc email
address. Zinc will parse incoming emails and automatically extract verification codes,
so orders can proceed without manual intervention.
Each managed account is assigned a dedicated forwarding address. You can find this address
in the [Zinc dashboard](https://app.zinc.com) under your managed account settings.
Once email forwarding is configured and verified, the `has_forwarding` field on your
managed account will be set to `true`.
### Setting Up Email Forwarding in Gmail
Rather than forwarding all incoming mail to Zinc, we recommend creating a Gmail filter
that only forwards emails from the retailer. This keeps your forwarding targeted and
avoids sending unrelated emails to Zinc.
Before Gmail can forward to any address, it must be registered. Go to **Settings >
Forwarding and POP/IMAP** and click **Add a forwarding address**. Enter the Zinc
forwarding email address shown in your managed account settings on the
[Zinc dashboard](https://app.zinc.com).
Google will send a confirmation email to the Zinc address. Zinc automatically verifies
the forwarding request — this may take a few minutes. Once confirmed, the address will
appear as verified in Gmail.
Do **not** enable the "Forward a copy of incoming mail to" option on this page. That
would forward all of your email. Instead, leave it set to **Disable forwarding** and
use a filter in the next steps to forward only retailer emails.
Go to **Settings > Filters and Blocked Addresses** and click **Create a new filter**.
In the **From** field, enter the retailer's email domain. For example, for Amazon
enter `amazon.com`. This will match all emails sent from any `@amazon.com` address.
Leave the other fields blank and click **Create filter**.
Check **Forward it to** and select the Zinc forwarding address from the dropdown.
You can also check **Never send it to Spam** to make sure retailer emails aren't
missed. Click **Create filter** to save.
If you also want to forward emails that are already in your inbox (e.g., a pending
verification code), check **Also apply filter to matching conversations** when creating
the filter.
## SMS Forwarding
Not every challenge arrives by email. Retailers and card issuers often send the one-time
code as a text message to the phone number on the account — Amazon sign-in codes, and the
card verification (CVC) step some issuers trigger during checkout. Zinc can't see those
messages, so an order stalls on a code sitting in someone's phone.
Send them to the same place: forward the text to the managed account's
`fwd-…@buy.zinc.com` address using an SMS-to-email app. A forwarded text is parsed exactly
like a forwarded email, so no other setup changes.
**SMS Forwarder: Forward SMS** — forwards incoming texts to an email address. Paid subscription.
**SMS Forwarder** (frzinapps) — forwards to email with per-sender and per-keyword filter rules.
### Configuring the forwarder
Use the `fwd-…@buy.zinc.com` address shown for that managed account in the
[Zinc dashboard](https://app.zinc.com) — the same address you use for email forwarding.
Each managed account has its own address, so a phone that receives codes for two
accounts needs a rule per account.
Forward only the senders that carry codes — the retailer's short code and your card
issuer — rather than every text on the phone. Both apps above support filtering on the
sending number or on keywords in the message.
Set the forwarded email's subject to `Verification code`. Zinc looks for
verification wording in the subject or body before it extracts a code, and the wording
in a text message is inconsistent — a fixed subject makes every forwarded code eligible.
Keep digits out of the subject. A number in the subject line is treated as the
verification code.
Configure the forwarder to send the message body, with any sender or timestamp
metadata after it. Short codes are six digits themselves — Amazon's US short code is `262966` —
and a six-digit number appearing above the real code can be picked up instead.
Test the rule before you rely on it: text yourself `Your verification code is 123456` from
another phone and confirm `has_forwarding` flips to `true` on the managed account. The
first message Zinc receives at a forwarding address marks it verified.
## Endpoints
| Method | Endpoint | Description |
| -------- | ------------------------------------------------------------------------------------------------ | ---------------------------------- |
| `GET` | [`/managed-accounts`](/docs/v2/api-reference/managed-accounts/list-managed-accounts) | List all your retailer credentials |
| `POST` | [`/managed-accounts`](/docs/v2/api-reference/managed-accounts/create-managed-account) | Create new retailer credentials |
| `PUT` | [`/managed-accounts/{short_id}`](/docs/v2/api-reference/managed-accounts/update-managed-account) | Update existing credentials |
| `DELETE` | [`/managed-accounts/{short_id}`](/docs/v2/api-reference/managed-accounts/delete-managed-account) | Delete credentials |
## Create Credentials
```bash theme={null}
curl -X POST https://api.zinc.com/managed-accounts \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"email": "your-amazon-email@example.com",
"password": "your-amazon-password",
"retailer": "amazon",
"totp_secret": "YOUR_64_CHARACTER_SECRET_KEY"
}'
```
### Request Fields
| Field | Type | Required | Description |
| ------------- | ------ | -------- | ------------------------------------------------------------------------------------------------ |
| `email` | string | Yes | The email address for the retailer account |
| `password` | string | No | The password for the retailer account (encrypted on storage) |
| `retailer` | string | No | Retailer name (e.g., `amazon`). If omitted, applies as default credentials |
| `totp_secret` | string | No | The secret key for two-factor authentication. Required if 2FA is enabled on the retailer account |
### Response
```json theme={null}
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"short_id": "zn_acct_a1b2c3d4",
"email": "your-amazon-email@example.com",
"retailer": "amazon",
"has_totp": true,
"created_at": "2026-01-15T10:30:00Z",
"updated_at": "2026-01-15T10:30:00Z"
}
```
Passwords and TOTP secrets are never returned in API responses. They are encrypted and stored securely. The `has_totp` field indicates whether 2FA is configured.
## List Credentials
```bash theme={null}
curl https://api.zinc.com/managed-accounts \
-H "Authorization: Bearer "
```
### Response
```json theme={null}
{
"credentials": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"short_id": "zn_acct_a1b2c3d4",
"email": "your-amazon-email@example.com",
"retailer": "amazon",
"has_totp": true,
"created_at": "2026-01-15T10:30:00Z",
"updated_at": "2026-01-15T10:30:00Z"
}
],
"total": 1
}
```
## Update Credentials
Use the `short_id` from the credentials response to update:
```bash theme={null}
curl -X PUT https://api.zinc.com/managed-accounts/zn_acct_a1b2c3d4 \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"password": "new-password"
}'
```
### Request Fields
All fields are optional. Only provided fields are updated.
| Field | Type | Description |
| ------------- | ------ | ------------------------- |
| `email` | string | New email address |
| `password` | string | New password |
| `retailer` | string | New retailer association |
| `totp_secret` | string | Update the 2FA secret key |
## Delete Credentials
```bash theme={null}
curl -X DELETE https://api.zinc.com/managed-accounts/zn_acct_a1b2c3d4 \
-H "Authorization: Bearer "
```
Returns `204 No Content` on success.
## Response Fields
| Field | Type | Description |
| ---------------- | ----------------- | ----------------------------------------------------------- |
| `id` | string (UUID) | Unique identifier |
| `short_id` | string | Short identifier used in URLs (e.g., `zn_acct_a1b2c3d4`) |
| `email` | string | Retailer account email |
| `retailer` | string or null | Retailer name, or null if default credentials |
| `has_totp` | boolean | Whether TOTP 2FA is configured for this account |
| `has_forwarding` | boolean | Whether email forwarding has been verified for this account |
| `created_at` | string (ISO 8601) | When the credentials were created |
| `updated_at` | string (ISO 8601) | When the credentials were last updated |
## Best Practices
1. **Use dedicated accounts** - Create retailer accounts specifically for Zinc orders to avoid conflicts with personal orders
2. **Monitor account health** - Retailer accounts can be locked if flagged for unusual activity. Check for `login_failed` or `account_locked` errors
3. **Keep credentials updated** - If you change your retailer account password, update it here to avoid order failures
4. **Enable 2FA** - Two-factor authentication prevents account lockouts from verification challenges and is the most reliable method for automated ordering
# API Changelog
Source: https://www.zinc.com/docs/v2/api-reference/introduction/api-changelog
Release notes and version-by-version changes to the Zinc API v2.
A version-by-version record of endpoints, fields, and schemas added or changed in the Zinc API.
For product updates, new features, and general improvements, see the [product changelog](/docs/changelog).
***
## 2026-08-21
This is the current API version.
No API changes in this version.
***
## 2026-08-11
**Changes**
* `OrderCreate` — Added `gift_message` (max 240 characters; requires `is_gift`)
* `OrderResponse` — Added `gift_message`, `merchant_order_ids`
* `GET /orders` — Added query parameter `merchant_order_id`, an exact-match filter on the retailer's own order number
**Behavior changes**
* `OrderCreate.is_gift` — An order requesting `is_gift` now **fails** with the
`gift_option_unavailable` error type when the retailer's checkout offers no free
gift option, rather than being placed as a normal order. If gifting is optional
in your flow, catch this error type and retry without `is_gift`. See
[Gift orders](/docs/v2/api-reference/orders/create-order#gift-orders).
**New retailers**
* Added `zazzle` to the supported retailer catalog. See [`GET /retailers`](/docs/v2/api-reference/retailers/list-retailers) for the full list.
***
## 2026-08-10
No API changes in this version.
***
## 2026-07-29
**Removed endpoints**
* `GET /agent/products/details` — Agent Product Details
* `GET /agent/products/offers` — Agent Product Offers
* `GET /agent/products/search` — Agent Product Search
* `GET /agent/search` — Agent Search
**New schemas**
* `CustomerNotificationStatus`
* `CustomerNotifications`
**Changes**
* `OrderCreate` — Added `customer_notifications`
* `OrderResponse` — Added `customer_notifications`
* `ProductSearchResult` — Added `url`, `variant_id`
* `GET /orders` — Added query parameter `metadata_key`, `metadata_value`
***
## 2026-07-17
**New endpoints**
* `GET /track/{order_id}` — Get Public Tracking
* `POST /agent/products/details` — Agent Product Details
* `POST /agent/products/offers` — Agent Product Offers
* `POST /agent/products/search` — Agent Product Search
* `POST /agent/search` — Agent Search
**New schemas**
* `ErrorCode` enum — `unauthorized`, `forbidden`, `invalid_token`, `token_expired`, `not_found`, `already_exists`, `validation_error`, `insufficient_funds`, `payment_failed`, `payment_method_required`, `invalid_payment_method`, `invalid_shipping_address`, `url_unreachable`, `non_us_retailer`, `unsupported_retailer`, `unsupported_country`, `retailer_country_not_supported`, `guest_checkout_not_supported`, `invalid_variant`, `out_of_stock`, `shipping_unavailable`, `order_not_cancellable`, `return_not_allowed`, `external_service_error`, `stripe_error`, `stytch_error`, `internal_error`, `bad_request`, `rate_limit_exceeded`, `service_unavailable`
* `ErrorDetail`
* `ErrorResponse`
* `PublicTrackingDestination`
* `PublicTrackingItem`
* `PublicTrackingResponse`
* `PublicTrackingShipment`
**Changes**
* `OrderPriceComponents` — Added `discount`, `fees`
* `ReturnRequestListResponse` — Added `total`
* `GET /managed-accounts` — Added query parameter `limit`, `offset`, `search`
* `GET /orders` — Added query parameter `created_after`, `created_before`, `has_tracking`, `search`, `tracking_status`
* `GET /returns` — Added query parameter `limit`, `offset`, `search`, `status`
* `POST /returns` — Added response code `409`
***
## 2026-07-10
**Changes**
* `TrackingNumberResponse` — Added `estimated_delivery_date` (carrier-reported delivery estimate as a destination-local calendar date; `null` until the carrier reports one)
* `POST /agent/orders` — Added query parameter `method` (restrict the `402` response to a single payment method, e.g. `stripe`, `tempo`, or `x402`, instead of advertising every configured method)
* `POST /agent/orders` — A present-but-invalid body (including malformed or non-object JSON) is now rejected with `422` before any payment challenge is issued or honored; only a bodyless discovery probe receives the lenient `402`
* `GET /products/search` — `free_shipping` no longer filters Amazon results (upstream search data under-reports Prime, so filtering would drop valid items); the filter now applies to Walmart only, and defaults to `false` for all accounts (previously `true` for BizAPI-enrolled accounts)
* `GET /agent/products/search` — `free_shipping` no longer filters Amazon results; Walmart only
***
## 2026-07-06
**New endpoints**
* `GET /orders/{order_id}/timeline` — Get Order Timeline
* `GET /usage` — Get My Usage
**New schemas**
* `OrderMilestone`
* `OrderTimelineResponse`
* `RecentCall`
* `UsageMetric`
* `UserUsageResponse`
**Removed endpoints**
* `GET /orders/{order_id}/events` — Get Order Events (now admin-only; use [Get Order Timeline](/docs/v2/api-reference/orders/get-order-timeline) for order status)
**Removed schemas**
* `OrderEventsResponse`, `ProgressEventResponse`
**Changes**
* `ProductSearchResponse` — Added `next_page`
* `PublicRetailer` — Added `no_account_needed`, `use_your_account`
* `PublicRetailer` — Removed `supported`
* `TrackingNumberResponse` — Added `zinc_tracking_number`, `zinc_tracking_url`
* `GET /agent/products/search` — Added query parameter `free_shipping`
* `GET /products/search` — Added query parameter `free_shipping`
***
## 2026-06-26
**New endpoints**
* `GET /agent/products/details` — Agent Product Details
* `GET /agent/products/offers` — Agent Product Offers
* `GET /agent/products/search` — Agent Product Search
* `GET /agent/search` — Agent Search
* `GET /retailers` — List Retailers
**New schemas**
* `ErrorDetails`
* `FieldError`
* `PublicRetailer`
* `PublicRetailerListResponse`
* `TrackingCheckpointResponse`
* `TrackingStatus` enum — `pending`, `in_transit`, `delivered`
**Changes**
* `OrderJobResult` — Added `error_details`
* `TrackingNumberResponse` — Added `checkpoints`, `status`
* `GET /orders` — Added query parameter `include`
***
## 2026-06-19
**New schemas**
* `MarginSpec`
* `OrderConnectInfo`
* `OrderJobResult`
* `OrderPayment`
* `OrderPriceComponents`
* `ProductCondition` enum — `New`, `Refurbished`, `UsedLikeNew`, `UsedVeryGood`, `UsedGood`, `UsedAcceptable`
**Removed schemas**
* `JobResultPayload`
* `LineItem`
* `MerchantOrderId`
* `PriceComponents`
**Changes**
* `OrderCreate` — Added `handling_days_max`, `is_gift`, `payment`
* `OrderItemResponse` — Added `condition_in`, `condition_not_in`
* `OrderProduct` — Added `condition_in`, `condition_not_in`
* `OrderResponse` — Added `connect`, `handling_days_max`, `is_gift`, `job_result`
***
## 2026-06-11
**New endpoints**
* `GET /orders/bulk` — List Bulk Uploads
* `GET /orders/bulk/{batch_id}` — Get Bulk Upload
* `GET /orders/bulk/{batch_id}/results.csv` — Download Bulk Results
* `POST /orders/bulk` — Create Bulk Upload
* `POST /orders/bulk/validate` — Validate Bulk Upload
**New schemas**
* `BulkBatchListResponse`
* `BulkBatchResponse`
* `BulkBatchStatus` enum — `pending`, `processing`, `completed`, `failed`
* `BulkRowResult`
* `BulkRowValidation`
* `BulkUploadRequest`
* `BulkValidateResponse`
**Changes**
* `OrderResponse` — Added `retailer_credentials_uuid`
* `ReturnRequestStatus` enum — Added `credited` (return resolved by crediting the customer's wallet)
***
## 2026-03-30
**New endpoints**
* `POST /returns` — Create Return Request
* `GET /returns` — List Return Requests
* `GET /returns/{return_request_id}` — Get Return Request
**New schemas**
* `ReturnRequestCreate`, `ReturnRequestResponse`, `ReturnRequestListResponse`
* `ReturnRequestReason` enum — `return.request_label`, `nondelivery.not_delivered`, `nondelivery.damaged`, `nondelivery.empty_box`, `tracking.request_update`, `cancel.forced_cancellation`, `other`
* `ReturnRequestStatus` enum — `open`, `in_progress`, `approved`, `denied`, `completed`
***
## 2026-03-25
**New endpoints**
* `GET /products/search` — Search Products
* `GET /products/{product_id}` — Get Product
* `GET /products/{product_id}/offers` — Get Product Offers
**New schemas**
* `ProductSearchResponse`, `ProductSearchResult`
**Changes**
* `POST /agent/orders` — Added `402` response code for MPP payment challenges
***
## 2026-03-23
**New endpoints**
* `POST /agent/orders` — Create MPP Order
**New schemas**
* `LineItem`
**Changes**
* `PriceComponents` — Added `line_items` field (array of `LineItem`)
* `JobResultPayload` — Added `estimated_delivery` field
***
## 2026-02-19
**Changes**
* `RetailerCredentialsCreate` — Added `retailer_config` field
* `RetailerCredentialsUpdate` — Added `retailer_config` field
* `RetailerCredentialsResponse` — Added `retailer_config` and `forwarding_email` fields
***
## 2026-02-12
**New endpoints**
* `GET /managed-accounts` — List Managed Accounts
* `POST /managed-accounts` — Create Managed Account
* `PUT /managed-accounts/{short_id}` — Update Managed Account
* `DELETE /managed-accounts/{short_id}` — Delete Managed Account
* `GET /orders/{order_id}/events` — Get Order Events
**New schemas**
* `OrderEventsResponse`, `ProgressEventResponse`
* `RetailerCredentialsCreate`, `RetailerCredentialsListResponse`, `RetailerCredentialsResponse`, `RetailerCredentialsUpdate`
***
## 2026-02-05
**Changes**
* `Address` — `state` field made optional and nullable to support international addresses
***
## 2026-01-28
**Changes**
* `OrderCreate` — Added `po_number` field for purchase order tracking
* `OrderResponse` — Added `po_number` field
***
## 2026-01-21
**Changes**
* `OrderCreate` — Added `metadata` field for custom key-value data
* `OrderResponse` — Added `metadata` field
***
## 2026-01-19
**New endpoints**
* `GET /orders/test-products` — List Test Products
**New schemas**
* `TrackingNumberResponse`
**Changes**
* `OrderResponse` — Added `attempts` (integer) and `tracking_numbers` (array) fields
***
## 2026-01-09
**Changes**
* `GET /orders` — Added `order_id` query parameter for filtering
* `OrderCreate` — Added `retailer_credentials_id` field
* `OrderResponse` — Added `retailer_credentials_id` field
***
## 2025-12-19
**New endpoints**
* `POST /orders/{order_id}/cancel` — Cancel Order
**New schemas**
* `ProductVariant`
**Changes**
* `OrderItemResponse` — Added `variant` field
* `OrderProduct` — Added `variant` field
* `OrderStatus` enum — Added `cancelled` value
* `JobResultPayload` — Several fields made optional/nullable
***
## 2025-11-24
Initial API version.
# Authentication
Source: https://www.zinc.com/docs/v2/api-reference/introduction/authentication
Authenticate Zinc API requests with your client token and API key.
To use the Zinc API, you must authenticate every request with your API key.
You can find your API key in your Zinc dashboard after creating an account.
## Example Authentication Request
```bash theme={null}
curl https://api.zinc.com/orders \
-H "Authorization: Bearer "
```
Authentication is performed using **Bearer token authentication**.
* Include your API key in the `Authorization` header
* Format: `Authorization: Bearer `
Never share your API key or expose it in public repositories, client-side code, or other insecure locations. Your API key is tied to your account, and you are responsible for all requests made with it.
If you believe your API key has been compromised, please contact [support@zinc.com](mailto:support@zinc.com) immediately.
# Error Handling
Source: https://www.zinc.com/docs/v2/api-reference/introduction/error-handling
Understand Zinc API error codes, status responses, and how to handle failures gracefully.
The Zinc API uses conventional HTTP response codes to indicate the success or failure of an API request.
## HTTP Status Codes
| Status Code | Description |
| ----------- | ------------------------------------------------------------- |
| `200` | Success - The request was successful |
| `201` | Created - A new resource was created successfully |
| `400` | Bad Request - The request was malformed or invalid |
| `401` | Unauthorized - Invalid or missing authentication |
| `402` | Payment Required - Payment or wallet issue |
| `403` | Forbidden - You don't have permission to access this resource |
| `404` | Not Found - The requested resource does not exist |
| `409` | Conflict - Resource already exists |
| `422` | Unprocessable Entity - Validation error |
| `500` | Internal Server Error - Something went wrong on our end |
| `502` | Bad Gateway - External service error |
## Error Response Format
All error responses follow a consistent structure:
```json theme={null}
{
"code": "error_code",
"message": "Human-readable error message",
"details": {
"field": "Additional context about the error"
}
}
```
## API Error Codes
### General Errors
| Error Code | Description |
| ------------------ | ------------------------------------------------------------------------ |
| `not_found` | Resource was not found |
| `validation_error` | A parameter was incorrect or missing. Check details for more information |
| `bad_request` | The request was malformed or invalid |
| `already_exists` | Resource already exists (e.g., duplicate idempotency key) |
| `internal_error` | Something went wrong with our internal systems |
#### Example
```json theme={null}
{
"code": "validation_error",
"message": "Invalid request parameters",
"details": {
"products": "At least one product is required"
}
}
```
### Authentication Errors
| Error Code | Description |
| --------------- | ------------------------------------------ |
| `unauthorized` | Authentication is required for this action |
| `forbidden` | You do not have permission for this action |
| `invalid_token` | Your API token is invalid |
| `token_expired` | Your API token has expired |
#### Example
```json theme={null}
{
"code": "invalid_token",
"message": "The provided API token is not valid"
}
```
### Wallet & Payment Errors
| Error Code | Description |
| ------------------------- | ------------------------------------------ |
| `insufficient_funds` | Insufficient funds in wallet for the order |
| `payment_failed` | Payment operation failed |
| `payment_method_required` | No default payment method configured |
| `invalid_payment_method` | The provided payment method is invalid |
#### Example
```json theme={null}
{
"code": "insufficient_funds",
"message": "Your wallet balance is insufficient for this order",
"details": {
"required": 4999,
"available": 1000
}
}
```
### Order Request Errors
| Error Code | Description |
| -------------------------- | --------------------------------------------------------- |
| `invalid_shipping_address` | The shipping address failed validation |
| `url_unreachable` | The product URL provided is inaccessible |
| `invalid_variant` | Product variant not provided or not found on product page |
| `out_of_stock` | Product is not currently available for purchase |
| `shipping_unavailable` | Shipping to this address is not available |
| `non_us_retailer` | Only US retailer sites are supported |
| `order_not_cancellable` | Order cannot be cancelled due to its current status |
#### Example
```json theme={null}
{
"code": "invalid_shipping_address",
"message": "The shipping address could not be validated",
"details": {
"field": "postal_code",
"reason": "Postal code does not match city/state"
}
}
```
### External Service Errors
| Error Code | Description |
| ------------------------ | ------------------------------- |
| `external_service_error` | An external service call failed |
#### Example
```json theme={null}
{
"code": "external_service_error",
"message": "Unable to connect to external service. Please try again."
}
```
## Order Processing Errors
When an order fails during processing, the `error_type` field in the order response or webhook payload contains one of these codes:
### Structured error details
Alongside the flat `error` and `error_type` keys (kept for backward compatibility), a failed job result may include an `error_details` object. This is the structured home for enrichment beyond the category and message — for example, the specific reasons a shipping address was rejected, or which fields failed validation.
| Field | Type | Description |
| ---------------------------- | --------- | -------------------------------------------------------- |
| `code` | string | Machine-readable error code |
| `message` | string | Human-readable error message |
| `address_validation_reasons` | string\[] | Reasons a shipping address was rejected, when applicable |
| `field_errors` | array | Per-field validation failures (see below) |
Each entry in `field_errors` is a `FieldError`:
| Field | Type | Description |
| ---------- | -------------- | ---------------------------------------- |
| `field` | string | The offending field |
| `code` | string | Machine-readable failure code |
| `message` | string | Human-readable description |
| `received` | any | The value that was received |
| `expected` | string \| null | The expected value or format, when known |
#### Example
```json theme={null}
{
"event": "order.failed",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "failed",
"timestamp": "2026-01-15T14:30:00Z",
"data": {
"error_type": "shipping_address_invalid",
"error": "The shipping address could not be validated",
"error_details": {
"code": "shipping_address_invalid",
"message": "The shipping address could not be validated",
"address_validation_reasons": [
"Postal code does not match city/state"
],
"field_errors": [
{
"field": "state",
"code": "invalid_value",
"message": "state should be a 2-letter code",
"received": "New York",
"expected": "NY"
}
]
}
}
}
```
### Product Errors
| Error Type | Description |
| ------------------------------ | -------------------------------------------- |
| `product_not_found` | Product page doesn't exist or was removed |
| `product_out_of_stock` | Product is out of stock |
| `product_unavailable` | Product exists but cannot be purchased |
| `invalid_product_url` | URL is malformed or not a valid product page |
| `product_variant_required` | Product requires a variant selection |
| `product_variant_unavailable` | Selected variant is not available |
| `product_quantity_unavailable` | Requested quantity is not available |
#### Example
```json theme={null}
{
"event": "order.failed",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "failed",
"timestamp": "2026-01-15T14:30:00Z",
"data": {
"error_type": "product_out_of_stock",
"error": "The requested product is currently out of stock"
}
}
```
### Price Errors
| Error Type | Description |
| -------------------- | ----------------------------------------- |
| `max_price_exceeded` | Total price exceeds the `max_price` limit |
#### Example
```json theme={null}
{
"event": "order.failed",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "failed",
"timestamp": "2026-01-15T14:30:00Z",
"data": {
"error_type": "max_price_exceeded",
"error": "Order total of $52.99 exceeds max_price of $50.00"
}
}
```
### Cart & Checkout Errors
| Error Type | Description |
| -------------------- | ---------------------------------------------- |
| `add_to_cart_failed` | Could not add product to cart |
| `cart_empty` | Cart became empty during checkout |
| `checkout_blocked` | Checkout blocked (captcha, verification, etc.) |
| `checkout_failed` | Generic checkout failure |
#### Example
```json theme={null}
{
"event": "order.failed",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "failed",
"timestamp": "2026-01-15T14:30:00Z",
"data": {
"error_type": "checkout_blocked",
"error": "Checkout requires additional verification"
}
}
```
### Gift Errors
| Error Type | Description |
| ------------------------- | --------------------------------------------------------------------------------- |
| `gift_option_unavailable` | Order requested `is_gift`, but the retailer's checkout offers no free gift option |
An order submitted with `is_gift: true` fails with this error type rather than
being placed as a normal order — a gift arriving with prices visible to the
recipient is treated as worse than no order. If gifting is an optional add-on in
your flow, catch this error type and retry without `is_gift` to place the order
as a standard purchase. See [Gift orders](/docs/v2/api-reference/orders/create-order#gift-orders).
`gift_message` does not fail this way. Where a retailer's checkout has no
gift-message field, the order is still placed without the message.
#### Example
```json theme={null}
{
"event": "order.failed",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "failed",
"timestamp": "2026-01-15T14:30:00Z",
"data": {
"error_type": "gift_option_unavailable",
"error": "This retailer does not offer a free gift option at checkout"
}
}
```
### Shipping Errors
| Error Type | Description |
| ----------------------------- | ------------------------------------------ |
| `shipping_address_invalid` | Address validation failed on retailer site |
| `shipping_unavailable` | Cannot ship to the given address |
| `shipping_method_unavailable` | No shipping methods available |
#### Example
```json theme={null}
{
"event": "order.failed",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "failed",
"timestamp": "2026-01-15T14:30:00Z",
"data": {
"error_type": "shipping_unavailable",
"error": "This item cannot be shipped to the selected address"
}
}
```
### Payment Errors
| Error Type | Description |
| ------------------------ | -------------------------------- |
| `payment_declined` | Payment was declined by retailer |
| `payment_method_invalid` | Payment method not accepted |
| `payment_failed` | Generic payment failure |
#### Example
```json theme={null}
{
"event": "order.failed",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "failed",
"timestamp": "2026-01-15T14:30:00Z",
"data": {
"error_type": "payment_declined",
"error": "The payment method was declined by the retailer"
}
}
```
### Account Errors
| Error Type | Description |
| ------------------------------- | --------------------------------------- |
| `login_failed` | Could not log into retailer account |
| `session_expired` | Session expired during checkout |
| `account_locked` | Retailer account is locked or suspended |
| `account_verification_required` | Account needs verification |
#### Example
```json theme={null}
{
"event": "order.failed",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "failed",
"timestamp": "2026-01-15T14:30:00Z",
"data": {
"error_type": "login_failed",
"error": "Unable to authenticate with retailer account"
}
}
```
### Retailer Errors
| Error Type | Description |
| ------------------------ | ---------------------------------------- |
| `retailer_unavailable` | Retailer website is down or inaccessible |
| `retailer_not_supported` | Retailer is not supported |
| `retailer_rate_limited` | Rate limited by retailer |
#### Example
```json theme={null}
{
"event": "order.failed",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "failed",
"timestamp": "2026-01-15T14:30:00Z",
"data": {
"error_type": "retailer_unavailable",
"error": "The retailer website is currently unavailable"
}
}
```
### Quantity Limit Errors
| Error Type | Description |
| ------------------------- | ------------------------------------- |
| `quantity_limit_exceeded` | Retailer has purchase quantity limits |
| `order_limit_exceeded` | Account has reached order limits |
#### Example
```json theme={null}
{
"event": "order.failed",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "failed",
"timestamp": "2026-01-15T14:30:00Z",
"data": {
"error_type": "quantity_limit_exceeded",
"error": "Maximum purchase quantity for this item is 3"
}
}
```
# Idempotency
Source: https://www.zinc.com/docs/v2/api-reference/introduction/idempotency
Send idempotent Zinc API requests to safely retry orders without creating duplicates.
Every order Zinc places spends real money. An idempotency key is your guarantee that one logical purchase results in one charge — no matter how many times the request is sent.
Use one on every order. It's the only thing standing between a flaky network, a buggy retry, or an over-eager client and a duplicate order on a real card.
## How it works
Include an `idempotency_key` in your request body. It can be any string up to 36 characters — a UUID is the right choice. Zinc enforces uniqueness on this key, so at most one order will ever exist for it, no matter how many times you send the same request.
Use a fresh key for each new order. The same key always means the same order.
## Behavior on retry
If you send a request with a key that has already been used, Zinc returns an `already_exists` error rather than replaying the original response:
```json theme={null}
{
"code": "already_exists",
"message": "Order with idempotency key 'a3f1...' already exists",
"details": {
"resource": "Order",
"identifier": "a3f1c2d4-..."
}
}
```
On a retry, this is **success, not failure** — your original request got through. Look the order up by its id (or by `metadata`) and move on.
## Example
```bash theme={null}
curl https://api.zinc.com/orders \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"idempotency_key": "a3f1c2d4-7b8e-4a9f-9c1d-2e3f4a5b6c7d",
"products": [
{ "url": "https://www.amazon.com/dp/B07JGBW826", "quantity": 1 }
],
"shipping_address": { ... },
"max_price": 2300
}'
```
If the call succeeds, store the returned order id. If it errors out, see below.
## When to retry
* **Retry on network problems and Zinc service errors.** Timeouts, connection resets, and `5xx` responses are usually transient. Use the **same** idempotency key and back off between attempts (e.g. 1s, 2s, 4s, capped at a few tries).
* **Don't retry on errors caused by your request.** Bad input, bad auth, insufficient funds — these will fail the same way next time. Fix the cause, then submit a new order with a new key.
An `already_exists` response on a retry means the original attempt succeeded. Treat it as success and stop retrying.
**Don't retry every error.** Retrying validation, auth, or payment errors won't make them go away. And if you "fix" the loop by changing the idempotency key on each attempt, you'll create duplicate orders the moment Zinc recovers.
## Anti-patterns
These are real patterns we've seen cause duplicate orders or hide real failures.
#### Retrying every error
Treating every non-success response as retryable hides bugs in your integration. Auth, validation, and payment errors are deterministic — the next attempt will fail the same way.
#### Changing the key per attempt
Every one of these defeats idempotency:
```text theme={null}
key attempt 1
key-retry attempt 2 (new key, new order)
key_attempt_2 attempt 3 (new key, new order)
newUuid() attempt 4 (new key, new order)
`${key}-${Date.now()}` attempt N (new key, new order)
```
The whole point is that the key does **not** change between attempts of the same logical operation. If you append a counter, a timestamp, an attempt number, or generate a fresh UUID per retry, you're creating N orders on N retries. Send the same string every time.
#### Treating a duplicate-key response as failure
An `already_exists` response on a retry means the original request succeeded. Surfacing it as an error — or retrying with a new key — turns a successful order into a duplicate.
#### Not persisting the key before sending the request
Generate the key, write it to your database alongside the pending order, *then* call Zinc. A key that lives only in memory is a key you can't retry with if your process crashes mid-call.
# Sandbox & Testing
Source: https://www.zinc.com/docs/v2/api-reference/introduction/sandbox
Test your Zinc API integration in the sandbox — place and simulate orders without buying anything.
The Zinc API provides a sandbox environment for testing your integration without placing real orders or incurring charges. Test mode uses isolated data and simulates various order scenarios.
Building an agent? It can mint its own test key with no account and no signup, in a single unauthenticated call — see the [Agent Sandbox](/docs/v2/agent-sandbox/overview).
## Enabling Test Mode
Use an API key with the `zn_test_` prefix:
```bash theme={null}
curl https://api.zinc.com/orders \
-H "Authorization: Bearer zn_test_abc123..."
```
Test mode uses a separate sandbox database. Orders created in test mode are
completely isolated from production data.
## Test Products
Use these special product URLs to simulate different order scenarios:
| Product URL | Scenario | Description |
| ---------------------------------------------------------- | -------------------- | --------------------------------- |
| `https://zinc.com/shop/products/test-success` | Success | Order completes successfully |
| `https://zinc.com/shop/products/test-out-of-stock` | Out of Stock | Product is unavailable |
| `https://zinc.com/shop/products/test-price-exceeded` | Price Exceeded | Total exceeds `max_price` |
| `https://zinc.com/shop/products/test-invalid-address` | Invalid Address | Shipping address validation fails |
| `https://zinc.com/shop/products/test-url-unreachable` | URL Unreachable | Product URL is inaccessible |
| `https://zinc.com/shop/products/test-invalid-variant` | Invalid Variant | Variant selection required |
| `https://zinc.com/shop/products/test-shipping-unavailable` | Shipping Unavailable | Cannot ship to address |
| `https://zinc.com/shop/products/test-insufficient-funds` | Insufficient Funds | Wallet balance too low |
### Get Test Products Programmatically
You can also retrieve the full list of test products from the API:
```bash theme={null}
curl https://api.zinc.com/orders/test-products \
-H "Authorization: Bearer "
```
#### Example Response
```json theme={null}
{
"products": [
{
"url": "https://zinc.com/shop/products/test-success",
"scenario": "success",
"name": "Success",
"is_synchronous_error": false
},
{
"url": "https://zinc.com/shop/products/test-invalid-address",
"scenario": "invalid_address",
"name": "Invalid Address",
"is_synchronous_error": true
}
]
}
```
## Error Timing
Test scenarios produce errors at different stages:
### Synchronous Errors
These errors occur immediately when creating the order:
* `test-invalid-address` - Returns `invalid_shipping_address` error
* `test-url-unreachable` - Returns `url_unreachable` error
* `test-insufficient-funds` - Returns `insufficient_funds` error
#### Example
```bash theme={null}
curl -X POST https://api.zinc.com/orders \
-H "Authorization: Bearer zn_test_abc123..." \
-H "Content-Type: application/json" \
-d '{
"products": [{"url": "https://zinc.com/shop/products/test-invalid-address"}],
"shipping_address": {...},
"max_price": 5000
}'
```
```json theme={null}
{
"code": "invalid_shipping_address",
"message": "The shipping address failed validation (test scenario)"
}
```
### Asynchronous Errors
These errors occur during order processing and are delivered via webhooks:
* `test-out-of-stock` - Order fails with `product_out_of_stock`
* `test-price-exceeded` - Order fails with `max_price_exceeded`
* `test-invalid-variant` - Order fails with `invalid_variant`
* `test-shipping-unavailable` - Order fails with `shipping_unavailable`
The order is created successfully, but transitions to `failed` status during processing.
## Test Success Scenario
The `test-success` product simulates a complete successful order:
```bash theme={null}
curl -X POST https://api.zinc.com/orders \
-H "Authorization: Bearer zn_test_abc123..." \
-H "Content-Type: application/json" \
-d '{
"products": [{"url": "https://zinc.com/shop/products/test-success"}],
"shipping_address": {
"first_name": "John",
"last_name": "Smith",
"address_line1": "123 Main Street",
"city": "Seattle",
"state": "WA",
"postal_code": "98101",
"country": "US",
"phone_number": "206-555-0100"
},
"max_price": 5000
}'
```
### Successful Test Order Response
When retrieved after processing:
```json theme={null}
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "order_placed",
"merchant_order_id": "TEST-550e8400",
"tracking_numbers": [
{
"carrier": "usps",
"tracking_number": "ZINC_TEST_123456789"
}
],
"price_components": {
"subtotal": 4500,
"tax": 250,
"shipping": 150,
"total": 4900
}
}
```
## Skipped Validations
In test mode, the following validations are bypassed to simplify testing:
* Wallet balance checks
* US retailer URL validation
* URL reachability validation
* Shipping address country validation
* Address verification via external APIs
Test mode behavior differs from production. Always perform final testing with
real orders before going live.
## Data Isolation
Test mode data is completely isolated:
* Orders created in test mode are stored in a sandbox database
* Test orders do not appear in production order lists
* Production orders do not appear in test mode
* Wallet balances are separate between test and production
## Best Practices
1. **Start with test mode** - Build and test your entire integration using test products before switching to production
2. **Test all scenarios** - Use each test product to verify your error handling works correctly
3. **Test webhooks** - Configure webhooks and verify you receive events for both successful and failed test orders
4. **Verify error handling** - Ensure your application gracefully handles both synchronous and asynchronous errors
5. **Use consistent mode** - Don't mix test and production API keys in the same environment
# Webhooks
Source: https://www.zinc.com/docs/v2/api-reference/introduction/webhooks
Receive real-time Zinc webhooks for order status, tracking, and delivery events.
Webhooks allow you to receive real-time HTTP notifications when events occur on your orders or returns. Instead of polling the API for updates, configure a webhook URL to receive automatic notifications.
## Configuration
Configure your webhook URL in the [Zinc dashboard](https://app.zinc.com) under Settings. You can also generate a webhook secret for signature verification.
### Webhook Secret
Your webhook secret is used to verify that incoming webhook requests are from Zinc. The secret format is:
```
zn_whsec_XXXXXXXXXXXXXXXXXXXXXXXX
```
Keep your webhook secret secure. If compromised, rotate it immediately in the dashboard. Rotating the secret invalidates the previous one.
## Events
### Order events
| Event | Description |
| ------------------------- | ------------------------------------------------------------------------------------ |
| `order.started` | Order has been created and queued for processing |
| `order.placed` | Order was successfully placed with the retailer |
| `order.failed` | Order failed after all retry attempts were exhausted |
| `order.tracking_received` | Tracking number(s) were received from the retailer |
| `order.delivered` | All packages on the order have been delivered |
| `order.cancelled` | Retailer cancelled the order after placement (your wallet is refunded automatically) |
### Return events
Return events carry an additional `return_id` field alongside `order_id` so you can route by return without parsing the `data` object. The `status` field on these events reflects the **return-request status** (not the order status).
| Event | Description |
| ----------------- | ------------------------------------------------------------------------------------------- |
| `return.created` | A return was filed against one of your orders |
| `return.approved` | A return was approved — typically accompanied by a merchant RMA / label URL |
| `return.denied` | A return was denied (e.g. outside the return window) |
| `return.credited` | A return was resolved by crediting the customer's wallet rather than working a merchant RMA |
## Payload Structure
All webhook payloads follow this structure:
```json theme={null}
{
"event": "order.placed",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "order_placed",
"timestamp": "2026-01-15T14:30:00Z",
"data": {}
}
```
### Payload Fields
| Field | Type | Description |
| ----------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `event` | string | The event type (e.g., `order.started`, `order.placed`, `return.approved`) |
| `order_id` | string | The UUID of the order |
| `return_id` | string \| null | The UUID of the return request. Only populated on `return.*` events; `null` for order events. |
| `status` | string | Current status of the subject. For order events this is the order status; for return events it's the return-request status. |
| `timestamp` | string (ISO 8601) | When the event occurred |
| `data` | object | Additional event-specific data |
### Event-Specific Data
**`order.placed`** includes price components:
```json theme={null}
{
"event": "order.placed",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "order_placed",
"timestamp": "2026-01-15T14:30:00Z",
"data": {
"price_components": {
"subtotal": 1999,
"shipping": 499,
"tax": 150,
"total": 2648
}
}
}
```
**`order.failed`** includes error information:
```json theme={null}
{
"event": "order.failed",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "failed",
"timestamp": "2026-01-15T14:30:00Z",
"data": {
"error_type": "product_not_found",
"error": "The product is no longer available"
}
}
```
**`order.tracking_received`** includes the tracking numbers we received:
```json theme={null}
{
"event": "order.tracking_received",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "shipped",
"timestamp": "2026-01-15T14:30:00Z",
"data": {
"tracking_numbers": [
{
"carrier": "UPS",
"tracking_number": "1Z999AA10123456784"
}
]
}
}
```
**`order.delivered`** fires when every package on the order has been delivered:
```json theme={null}
{
"event": "order.delivered",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "delivered",
"timestamp": "2026-01-15T14:30:00Z",
"data": {
"tracking_numbers": [
{
"id": "tn_01HXYZ...",
"carrier": "UPS",
"tracking_number": "1Z999AA10123456784",
"delivered_at": "2026-01-15T13:42:00Z"
}
]
}
}
```
**`order.cancelled`** fires when the retailer cancels the order after placement. The order's wallet hold is refunded at the same time:
```json theme={null}
{
"event": "order.cancelled",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "cancelled_by_retailer",
"timestamp": "2026-01-15T14:30:00Z",
"data": {
"reason": "cancelled_by_retailer",
"merchant_order_id": "112-1234567-1234567",
"refund_amount": 2648
}
}
```
**`return.created`** fires when a customer files a return against one of your orders. The `data` object echoes the return reason, free-text notes, and the line items being returned:
```json theme={null}
{
"event": "return.created",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"return_id": "a1b2c3d4-e29b-41d4-a716-446655440099",
"status": "open",
"timestamp": "2026-01-20T10:00:00Z",
"data": {
"reason": "damaged",
"notes": "Box arrived crushed",
"items": [
{ "order_item_id": "item-abc", "quantity": 1 }
]
}
}
```
**`return.approved`** fires when the return is approved. Where applicable, the `data` object carries the merchant-issued return id and a printable label URL so you can hand them to the customer:
```json theme={null}
{
"event": "return.approved",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"return_id": "a1b2c3d4-e29b-41d4-a716-446655440099",
"status": "approved",
"timestamp": "2026-01-20T14:30:00Z",
"data": {
"reason": "damaged",
"resolution_notes": "RMA filed with Amazon",
"merchant_return_id": "RMA-1234",
"external_label_url": "https://carrier.example.com/labels/abc.pdf"
}
}
```
**`return.denied`** fires when the return is denied (e.g. outside the retailer's return window). `merchant_return_id` and `external_label_url` are null on denied returns:
```json theme={null}
{
"event": "return.denied",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"return_id": "a1b2c3d4-e29b-41d4-a716-446655440099",
"status": "denied",
"timestamp": "2026-01-20T14:30:00Z",
"data": {
"reason": "no_longer_needed",
"resolution_notes": "Outside return window",
"merchant_return_id": null,
"external_label_url": null
}
}
```
**`return.credited`** fires when the return is resolved by crediting the customer's wallet rather than working a merchant RMA. There's no carrier label or merchant return id in this flow:
```json theme={null}
{
"event": "return.credited",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"return_id": "a1b2c3d4-e29b-41d4-a716-446655440099",
"status": "credited",
"timestamp": "2026-01-20T14:30:00Z",
"data": {
"reason": "damaged",
"resolution_notes": "Item unrecoverable, no RMA needed",
"merchant_return_id": null,
"external_label_url": null
}
}
```
## Security
Webhook requests include headers for verification:
| Header | Description |
| --------------------- | ------------------------------------ |
| `Content-Type` | Always `application/json` |
| `X-Webhook-Signature` | HMAC-SHA256 signature of the payload |
| `X-Webhook-Event` | The event type |
### Verifying Signatures
To verify a webhook is from Zinc, compute the HMAC-SHA256 signature of the raw request body using your webhook secret and compare it to the `X-Webhook-Signature` header.
**Python Example:**
```python theme={null}
import hmac
import hashlib
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
# In your webhook handler
@app.post("/webhook")
async def handle_webhook(request: Request):
payload = await request.body()
signature = request.headers.get("X-Webhook-Signature")
if not verify_webhook(payload, signature, WEBHOOK_SECRET):
raise HTTPException(status_code=401, detail="Invalid signature")
data = json.loads(payload)
# Process the webhook event
```
**Node.js Example:**
```javascript theme={null}
const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
// In your webhook handler
app.post('/webhook', (req, res) => {
const signature = req.headers['x-webhook-signature'];
if (!verifyWebhook(req.rawBody, signature, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = req.body;
// Process the webhook event
});
```
Always verify webhook signatures before processing the payload to ensure the request originated from Zinc.
## Best Practices
1. **Respond quickly** - Return a 2xx status code as soon as possible. Process the webhook asynchronously if needed.
2. **Handle duplicates** - Webhooks may occasionally be delivered more than once. Use the `order_id` to deduplicate.
3. **Verify signatures** - Always validate the `X-Webhook-Signature` header before trusting the payload.
4. **Use HTTPS** - Configure an HTTPS endpoint to ensure webhook data is encrypted in transit.
5. **Log events** - Keep records of received webhooks for debugging and auditing.
# Create Managed Account
Source: https://www.zinc.com/docs/v2/api-reference/managed-accounts/create-managed-account
versions/latest.json POST /managed-accounts
Add retailer account credentials (a managed account) for Zinc to place orders with via the API.
Create new retailer credentials for order processing. Credentials are encrypted and stored securely.
## Request Fields
* **email** (required) - The email address for the retailer account
* **password** - The password for the retailer account (encrypted at rest)
* **retailer** - Retailer name (e.g., `amazon`). If omitted, applies as default credentials
* **totp\_secret** - TOTP secret key for two-factor authentication (encrypted at rest)
If your retailer account has 2FA enabled, you must provide the `totp_secret` to avoid verification issues during order processing. See the [Managed Accounts guide](/docs/v2/api-reference/configuration/managed-accounts#two-factor-authentication-totp) for details on finding your TOTP key.
## Response
Returns the created credential object with:
* **id** - Unique identifier (UUID)
* **short\_id** - Short identifier used in URLs (e.g., `zn_acct_a1b2c3d4`)
* **email** - Retailer account email
* **retailer** - Retailer name, or null if default credentials
* **has\_totp** - Whether TOTP 2FA is configured
* **has\_forwarding** - Whether email forwarding has been verified
* **created\_at** - Creation timestamp
* **updated\_at** - Last update timestamp
Passwords and TOTP secrets are never returned in API responses. The `has_totp` field indicates whether 2FA is configured.
# Delete Managed Account
Source: https://www.zinc.com/docs/v2/api-reference/managed-accounts/delete-managed-account
versions/latest.json DELETE /managed-accounts/{short_id}
Delete a managed account and its stored retailer credentials from your Zinc account.
Permanently delete retailer credentials. This action cannot be undone.
## Path Parameters
* **short\_id** (required) - The short identifier of the credentials to delete (e.g., `zn_acct_a1b2c3d4`)
Deleting credentials that are actively in use by a processing order may cause the order to fail. Ensure no orders are currently processing with these credentials before deleting.
# List Managed Accounts
Source: https://www.zinc.com/docs/v2/api-reference/managed-accounts/list-managed-accounts
versions/latest.json GET /managed-accounts
List all managed accounts and stored retailer credentials on your Zinc account.
Retrieve a list of all retailer credentials associated with your account.
## Response
Returns an object containing:
* **credentials** - Array of retailer credential objects
* **total** - Total number of credentials
Each credential includes:
* **id** - Unique identifier (UUID)
* **short\_id** - Short identifier used in URLs (e.g., `zn_acct_a1b2c3d4`)
* **email** - Retailer account email
* **retailer** - Retailer name, or null if default credentials
* **has\_totp** - Whether TOTP 2FA is configured
* **has\_forwarding** - Whether email forwarding has been verified
* **created\_at** - When the credentials were created
* **updated\_at** - When the credentials were last updated
# Update Managed Account
Source: https://www.zinc.com/docs/v2/api-reference/managed-accounts/update-managed-account
versions/latest.json PUT /managed-accounts/{short_id}
Update the stored retailer credentials for an existing Zinc managed account.
Update an existing managed account's retailer credentials. Only provided fields are updated.
## Path Parameters
* **short\_id** (required) - The short identifier of the credentials to update (e.g., `zn_acct_a1b2c3d4`)
## Request Fields
All fields are optional. Only provided fields are updated.
* **email** - New email address
* **password** - New password (encrypted at rest)
* **retailer** - New retailer association
* **totp\_secret** - Update the 2FA secret key (encrypted at rest)
## Response
Returns the updated credential object.
# Cancel Order
Source: https://www.zinc.com/docs/v2/api-reference/orders/cancel-order
versions/latest.json POST /orders/{order_id}/cancel
Cancel an in-progress Zinc order before retailer fulfillment completes.
Cancel an order that is currently being processed. Currently cancellation is only possible while the order is still waiting in the queue to be processed. Once we start an order, we are unable to cancel it.
Orders that have already been started or completed cannot be cancelled.
# Create Bulk Upload
Source: https://www.zinc.com/docs/v2/api-reference/orders/create-bulk-upload
versions/latest.json POST /orders/bulk
Place many orders at once by uploading a CSV batch to the Zinc API.
Create a bulk-upload batch from parsed CSV rows. The batch is accepted
immediately and its rows are placed as individual orders asynchronously.
Validate the upload first with
[Validate Bulk Upload](/docs/v2/api-reference/orders/validate-bulk-upload) — it
reports per-row errors and estimated spend without placing anything.
## Request
The request body is identical to the validate endpoint:
* **filename** *(optional)* — Original uploaded filename, for display.
* **rows** — Array of objects, one per CSV row, mapping column header to cell value.
Each row becomes one order, so a row's `product_urls` may list at most **10**
products — a row with more fails validation with
`Order cannot contain more than 10 items`. A batch itself may contain at most
**500 rows**.
Unrecognized columns are preserved as `passthrough` data on each row and
echoed back in the [results CSV](/docs/v2/api-reference/orders/download-bulk-results).
## Response
Returns the new batch with `status: "pending"`. Track progress by polling
[Get Bulk Upload](/docs/v2/api-reference/orders/get-bulk-upload):
* **id** — Batch UUID.
* **status** — `pending` → `processing` → `completed` (or `failed`).
* **row\_count** / **placed\_count** / **failed\_count** — Row tallies, updated as
rows are processed.
Each successfully placed row becomes a regular order — it appears in
[List Orders](/docs/v2/api-reference/orders/list-orders) and emits the usual
[webhook events](/docs/v2/api-reference/introduction/webhooks).
# Create Order
Source: https://www.zinc.com/docs/v2/api-reference/orders/create-order
versions/latest.json POST /orders
Place a purchase order on Amazon, Walmart, and other retailers via the Zinc API.
Create a new order for processing. Orders are queued and processed asynchronously.
## Request Flow
1. **Submit Order** - Send order details including products and shipping address
2. **Validation** - We validate product URLs and shipping address
3. **Queued** - Order is queued for processing
4. **Processing** - Our system places the order with the retailer
5. **Completed** - You receive confirmation with tracking details
## Product URLs
Provide direct product URLs from supported retailers. Each product must include:
* **url** - Direct link to the product page
Make sure product URLs are accessible and lead directly to the product page, not search results or category pages.
An order can contain at most **10 products**. More than that is rejected with
`422` / `validation_error` — `Order cannot contain more than 10 items`. The limit
is on entries in the `products` array, not units: one entry with `quantity: 50`
still counts as one. See
[The Products Array](/docs/v2/api-reference/orders/multiple-products-quantities#products-array).
Optionally, the product can include:
* **quantity** - Number of items to order (integer, default 1)
* **variant** - A list of label, value pairs indicating a variant of a product.
For example, if you're ordering a shirt. The shirt may come in different colors and different sizes.
To indicate a red medium shirt, you would do:
```json theme={null}
[
{
"label": "Color",
"value": "Red"
},
{
"label": "Size",
"value": "Medium"
}
]
```
Make sure the strings used for both the label and value match up to what is
present on the retailer website. For example, if a `medium` is indicated by the value `M`, use `M`
for the value.
* **condition\_in** / **condition\_not\_in** - Condition allow/deny lists. Limit
which offers are eligible by item condition — useful for buying only new
items, or for accepting used items down to a floor. See
[Condition Filtering](/docs/v2/api-reference/orders/multiple-products-quantities#condition-filtering).
## Shipping Address
All orders require a valid shipping address. Addresses are validated using Google's Address Validation API.
Required fields:
* `first_name` and `last_name`
* `address_line1` (use `address_line2` for apartment/suite, optional)
* `city`
* `postal_code`
* `phone_number`
Optional fields:
* `state` — omit for countries that don't use states/provinces
* `country` — ISO 3166-1 alpha-2 code (e.g. `US`, `CA`, `GB`, `DE`); defaults to `US`
International shipping is supported. Provide the destination `country` as an ISO 3166-1 alpha-2 code; `state` is optional where it doesn't apply.
## Payment
By default, orders draw from your prepaid [wallet](/docs/v2/wallet) — no `payment` object needed. To charge your own end-customer's card instead and keep a margin, include a `payment` object with `mode: "connect"`. See the [Stripe Connect](/docs/v2/connect) guide for the full flow.
| Field | Required | Description |
| ---------------- | -------- | --------------------------------------------------------------------------------------------------- |
| `mode` | | `"wallet"` (default) or `"connect"`. |
| `payment_method` | connect | Your end-customer's saved Stripe payment-method id (`pm_…`) on your connected account. |
| `customer` | connect | Your end-customer's Stripe Customer id (`cus_…`) on your connected account. |
| `margin` | connect | Your markup: `{ "type": "flat", "value": }` or `{ "type": "percent", "value": }`. |
In Connect mode the authorization hold is sized from `max_price`; your end-customer is charged the actual total when Zinc places the order with the retailer.
```json theme={null}
{
"payment": {
"mode": "connect",
"payment_method": "pm_1ExampleCard",
"customer": "cus_ExampleEndCustomer",
"margin": { "type": "flat", "value": 250 }
}
}
```
## Optional Order Data
You can include additional data with your order for tracking and reference purposes.
This data *will* be used by our system as input to any fields during checkout that match.
* **po\_number** - Your internal purchase order number for tracking and reconciliation
* **is\_gift** - Mark the order as a gift (boolean, default `false`), suppressing
prices on the packing slip. See [Gift orders](#gift-orders) for the failure
behavior when a retailer offers no gift option.
* **gift\_message** - Optional note for the recipient, entered into the retailer's
gift-message field at checkout (string, max 240 characters). Requires `is_gift`
to be `true`.
* **handling\_days\_max** - Optional ceiling on a seller's handling days (integer,
minimum `1`). Offers from sellers whose handling time exceeds this are skipped.
Omit or send `null` for no limit.
If you only need data for internal reference, use the `metadata` field instead.
## Gift orders
Setting `is_gift: true` suppresses prices on the packing slip. Because a gift that
arrives with prices visible to the recipient is worse than no order at all, this is
treated as a hard requirement rather than a preference:
If the retailer's checkout offers no free gift option, the order **fails** with
`gift_option_unavailable` instead of being placed as a normal order. Handle this
error type if you offer gifting as an optional add-on — see
[Order Processing Errors](/docs/v2/api-reference/introduction/error-handling#gift-errors).
`gift_message` is best-effort by contrast: it's delivered where the retailer's
checkout offers a gift-message field, and the order is still placed without it
where one isn't available.
```json theme={null}
{
"is_gift": true,
"gift_message": "Happy birthday! Hope you enjoy it."
}
```
`is_gift` and `gift_message` are also accepted on [`POST /agent/orders`](/docs/v2/api-reference/agent/create-order), which shares the same order-creation schema.
## Response
A successful order creation returns:
* **id** - Unique order identifier (UUID)
* **status** - Current order status (initially "pending")
* **items** - Array of order items with their details
* **shipping\_address** - Confirmed shipping address
* **created\_at** - Timestamp of order creation
Use the order `id` to retrieve order status and updates.
# Download Bulk Results
Source: https://www.zinc.com/docs/v2/api-reference/orders/download-bulk-results
versions/latest.json GET /orders/bulk/{batch_id}/results.csv
Download the per-order results of a Zinc bulk upload batch as a CSV file.
Download the batch results as a CSV. Each row contains the placement status,
the order ID (when placed), any error message, and your original custom
columns echoed back — handy for reconciling against the file you uploaded.
The response is `text/csv`, not JSON. Results are available as soon as rows
start processing, but the file is only complete once the batch status is
`completed`.
# Get Bulk Upload
Source: https://www.zinc.com/docs/v2/api-reference/orders/get-bulk-upload
versions/latest.json GET /orders/bulk/{batch_id}
Retrieve a Zinc bulk upload batch with per-order status and results.
Retrieve a batch with per-row results and live order statuses.
## Batch Statuses
| Status | Description |
| ------------ | -------------------------------- |
| `pending` | Accepted, rows not yet processed |
| `processing` | Rows are being placed |
| `completed` | Every row has been processed |
| `failed` | The batch could not be processed |
## Per-Row Results
Each entry in `rows` reports the outcome for one CSV row:
* **status** — `pending`, `placed`, or `failed`.
* **order\_id** / **order\_status** — Set once the row is placed; use
[Get Order](/docs/v2/api-reference/orders/get-order) for full order details.
* **error** — Failure reason for `failed` rows.
* **passthrough** — Your unrecognized CSV columns, echoed back for reconciliation.
Poll this endpoint while the batch is `pending` or `processing`. Once it's
`completed`, download the full results with
[Download Bulk Results](/docs/v2/api-reference/orders/download-bulk-results).
# Get Order
Source: https://www.zinc.com/docs/v2/api-reference/orders/get-order
versions/latest.json GET /orders/{order_id}
Retrieve the status and details of a specific Zinc order by its ID.
Retrieve detailed information about a specific order using its unique identifier.
## Path Parameters
* **order\_id** (required) - The UUID of the order to retrieve
## Response
Returns a complete order object with:
* **id** - Order UUID
* **status** - Current order status
* **items** - Array of order items with individual statuses
* **shipping\_address** - Delivery address
* **job\_result** - Detailed processing results (when available)
* **merchant\_order\_ids** - The retailer's own order number(s) for this order
* **created\_at** - Order creation timestamp
* **updated\_at** - Last update timestamp
## Merchant Order IDs
`merchant_order_ids` holds the retailer's own order number(s) for this order — for
example an Amazon `113-…` ID — as recorded when the order was placed. Use it to
cross-reference Zinc orders against retailer records, invoices, or a customer's
account history.
```json theme={null}
{
"merchant_order_ids": ["113-1234567-1234567"]
}
```
The array is empty while the order is still processing, or if it never reached the
retailer. It holds more than one ID when a retailer splits the order into multiple
order-placing jobs.
You can look an order up by this value with [`GET /orders?merchant_order_id=…`](/docs/v2/api-reference/orders/list-orders). The filter is an exact match, not a partial one, and matches dashes both as typed and stripped.
## Item-Level Status
Each item in the order has its own status tracking:
```json theme={null}
{
"id": "item-uuid",
"url": "https://www.amazon.com/...",
"quantity": 2,
"status": "shipped",
"created_at": "2025-11-24T10:00:00Z",
"updated_at": "2025-11-24T12:30:00Z"
}
```
## Job Results
For completed or failed orders, the `job_result` field contains detailed information about the order processing, including:
* Success/failure status
* Retailer confirmation numbers
* Tracking information
* Error details (if failed)
Poll this endpoint to track order progress. We recommend checking every 30-60 seconds while the order is processing.
## Price Components
Once the retailer total is known, `job_result.price_components` breaks the charge down. All amounts are in **cents**.
| Field | Description |
| ------------------------- | --------------------------------------------------------------- |
| `subtotal` | Item subtotal before tax and shipping |
| `tax` | Tax charged by the retailer |
| `shipping` | Shipping charged by the retailer |
| `total` | Order total in the order currency (`subtotal + tax + shipping`) |
| `converted_payment_total` | `total` converted to the currency actually charged |
| `currency` | Currency of `total` (e.g. `USD`) |
| `payment_currency` | Currency the order was paid in |
| `line_items` | Itemized rows, each `{ description, amount, category }` |
```json theme={null}
{
"job_result": {
"success": true,
"price_components": {
"subtotal": 4800,
"tax": 396,
"shipping": 0,
"total": 5196,
"converted_payment_total": 5196,
"currency": "USD",
"payment_currency": "USD",
"line_items": [
{ "description": "Subtotal", "amount": 4800, "category": "subtotal" },
{ "description": "Tax", "amount": 396, "category": "tax" }
]
}
}
}
```
## Connect Charge
For orders paid via [Stripe Connect](/docs/v2/connect), the response includes a `connect` object with the charge breakdown and its current state. It is `null` for prepaid-wallet orders. All amounts are in **cents**.
| Field | Description |
| ---------------------- | ---------------------------------------------------------------------------------------------- |
| `state` | `secured` → `captured`, or `released` / `refunded` on a terminal outcome. |
| `secured_amount` | Amount held on the end-customer's card when the order was placed (sized from `max_price`). |
| `order_cost` | Actual order cost (goods), captured once Zinc places the order with the retailer. |
| `customer_margin` | Your margin, transferred to your connected account. |
| `zinc_fee` | Zinc's platform fee. |
| `stripe_fee` | Stripe's processing fee on the charge. |
| `final_charge` | Total captured from the end-customer (`order_cost + customer_margin + zinc_fee + stripe_fee`). |
| `transfer_amount` | Amount transferred to your connected account (your margin). |
| `payment_intent_id` | Stripe PaymentIntent id (`pi_…`) for the charge. |
| `connected_account_id` | Your connected account id (`acct_…`). |
| `simulated` | `true` for test-mode orders that didn't hit Stripe. |
The post-capture fields (`order_cost` and below) are populated once the order is placed and the actual total is captured; before that they are `null` and `state` is `secured`.
```json theme={null}
{
"connect": {
"state": "captured",
"secured_amount": 6041,
"order_cost": 4800,
"customer_margin": 250,
"zinc_fee": 100,
"stripe_fee": 185,
"final_charge": 5335,
"transfer_amount": 250,
"payment_intent_id": "pi_3ExampleCharge",
"connected_account_id": "acct_1ExampleConnected",
"simulated": false
}
}
```
## Error Responses
* **404 Not Found** - Order ID does not exist or you don't have access to it
* **401 Unauthorized** - Invalid or missing authentication
# Get Order Timeline
Source: https://www.zinc.com/docs/v2/api-reference/orders/get-order-timeline
versions/latest.json GET /orders/{order_id}/timeline
Retrieve the customer-facing lifecycle timeline of events for a Zinc order.
Retrieve the customer-facing lifecycle timeline for an order. The timeline merges the order's placement outcome with carrier tracking state into an ordered list of milestones.
The timeline is derived from existing order and tracking data on every
request, so it is available for all orders — including orders placed before
this endpoint was introduced.
# Get Public Tracking
Source: https://www.zinc.com/docs/v2/api-reference/orders/get-public-tracking
versions/latest.json GET /track/{order_id}
Retrieve the public, unauthenticated tracking view for an order by its UUID.
Returns a public tracking view for a single order, keyed by its UUID. The order's random UUID doubles as an unguessable share token, so this endpoint requires **no authentication** — it's suitable for a customer-facing tracking page or a shareable link.
The response includes the order status, destination address, per-package carrier checkpoints and ETAs, and the ordered items.
The order UUID is the only credential protecting this view. Treat the tracking
link as sensitive and share it only with the recipient.
For privacy, the destination deliberately omits the recipient's name and phone
number. Every other part of the shipping address (street, city, state, ZIP,
country) is included.
# List Bulk Uploads
Source: https://www.zinc.com/docs/v2/api-reference/orders/list-bulk-uploads
versions/latest.json GET /orders/bulk
List all bulk upload batches placed through your Zinc account.
List your bulk-upload batches, newest first.
## Query Parameters
* **limit** — Number of batches to return (1–200, default 50)
* **offset** — Number of batches to skip (default 0)
## Response
An object with a `batches` array and a `total` count. Each batch includes its
`status`, row tallies, and timestamps — see
[Get Bulk Upload](/docs/v2/api-reference/orders/get-bulk-upload) for the full shape
including per-row results.
# List Orders
Source: https://www.zinc.com/docs/v2/api-reference/orders/list-orders
versions/latest.json GET /orders
List and filter all orders placed through your Zinc account.
Retrieve a list of all orders associated with your account. Orders are returned in reverse chronological order (most recent first).
## Response
Returns an array of order objects, each containing:
* **id** - Order UUID
* **status** - Current order status
* **items** - Products in the order
* **shipping\_address** - Delivery address
* **job\_result** - Processing results (for completed/failed orders)
* **merchant\_order\_ids** - The retailer's own order number(s), once placed — see [Merchant Order IDs](/docs/v2/api-reference/orders/get-order#merchant-order-ids)
* **created\_at** - Order creation timestamp
* **updated\_at** - Last update timestamp
## Order Statuses
| Status | Description |
| ------------- | ---------------------------------- |
| `pending` | Order queued, not yet processing |
| `in_progress` | Order is being placed |
| `ordered` | Successfully placed with retailer |
| `shipped` | Order shipped (tracking available) |
| `delivered` | Order delivered to address |
| `cancelled` | Order was cancelled |
| `failed` | Order processing failed |
## Expanding tracking events
By default, list responses keep tracking payloads small: each tracking number includes its `status` but **not** the full carrier checkpoint timeline. Pass `include=tracking_events` to embed the per-scan `checkpoints` array on every tracking number.
```bash theme={null}
curl "https://api.zinc.com/orders?include=tracking_events" \
-H "Authorization: Bearer "
```
The single-order read ([`GET /orders/{order_id}`](/docs/v2/api-reference/orders/get-order)) always includes the full checkpoint timeline — `include` only affects the list endpoint. See [Order Tracking](/docs/v2/api-reference/orders/tracking) for the checkpoint fields.
## Pagination
Currently, all orders are returned in a single response. Pagination will be added in a future update.
# The Products Array
Source: https://www.zinc.com/docs/v2/api-reference/orders/multiple-products-quantities
Build the Zinc order products array — multiple items, quantities, variants, and condition.
The Zinc API allows you to order multiple products in a single request and specify quantities for each item.
## Products Array
The `products` array in your order request accepts up to **10** `OrderProduct` objects. Each product is processed as part of the same order.
An order can contain at most **10 products**. A request with more is rejected with
`422` / `validation_error` — `Order cannot contain more than 10 items`. Split
larger baskets across multiple order requests, or use a
[bulk upload](/docs/v2/api-reference/orders/create-bulk-upload) (each row is itself
one order, and the same 10-product limit applies per row).
The limit counts **entries in the `products` array**, not units — a single entry
with `quantity: 50` is one of your 10.
All products in an order must be from the same retailer. You cannot mix products from different retailers in a single order request.
```json theme={null}
{
"products": [
{
"url": "https://www.amazon.com/dp/B07JGBW826"
},
{
"url": "https://www.amazon.com/dp/B09V3KXJPB"
}
],
"shipping_address": { ... },
"max_price": 5000
}
```
## Setting Quantities
Each product can include a `quantity` field to specify how many units to order. What
will be accepted by the retailer depends on the product and availability. We will return
an error code, `product_quantity_unavailable` if we are unable to purchase the amount
specified.
* **Range**: 1 to 100
* **Default**: 1 (if omitted)
```json theme={null}
{
"products": [
{
"url": "https://www.amazon.com/dp/B07JGBW826",
"quantity": 3
}
],
"shipping_address": { ... },
"max_price": 3000
}
```
## Combining with Variants
When ordering products with variants (size, color, etc.), you can combine the `variant` array with `quantity`:
```json theme={null}
{
"products": [
{
"url": "https://www.amazon.com/dp/B07JGBW826",
"quantity": 2,
"variant": [
{
"label": "Color",
"value": "Black"
},
{
"label": "Size",
"value": "Large"
}
]
}
],
"shipping_address": { ... },
"max_price": 4000
}
```
## Condition Filtering
Each product can constrain which offers are eligible by item condition using
`condition_in` (an allowlist) and `condition_not_in` (a denylist). The agent
only buys an offer whose condition passes both lists.
Both fields take an array of canonical condition values:
| Value | Meaning |
| ---------------- | ---------------------------------- |
| `New` | Brand new |
| `Refurbished` | Manufacturer or seller refurbished |
| `UsedLikeNew` | Used — like new |
| `UsedVeryGood` | Used — very good |
| `UsedGood` | Used — good |
| `UsedAcceptable` | Used — acceptable |
Matching is **exact and case-sensitive** against these values. Omit a field, or
send `null` / `[]`, for no constraint.
Buy only new items:
```json theme={null}
{
"products": [
{
"url": "https://www.amazon.com/dp/B07JGBW826",
"condition_in": ["New"]
}
],
"shipping_address": { ... },
"max_price": 3000
}
```
Accept used items, but nothing below "good":
```json theme={null}
{
"products": [
{
"url": "https://www.amazon.com/dp/B07JGBW826",
"condition_in": ["New", "Refurbished", "UsedLikeNew", "UsedVeryGood", "UsedGood"]
}
],
"shipping_address": { ... },
"max_price": 3000
}
```
Allow anything except acceptable-grade used items:
```json theme={null}
{
"products": [
{
"url": "https://www.amazon.com/dp/B07JGBW826",
"condition_not_in": ["UsedAcceptable"]
}
],
"shipping_address": { ... },
"max_price": 3000
}
```
## Complete Example
Here's an example ordering multiple products with different quantities and variants:
```json theme={null}
{
"products": [
{
"url": "https://www.amazon.com/dp/B07JGBW826",
"quantity": 2,
"variant": [
{
"label": "Color",
"value": "Navy"
}
]
},
{
"url": "https://www.amazon.com/dp/B09V3KXJPB",
"quantity": 1
},
{
"url": "https://www.amazon.com/dp/B08N5WRWNW",
"quantity": 3,
"variant": [
{
"label": "Size",
"value": "Medium"
}
]
}
],
"shipping_address": {
"first_name": "John",
"last_name": "Smith",
"address_line1": "123 Main Street",
"city": "Seattle",
"state": "WA",
"postal_code": "98101",
"country": "US",
"phone_number": "206-555-0100"
},
"max_price": 15000
}
```
## Max Price Considerations
The `max_price` field applies to the **total order amount** across all products and quantities combined.
If the total order cost (including all products, quantities, taxes, and shipping) exceeds `max_price`, the order will fail with an error. Set your `max_price` high enough to account for the full order total.
When calculating `max_price`, consider:
* Unit price × quantity for each product
* Applicable taxes
* Shipping costs
* Any additional fees
# Order Tracking
Source: https://www.zinc.com/docs/v2/api-reference/orders/tracking
Retrieve carrier tracking numbers and delivery status for your Zinc orders.
Once an order has been placed with a retailer, tracking information becomes available as shipments are dispatched. Tracking numbers are automatically extracted from retailer shipping notifications and associated with your order.
## How Tracking Works
1. Your order is successfully placed with the retailer
2. The retailer ships the item and sends a shipping notification
3. We automatically extract tracking numbers from the notification
4. Tracking information appears in the order response
Tracking numbers are added to orders automatically. There is no separate endpoint to create or manage tracking numbers.
## Tracking in Order Response
Tracking information is returned as part of the order response when you retrieve an order:
```json theme={null}
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "order_placed",
"tracking_numbers": [
{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"carrier": "ups",
"tracking_number": "1Z999AA10123456784",
"status": "in_transit",
"estimated_delivery_date": "2026-01-18",
"checkpoints": [
{
"checkpoint_time": "2026-01-16T08:12:00Z",
"status": "in_transit",
"message": "Departed shipping facility",
"city": "Louisville",
"state": "KY",
"country": "US",
"zip": "40209",
"location": null
},
{
"checkpoint_time": "2026-01-15T18:40:00Z",
"status": "pending",
"message": "Shipping label created",
"city": null,
"state": null,
"country": "US",
"zip": null,
"location": null
}
],
"created_at": "2026-01-15T14:30:00Z"
}
],
...
}
```
### Tracking Number Fields
| Field | Type | Description |
| ------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------- |
| `id` | string (UUID) | Unique identifier for the tracking record |
| `carrier` | string | Shipping carrier (see supported carriers below) |
| `tracking_number` | string | The carrier's tracking number |
| `status` | string | Authoritative carrier-derived shipment state (see [Tracking Status](#tracking-status)). Always present. |
| `estimated_delivery_date` | string (date) \| null | Carrier-reported delivery estimate (see [Estimated Delivery Date](#estimated-delivery-date)). |
| `checkpoints` | array | Carrier scan events, most recent first (see [Checkpoint Timeline](#checkpoint-timeline)). |
| `created_at` | string (ISO 8601) | When the tracking number was extracted |
## Tracking Status
The `status` field is the carrier-derived state of the shipment and is always present on each tracking number:
| `status` value | Description |
| -------------- | ----------------------------------------- |
| `pending` | Label created; not yet scanned in transit |
| `in_transit` | Moving through the carrier network |
| `delivered` | Delivered to the address |
## Estimated Delivery Date
`estimated_delivery_date` is the carrier's current delivery estimate for the shipment, as a calendar date in the destination's local time zone (e.g. `"2026-01-18"`). It is refreshed on every carrier poll, so the value can shift as the carrier revises its estimate. It is `null` when the carrier hasn't reported an estimate — typically until the first in-transit scan — so always handle the null case.
## Checkpoint Timeline
`checkpoints` is the per-scan history of a tracking number, most recent first. Each checkpoint is a single carrier-reported scan event:
| Field | Type | Description |
| ----------------- | ----------------- | ------------------------------------------------------------------------ |
| `checkpoint_time` | string (ISO 8601) | When the carrier recorded this scan |
| `status` | string | Shipment state at this checkpoint (`pending`, `in_transit`, `delivered`) |
| `message` | string | Carrier-provided description of the scan |
| `city` | string \| null | Scan city, when available |
| `state` | string \| null | Scan state/region, when available |
| `country` | string \| null | Scan country, when available |
| `zip` | string \| null | Scan postal code, when available |
| `location` | string \| null | Free-form location string when `city`/`state` are absent |
The checkpoint timeline is embedded automatically on the single-order read ([`GET /orders/{order_id}`](/docs/v2/api-reference/orders/get-order)). On the [List Orders](/docs/v2/api-reference/orders/list-orders) endpoint it is omitted by default to keep payloads small — request it with `include=tracking_events`.
## Supported Carriers
The following carriers are automatically detected:
| Carrier | `carrier` value | Example Format |
| ---------------- | --------------- | ------------------------ |
| UPS | `ups` | `1Z999AA10123456784` |
| FedEx | `fedex` | `123456789012` |
| USPS | `usps` | `9400111899223033005001` |
| Amazon Logistics | `amazon` | `TBA123456789000` |
| DHL | `dhl` | `1234567890` |
## Multiple Tracking Numbers
An order may have multiple tracking numbers if:
* Items ship separately from the retailer
* Multiple products in the order ship from different fulfillment centers
```json theme={null}
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "order_placed",
"tracking_numbers": [
{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"carrier": "ups",
"tracking_number": "1Z999AA10123456784",
"created_at": "2026-01-15T14:30:00Z"
},
{
"id": "8d0f7780-8536-51ef-055c-f18fd2g01bf8",
"carrier": "amazon",
"tracking_number": "TBA123456789000",
"created_at": "2026-01-16T09:15:00Z"
}
]
}
```
## Tracking Links
You can construct tracking URLs for each carrier:
| Carrier | Tracking URL |
| ------- | ------------------------------------------------------------------------------- |
| UPS | `https://www.ups.com/track?tracknum={tracking_number}` |
| FedEx | `https://www.fedex.com/fedextrack/?trknbr={tracking_number}` |
| USPS | `https://tools.usps.com/go/TrackConfirmAction?tLabels={tracking_number}` |
| Amazon | `https://www.amazon.com/progress-tracker/package/?trackingId={tracking_number}` |
| DHL | `https://www.dhl.com/us-en/home/tracking.html?tracking-id={tracking_number}` |
## When Tracking Is Available
Tracking numbers appear after the order status changes to `order_placed` and the retailer has shipped the item. The timing depends on:
* Retailer processing time
* Shipping method selected
* Product availability
Tracking numbers may not be available immediately after an order is placed. Check the order periodically to retrieve tracking information once items have shipped, or subscribe to the [`order.tracking_received` and `order.delivered` webhook events](/docs/v2/api-reference/introduction/webhooks) for push notifications.
# Validate Bulk Upload
Source: https://www.zinc.com/docs/v2/api-reference/orders/validate-bulk-upload
versions/latest.json POST /orders/bulk/validate
Validate a CSV bulk upload with the Zinc API before placing any real orders.
Dry-run a CSV upload: every row is validated and the estimated spend is
calculated, but **no orders are placed**. Use this to show a confirmation
preview before calling [Create Bulk Upload](/docs/v2/api-reference/orders/create-bulk-upload).
## Request
Send the parsed CSV as JSON:
* **filename** *(optional)* — Original uploaded filename, for display.
* **rows** — Array of objects, one per CSV row, mapping column header to cell value.
```json theme={null}
{
"filename": "may-orders.csv",
"rows": [
{
"product_url": "https://www.amazon.com/dp/B08N5WRWNW",
"quantity": "2",
"name": "Jane Doe",
"address1": "123 Main St",
"city": "Portland",
"state": "OR",
"zip": "97201",
"max_price": "4999",
"internal_ref": "PO-1042"
}
]
}
```
Columns that don't map to a known order field are echoed back as
`passthrough` data on each row and included in the results CSV.
## Response
The summary tells you whether the batch is safe to place:
* **total\_rows** / **valid\_rows** / **invalid\_rows** — Row counts.
* **estimated\_total\_cents** — Estimated spend across all valid rows.
* **wallet\_balance\_cents** — Your current wallet balance.
* **sufficient\_funds** — Whether your balance covers the estimate.
* **rows** — Per-row validation results, each with `ok` and any `errors`.
Surface `rows[].errors` to the user so they can fix the CSV and re-validate.
Only call [Create Bulk Upload](/docs/v2/api-reference/orders/create-bulk-upload)
once `invalid_rows` is `0` and `sufficient_funds` is `true`.
# Get Product Details
Source: https://www.zinc.com/docs/v2/api-reference/products/get-product
versions/latest.json GET /products/{product_id}
Retrieve product details, images, and attributes from a retailer via the Zinc API.
# Get Product Offers
Source: https://www.zinc.com/docs/v2/api-reference/products/get-product-offers
versions/latest.json GET /products/{product_id}/offers
Retrieve all seller offers and prices for a product from a retailer via the Zinc API.
# Search Products
Source: https://www.zinc.com/docs/v2/api-reference/products/search
versions/latest.json GET /products/search
Search retailer catalogs for products, prices, and offer data via the Zinc API.
# List Retailers
Source: https://www.zinc.com/docs/v2/api-reference/retailers/list-retailers
versions/latest.json GET /retailers
Browse the public catalog of retailers supported by the Zinc API — no authentication required.
`GET /retailers` is the public "what do you support?" catalog. It returns one flat object per retailer brand with its identifier, domain, the countries Zinc ships to, and the free-shipping policy.
**No authentication required** — no API key, no MPP payment.
## Notes
* International marketplaces (e.g. `amazon.com` / `amazon.de`) are grouped under a single brand, with each country listed in `supported_countries` (ISO 3166-1 alpha-2; defaults to `["US"]`).
* `free_shipping` indicates whether free shipping is offered either unconditionally or above `free_shipping_threshold_cents`. A `null` threshold means there is no minimum.
* Orders are Zinc-managed by default (no retailer account needed), so account and guest-checkout details are omitted from this catalog.
* Use `limit` / `offset` to page through results (default `limit` 100, max 1000), and `name` to filter by a case-insensitive partial match.
```json 200 OK theme={null}
{
"retailers": [
{
"retailer": "amazon",
"display_name": "Amazon",
"supported": true,
"base_url": "amazon.com",
"supported_countries": ["US", "DE", "UK"],
"free_shipping": true,
"free_shipping_threshold_cents": 2500
},
{
"retailer": "walmart",
"display_name": "Walmart",
"supported": true,
"base_url": "walmart.com",
"supported_countries": ["US"],
"free_shipping": true,
"free_shipping_threshold_cents": 3500
}
],
"total": 2
}
```
# Create Return Request
Source: https://www.zinc.com/docs/v2/api-reference/returns/create-return
versions/latest.json POST /returns
Submit a return request for an existing Zinc order via the API.
Submit a return request for a previously placed order. Returns are reviewed
and resolved asynchronously — subsequent reads will show the current status
and any return labels.
## Request
Required fields:
* **order\_id** — UUID of the order being returned. The order must belong to the
caller and be in `order_placed` status.
* **items** — One or more line items being returned. Each entry has
`order_item_id` (UUID of a line on the order) and `quantity` (1–100).
* **reason** — One of the values in the Reasons table below.
Optional fields:
* **notes** — Free-form context (max 2000 characters). Recommended when
`reason` is `other`.
```json theme={null}
{
"order_id": "8c2d…",
"items": [
{ "order_item_id": "a1b2…", "quantity": 1 }
],
"reason": "damaged",
"notes": "Arrived with crushed corner."
}
```
## Return Reasons
| Reason | When to use |
| --------------------- | ----------------------------------------------------- |
| `damaged` | Arrived damaged in transit |
| `not_delivered` | Package never arrived |
| `empty_box` | Package arrived empty / item missing |
| `wrong_item` | A different item shipped than what was ordered |
| `defective` | Item works incorrectly / quality issue |
| `not_as_described` | Item doesn't match the listing |
| `wrong_size` | Apparel or sizing mismatch |
| `no_longer_needed` | Customer changed their mind |
| `forced_cancellation` | Retailer / system forced cancellation after placement |
| `other` | Anything else — include details in `notes` |
Use `other` only when none of the specific reasons fit. Categorized reasons
route faster.
## Response
A successful request returns the new return request with `status: "open"` and
an empty `label_urls` array. See
[Get Return Request](/docs/v2/api-reference/returns/get-return) for the full
response shape.
# Get Return Request
Source: https://www.zinc.com/docs/v2/api-reference/returns/get-return
versions/latest.json GET /returns/{return_request_id}
Retrieve the status and details of a specific Zinc return request by ID.
Retrieve a return request, including any return labels issued for it.
Each entry in `label_urls` is a signed URL that expires after **7 days**.
Re-fetch the return to refresh the URLs if a customer hasn't downloaded
the label in that window.
# List Return Requests
Source: https://www.zinc.com/docs/v2/api-reference/returns/list-returns
versions/latest.json GET /returns
List all return requests submitted through your Zinc account.
Retrieve every return request you've filed, sorted newest first.
## Response
An object with a `return_requests` array. Each entry has the same shape as
[Get Return Request](/docs/v2/api-reference/returns/get-return) — see that page for
the full field list.
## Return Request Statuses
| Status | Description |
| ---------- | ----------------------------------------------------------------------------------------- |
| `open` | Submitted, awaiting or in active processing |
| `approved` | Return was approved and completed |
| `denied` | Return request was denied |
| `credited` | Return was resolved by crediting the customer's wallet rather than working a merchant RMA |
`approved`, `denied`, and `credited` are all terminal states. Once a return
reaches one of them, it won't change again.
# Claim a Sandbox
Source: https://www.zinc.com/docs/v2/api-reference/sandbox/claim-sandbox
versions/latest.json POST /sandbox/claim
Fold a provisional sandbox — its order history and its API key — into a real Zinc account without breaking the agent that created it.
Folds a provisional sandbox into the **authenticated** account. This is what the `claim_url` from [the mint response](/docs/v2/api-reference/sandbox/mint-sandbox-key) leads to: a human signs in, and the agent's sandbox becomes part of their Zinc account.
Claiming is always a **merge** — signing in creates the account if it didn't exist, so by the time this endpoint runs there is always a real account to merge into.
## What moves
| | Result |
| -------------- | ------------------------------------------------------------------------ |
| Sandbox orders | Repointed to the claiming account. The test history survives. |
| API keys | **Reassigned, not revoked.** Whatever the agent hardcoded keeps working. |
The response reports the counts (`orders_moved`, `keys_moved`) plus a human-readable `message`.
Not revoking the key is the whole point of claiming rather than starting over. An agent that has been running against a sandbox for a week doesn't need redeployment to keep going.
## Errors
| Status | Cause |
| ------ | ----------------------------------------------------------------------------------------------------------------- |
| `404` | Unknown, already-used, or expired claim token. Tokens are **single-use** and die with the sandbox they belong to. |
| `403` | A sandbox key tried to claim a sandbox. This endpoint needs a signed-in Zinc account, not a `zn_test_` key. |
The claim token is returned **once**, in the `claim_url` at mint time — only its hash is stored, so the URL cannot be regenerated. If it's lost, mint a new sandbox.
## After claiming
A claimed key still only reaches the sandbox. To place real orders, the account owner mints a `zn_live_` key in the [dashboard](https://app.zinc.com) and funds a [wallet](/docs/v2/wallet). The agent can confirm the claim landed with [`GET /sandbox/status`](/docs/v2/api-reference/sandbox/get-sandbox-status).
# Agent Quickstart
Source: https://www.zinc.com/docs/v2/api-reference/sandbox/get-agent-quickstart
versions/latest.json GET /sandbox/quickstart
Fetch the zero-to-delivered-order agent quickstart as plain markdown, served for agents to execute rather than read.
Returns the agent quickstart as plain markdown (`text/markdown`). **No authentication required.**
```bash theme={null}
curl https://api.zinc.com/sandbox/quickstart
```
Unlike this documentation site, the quickstart is written to be **executed** by an agent: every step is a concrete request, and every response points at the next step. It covers minting a key, placing a sandbox order, watching the lifecycle, rehearsing each failure mode, and going live — in about two minutes end to end.
Point an agent at this URL when you want it to onboard itself. It's the same content as the [Agent Sandbox overview](/docs/v2/api-reference/sandbox/overview), shaped for a model's context window instead of a browser.
Zinc also serves a discovery file at [`/llms.txt`](https://api.zinc.com/llms.txt) that links the quickstart, the mint endpoint, and the OpenAPI spec.
# Get Sandbox Status
Source: https://www.zinc.com/docs/v2/api-reference/sandbox/get-sandbox-status
versions/latest.json GET /sandbox/status
Check whether the sandbox this key belongs to has been claimed into a real Zinc account yet.
Reports whether the sandbox behind the calling key has been [claimed](/docs/v2/api-reference/sandbox/claim-sandbox) yet.
An agent that hands its `claim_url` to a human has no other way to learn what happened. This endpoint closes that loop: poll it with your own key.
```bash theme={null}
curl https://api.zinc.com/sandbox/status \
-H "Authorization: Bearer $ZINC_KEY"
```
## Responses
| `claimed` | Case | `hint` tells you |
| --------- | ----------------- | ------------------------------------------------------------------------------- |
| `false` | Still provisional | Nobody has claimed it. Re-send the `claim_url`, or keep waiting. |
| `false` | Ordinary account | This key belongs to a regular account, not a claimed sandbox. Nothing to claim. |
| `true` | Claimed | The sandbox now belongs to a real account; `claimed_by_email` names it. |
Every response carries a `hint` in plain language, so an agent can act on the answer without special-casing each shape.
`claimed: true` is derived from the claim record on the account, not from the account merely being real — otherwise every ordinary account would report as claimed.
The response never includes `claim_url`. Only the token's hash is stored, so the URL can't be reproduced — keep the one from the [mint response](/docs/v2/api-reference/sandbox/mint-sandbox-key).
# Mint a Sandbox Key
Source: https://www.zinc.com/docs/v2/api-reference/sandbox/mint-sandbox-key
versions/latest.json POST /sandbox/keys
Mint a sandbox API key with no account and no authentication, and get a complete example order back with it.
Mints a provisional account and a `zn_test_` API key. **No authentication required** — this is the entry point for an agent that has nothing but the hostname.
The response is written to be self-guiding: send `api_key` as a Bearer token, `POST` the `example_order` body to `/orders` as-is, and work through `next` in order.
```bash theme={null}
curl -X POST https://api.zinc.com/sandbox/keys \
-H "Content-Type: application/json" \
-d '{"name": "acme-shopping-agent dev testing"}'
```
An empty body — or no body at all — is valid. Both fields are optional:
* `name` becomes the key's label in the dashboard.
* `email` is the address of whoever will [claim](/docs/v2/api-reference/sandbox/claim-sandbox) this sandbox later. It is never required and is not used for anything else.
## What you get back
| Field | Use |
| ---------------- | ------------------------------------------------------------------------------------------------------------ |
| `api_key` | The `zn_test_` key. Send as `Authorization: Bearer `; test keys route to the sandbox automatically. |
| `example_order` | A complete, valid `POST /orders` body. Send it unchanged for your first order. |
| `claim_url` | Give this to the human you work for. This is the **only** time it's returned — only its hash is stored. |
| `quickstart_url` | Where to fetch [the agent quickstart](/docs/v2/api-reference/sandbox/get-agent-quickstart) as markdown. |
| `expires_policy` | Plain-language statement of the 7-idle-day expiry. |
| `next` | The ordered list of steps to take now. |
Keys expire after **7 days without use**, and everything they created is sandbox data that is deleted with them. Any use resets the clock, so persist the key across runs rather than minting a new one each time.
## Rate limits
This endpoint is capped per IP per day, plus a global daily ceiling.
* **`429`** — daily cap reached. Reuse a key you already minted, or wait out `Retry-After` (the window resets at UTC midnight; `X-RateLimit-Reset` gives the exact timestamp).
* **`503`** — the sandbox or its limiter is temporarily unavailable. The limiter fails closed rather than leaving an unauthenticated write path uncapped. Retry after `Retry-After`.
Treat `429` as "reuse what you have", not "retry in a loop". A minted key is good for 7 idle days, so an agent that stores its key needs the mint endpoint roughly once.
# Agent Sandbox
Source: https://www.zinc.com/docs/v2/api-reference/sandbox/overview
The four /sandbox/* routes an agent uses to mint its own test key, onboard itself, and graduate into a real Zinc account.
The `/sandbox/*` routes let an autonomous agent onboard itself: mint a `zn_test_` key with no account, place sandbox orders, and later have a human fold the sandbox into a real Zinc account without invalidating the agent's key.
This group is the route-by-route reference. For the narrative version — why it works this way, the walkthrough, and what changes when you go live — start with the [Agent Sandbox guide](/docs/v2/agent-sandbox/overview).
## Endpoints
| Route | Auth | What it does |
| -------------------------------------------------------------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------- |
| [`POST /sandbox/keys`](/docs/v2/api-reference/sandbox/mint-sandbox-key) | None | Mints a provisional user and a `zn_test_` key. Returns the key, an example order, and the claim URL. |
| [`GET /sandbox/quickstart`](/docs/v2/api-reference/sandbox/get-agent-quickstart) | None | The agent quickstart as plain markdown. |
| [`POST /sandbox/claim`](/docs/v2/api-reference/sandbox/claim-sandbox) | Signed-in account | Folds a provisional sandbox into a real account. |
| [`GET /sandbox/status`](/docs/v2/api-reference/sandbox/get-sandbox-status) | Sandbox or live key | Whether this sandbox has been claimed yet. |
## Behavior worth knowing before you call them
* **Provisional accounts are sandbox-only.** A minted account can never act on live mode, claimed or not, and has no wallet — sandbox orders don't need one.
* **Keys expire after 7 days without use**, and everything they created is deleted with them. Any use resets the clock.
* **Minting is rate-limited** per IP per day plus a global ceiling. `429` means reuse the key you already have; `503` means the mint path is temporarily unavailable and failing closed rather than running uncapped. Both carry `Retry-After`.
* **Claim tokens are single-use** and returned exactly once, in the mint response's `claim_url`. Only the hash is stored, so the URL cannot be regenerated.
* **Claiming reassigns the key rather than revoking it**, which is the point — the agent keeps working afterward.
## Related
The flow end to end, with a quickstart and a going-live checklist.
Test mode itself — the magic product URLs and the scenarios they simulate.
# Cross-Retailer Search
Source: https://www.zinc.com/docs/v2/api-reference/search/cross-retailer
versions/latest.json GET /search
Search every retailer Zinc supports in a single API call to compare products and prices (beta).
**Beta** — this endpoint is in active development and may change without notice. Field names, ranking, and coverage are still evolving. We'd love your feedback: [**book a 30-minute customer interview**](https://cal.com/zinc-ian/customer-interview) and help shape the v1 — **we'll send you a \$50 Amazon gift card** as a thank-you.
`GET /search` returns a single ranked list of buyable products across every supported retailer — Amazon, Walmart, Target, Best Buy, Home Depot, Lowe's, Costco, eBay, Wayfair, Macy's, and more — in one call.
Each result's `url` is the orderable retailer URL: pass it directly to [Create order](/docs/v2/api-reference/orders/create-order). There is no opaque sku id and no separate ASIN / item-id field — the URL is the contract.
This is distinct from the per-retailer [`/products/search`](/docs/v2/api-reference/products/search), which searches one retailer at a time. Use `/search` when you want breadth.
## Ranking
Results are ranked by a quality signal that combines rating and review volume, with price as a tiebreaker for unrated items. The list is then woven across retailers so a single retailer doesn't dominate the top — typically you'll see two or three different retailers in the first few results.
## Beta limits
These will change before v1:
* No condition filter — used / marketplace listings may appear when the source doesn't expose condition.
* Coverage varies by query; some long-tail and DTC retailers may not appear.
* No `limit`, `sort`, or `filter` parameters yet.
* Response shape may add fields (e.g. condition, shipping ETA). Existing fields won't be removed without notice.
## Help shape the beta
We're actively iterating on `/search`. Tell us what queries you're running, what coverage you need, and what's missing. As a thank-you we'll send you a **\$50 Amazon gift card**.
```bash cURL theme={null}
curl https://api.zinc.com/search?q=cast+iron+skillet \
-H "Authorization: Bearer "
```
```python Python theme={null}
import requests
res = requests.get(
"https://api.zinc.com/search",
params={"q": "cast iron skillet"},
headers={"Authorization": "Bearer "},
)
res.raise_for_status()
results = res.json()["results"]
```
```javascript Node theme={null}
const res = await fetch(
"https://api.zinc.com/search?q=" + encodeURIComponent("cast iron skillet"),
{ headers: { Authorization: "Bearer " } }
);
const { results } = await res.json();
```
```json 200 OK theme={null}
{
"status": "completed",
"query": "cast iron skillet",
"results": [
{
"url": "https://www.wayfair.com/cookware/pdp/lodge-cast-iron-skillet-6-5-...",
"retailer": "wayfair",
"title": "Lodge Cast Iron Skillet 6.5\" Black/Gray, Pre-Seasoned",
"image": "https://thf.bing.com/th?id=OPHS.DO%2fNmrRBpgF0sA474C474&w=220&h=220",
"brand": null,
"price": 1890,
"stars": 4.7,
"num_reviews": 1000,
"available": null
},
{
"url": "https://www.homedepot.com/p/10-5-in-Cast-Iron-Skillet-In-Black/...",
"retailer": "homedepot",
"title": "10.5 in. Cast Iron Skillet In Black",
"image": "https://thf.bing.com/th/id/OPHS.Jv2SxDM0Y6PO8g474C474?w=220&h=220",
"brand": null,
"price": 2490,
"stars": 4.7,
"num_reviews": 1000,
"available": null
},
{
"url": "https://www.target.com/p/lodge-12-cast-iron-skillet/-/A-...",
"retailer": "target",
"title": "Lodge 12\" Cast Iron Skillet: Preseasoned, Non-Stick, Scratch-Resistant",
"image": "https://thf.bing.com/th/id/OPHS.W0Kf0gUKdW1yoA474C474?w=220&h=220",
"brand": null,
"price": 2999,
"stars": 4.7,
"num_reviews": 1000,
"available": null
},
{
"url": "https://www.amazon.com/dp/B00006JSUC",
"retailer": "amazon",
"title": "Lodge L8SK3 10.25-inch Pre-Seasoned Cast Iron Skillet",
"image": "https://m.media-amazon.com/images/I/81ysMDr2VgL._AC_UL320_.jpg",
"brand": "Lodge",
"price": 2497,
"stars": 4.8,
"num_reviews": 132840,
"available": true
}
]
}
```
```json 401 Unauthorized theme={null}
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key",
"details": null
}
}
```
```json 402 Payment Required theme={null}
{
"detail": "Insufficient wallet balance for data API call"
}
```
# Stripe Connect
Source: https://www.zinc.com/docs/v2/connect
Charge your own customer's card in real time and keep a margin, with no prepaid Zinc balance.
Most Zinc accounts pre-fund a **prepaid wallet** that each order draws down. Stripe Connect is a new way to pay that skips the balance entirely.
Instead of drawing down a wallet, Zinc places a single **shared charge** on your own end-customer's card: one charge that covers the order cost, your margin, and our fee.
You keep your margin. Zinc keeps the cost (reimbursing the purchase it fronts for you) plus our standard \$1 fee. Every order funds itself, so there's nothing to top up.
## How It Works
Because the retailer's final total isn't known until checkout, Stripe Connect places a hold when the order goes in, then captures the final amount once Zinc places the order with the retailer and releases the rest of the hold.
In your [settings](https://app.zinc.com/settings), authorize Zinc against your existing Stripe account. You keep your keys, dashboard, and payouts. Set a **statement descriptor** your end-customers will recognize before enabling charges, so they aren't surprised by an unfamiliar line item.
Add a `payment` object to your [standard create-order](/docs/v2/api-reference/orders/create-order) call, with `mode: "connect"`. Zinc authorizes a hold on your end-customer's card; if it's declined, no order is created.
Zinc places the order with the retailer, just like any other order.
Once the order is placed with the retailer, Zinc captures the actual amount and releases the rest of the hold. Your margin settles to your Stripe account; Zinc keeps the order cost (reimbursing the purchase it fronted) plus our fee.
**Example:** a \$50.00 order with a flat \$2.50 margin:
| Component | Amount |
| --------------------------- | ----------- |
| Order cost (goods) | \$50.00 |
| Your margin | \$2.50 |
| Zinc fee | \$1.00 |
| Stripe fee (2.9% + \$0.30) | \$1.91 |
| **End-customer is charged** | **\$55.41** |
| → You receive (margin) | \$2.50 |
| → Zinc keeps (cost + fee) | \$51.00 |
Connect orders are funded by the end-customer's charge, so they never touch your Zinc wallet and don't appear in your wallet balance or transaction history.
## Placing a Connect Order
Add a `payment` object to your [standard create-order](/docs/v2/api-reference/orders/create-order) call with `mode: "connect"`. Omit it (or send `mode: "wallet"`) for unchanged prepaid-wallet billing.
```bash theme={null}
curl https://api.zinc.com/orders \
-H "Authorization: Bearer zn_..." \
-H "Content-Type: application/json" \
-d '{
"products": [{"url": "https://www.amazon.com/dp/B0EXAMPLE"}],
"max_price": 6000,
"shipping_address": {
"first_name": "Jane",
"last_name": "Smith",
"address_line1": "123 Main St",
"city": "Seattle",
"state": "WA",
"postal_code": "98101",
"country": "US",
"phone_number": "2065551234"
},
"payment": {
"mode": "connect",
"payment_method": "pm_1ExampleCard",
"customer": "cus_ExampleEndCustomer",
"margin": { "type": "flat", "value": 250 }
}
}'
```
`max_price` is the most the **goods** can cost, the same ceiling used for any order; it does **not** include your margin or fees. Zinc holds that ceiling plus your margin, our \$1 fee, and Stripe's fee, then charges your customer only the actual total once the order is placed.
### The `payment` object
| Field | Required | Description |
| ---------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mode` | | `"wallet"` (default) or `"connect"`. |
| `payment_method` | ✓ | Your end-customer's saved Stripe payment-method id (`pm_…`) on your connected account. |
| `customer` | ✓ | Your end-customer's Stripe Customer id (`cus_…`) on your connected account. The card is charged off-session, so Stripe requires the Customer it's saved on. |
| `margin` | ✓ | Your markup: `{ "type": "flat", "value": }` or `{ "type": "percent", "value": }` (e.g. `15` = 15%). |
The card must be vaulted on your connected account, not on Zinc. Zinc charges it off-session via Stripe Connect; it never sees or stores raw card details.
## Order Lifecycle & Refunds
* **Order fails:** the hold is voided and your end-customer's funds are released; nothing is captured.
* **Cancelled by the retailer after capture:** Zinc refunds your end-customer in full on your connected account, including the platform fee (Zinc keeps nothing on a cancelled order).
* **Successful order:** the actual total is captured; your margin settles to your account.
Refunds are issued server-side against the original charge on your connected account and follow Stripe's standard settlement timelines.
## Next Steps
See the full create-order API spec, including the `payment` object.
Link your Stripe account in dashboard settings.
# Connect a Client
Source: https://www.zinc.com/docs/v2/mcp/connect
Add the Zinc MCP server to Claude, ChatGPT, Cursor, or VS Code and authorize it with your Zinc account.
The Zinc MCP server is remote and hosted — there is nothing to install and no API key to configure. Point your client at the URL below and sign in when it opens a browser window.
```
https://mcp.zinc.com/mcp
```
## Add the server
In Claude (web or desktop), open **Settings → Connectors → Add custom connector**, then paste the server URL:
```
https://mcp.zinc.com/mcp
```
Claude opens a Zinc sign-in window, shows you the permissions it's asking for, and finishes the connection. The Zinc tools then appear in the tools menu of any new chat.
Add it from the CLI:
```bash theme={null}
claude mcp add --transport http zinc https://mcp.zinc.com/mcp
```
Then run `/mcp` inside Claude Code and pick **Authenticate** to complete the OAuth flow in your browser.
Enable **Developer mode** under **Settings → Connectors → Advanced**, then add a connector with the URL:
```
https://mcp.zinc.com/mcp
```
ChatGPT walks you through the Zinc sign-in and consent screen. Custom MCP connectors require a paid ChatGPT plan.
Add the server through **Settings → MCP → Add new MCP server**, or edit `~/.cursor/mcp.json` directly:
```json ~/.cursor/mcp.json theme={null}
{
"mcpServers": {
"zinc": {
"url": "https://mcp.zinc.com/mcp"
}
}
}
```
Cursor prompts you to sign in the first time a Zinc tool is used.
Create `.vscode/mcp.json` in your workspace (or add the same entry to your user settings):
```json .vscode/mcp.json theme={null}
{
"servers": {
"zinc": {
"type": "http",
"url": "https://mcp.zinc.com/mcp"
}
}
}
```
Start the server from the MCP view and complete the sign-in when prompted.
Any client that speaks **Streamable HTTP** with OAuth can connect to the URL directly. Clients that only support local stdio servers can bridge with [`mcp-remote`](https://www.npmjs.com/package/mcp-remote):
```json theme={null}
{
"mcpServers": {
"zinc": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://mcp.zinc.com/mcp"]
}
}
}
```
Interactive cards render in clients that support [MCP Apps](https://modelcontextprotocol.io) widgets. In clients that don't, the same tools still work — you get the structured result as text instead of a card, and the confirmation buttons for spending are unavailable.
## Signing in
The server holds no credentials of its own. Every request carries **your** access token, which is why each connection only ever sees its own account's orders and wallet.
Authorization runs against `login.zinc.com` and the sign-in page is the same [Zinc dashboard](https://app.zinc.com) login you already use. Clients register themselves automatically, so there's no client ID or secret to create.
## Permissions
The consent screen asks for these scopes. You choose which to grant:
| Scope | What it unlocks |
| --------------- | ------------------------------------------------------------- |
| `orders:read` | List orders, order detail, tracking, and order review cards |
| `orders:write` | Placing an order you've confirmed |
| `wallet:read` | Balance, per-order fee, saved payment methods, funding checks |
| `wallet:write` | Charging a deposit you've confirmed |
| `products:read` | Cross-retailer product search (billed per search) |
`products:read` is deliberately separate from `orders:read`: search bills your wallet per call, and a search result is directly orderable, so finding a product and buying it are two different permissions. Neither implies the other.
**Scopes are fixed when you authorize.** Granting a scope later requires disconnecting and reconnecting the server — a live connection cannot widen its own permissions.
Tools degrade rather than failing blindly when a scope is missing. Without `wallet:read` the order review card simply omits its funding line; without `orders:write` the card renders but the **Place order** button stays disabled and names the scope to reconnect for.
## Verifying the connection
Start a new chat and ask:
> "List my recent Zinc orders."
A card list of your orders means the connection is live. If you get an authentication error instead, reconnect the server — the most common cause is an expired or revoked authorization.
## Disconnecting
Remove the connector in your client's settings. You can also revoke the connected app from your [Zinc dashboard](https://app.zinc.com), which invalidates its tokens immediately.
## Troubleshooting
| Symptom | Cause and fix |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------- |
| Every tool returns an authentication error | The authorization expired or was revoked. Remove and re-add the connector. |
| A tool says it lacks a scope | That scope wasn't granted at sign-in. Reconnect and approve it. |
| Results arrive as text, no cards | The client doesn't support MCP Apps widgets. Reads work; confirmation buttons don't. |
| Search fails asking for funds | Search is metered against your wallet. Confirm a top-up, or [add funds in the dashboard](/docs/v2/wallet). |
| Top-up says no payment method | Payment methods can't be added over MCP by design. Add a card in the [dashboard](https://app.zinc.com) first. |
## Next steps
Search, order, track, and top up through conversation.
# Zinc MCP Server
Source: https://www.zinc.com/docs/v2/mcp/overview
Connect Claude, ChatGPT, Cursor, or any MCP client to your Zinc account and shop through interactive cards.
The **Zinc MCP server** is a hosted [Model Context Protocol](https://modelcontextprotocol.io) endpoint that connects an AI assistant to your Zinc account. Once connected, the assistant can search products, review and place orders, follow shipments, and manage the wallet that pays for them — and every step renders as an **interactive card** inside the chat rather than as a wall of JSON.
Connect to it at:
```
https://mcp.zinc.com/mcp
```
There is no API key to paste and no code to write. You sign in with your Zinc account over OAuth, approve what the assistant may do, and the connection uses your own credentials from then on.
The MCP server talks to the same [Zinc API v2](/docs/v2/api-reference/introduction/authentication) documented here, so orders placed through it are ordinary Zinc orders — visible in the [dashboard](https://app.zinc.com), on `GET /orders`, and over [webhooks](/docs/v2/api-reference/introduction/webhooks).
## Tools
| Tool | What it does | Spends money |
| -------------------- | --------------------------------------------------------------------------------------------------------- | ----------------------------- |
| `search-products` | Finds products across Amazon, Walmart, Target, Best Buy, and more. Each result carries an orderable URL. | Billed per search — see below |
| `prepare-order` | Renders an order for you to review: items, destination, price ceiling, and whether your wallet covers it. | No |
| `place-order` | Places the order you reviewed. Only your click can trigger it. | Yes |
| `list-orders` | Lists or searches your orders as a scrollable card list; each card expands to show live tracking. | No |
| `get-order` | One order in detail: line items, retailer price breakdown, ship-to, shipment timeline. | No |
| `get-order-tracking` | Just the carrier checkpoints for an order. | No |
| `get-wallet-balance` | Balance, your per-order fee, and saved payment methods. | No |
| `prepare-topup` | Renders a deposit for you to review: amount, processing fee, total charged. | No |
| `add-funds` | Charges the deposit you reviewed. Only your click can trigger it. | Yes |
## Spending money always takes two steps
The assistant can only ever perform the *first* step of anything that moves money. `prepare-order` and `prepare-topup` draw a card; `place-order` and `add-funds` require a confirmation token that the MCP runtime hands to the **card** and withholds from the model. Only a human clicking the button inside that card can produce it.
This is what makes the confirmation card real rather than decorative. Without it, a model could call the spending tool on its own — and a prompt injection hidden in any web page it happened to read could become a purchase.
Each confirmation is bound to the exact order and account it was issued for, expires after 10 minutes, and carries an idempotency key. Clicking twice, reloading the card, or a client retrying a request that already succeeded cannot produce a second order or a second charge.
Confirming an order spends real money from your Zinc wallet, and confirming a top-up charges your saved payment method. Everything else — searching aside — only reads.
## What search costs
`search-products` is the one tool whose *use* is billed: Zinc meters cross-retailer search against your wallet at the standard data-call fee of **\$0.01 per search**. Two consequences:
* It is deliberately **not** marked read-only, so clients that auto-run safe tools will still ask you first.
* If your balance can't cover a search, the tool says so and offers a top-up instead of retrying — a retry loop on a metered endpoint bills every attempt.
All the other read tools (`list-orders`, `get-order`, `get-order-tracking`, `get-wallet-balance`) are free.
## MCP server or Agent Skill?
Both let an agent shop through Zinc. They suit different setups:
| | **MCP server** | **[Universal Checkout Skill](/docs/v2/agent-skills/overview)** |
| ------------ | -------------------------------------- | ----------------------------------------------------------------- |
| Setup | Paste one URL, sign in with OAuth | Install a `SKILL.md`, set `ZINC_API_KEY` or an MPP payment method |
| Credentials | Your Zinc login, scoped by consent | An API key you manage, or pay-per-request via MPP |
| Interface | Interactive cards you click to confirm | Natural language; the agent asks before spending |
| Zinc account | Required | Not required on the MPP rail |
| Best for | Chat clients — Claude, ChatGPT, Cursor | Coding agents and headless automation |
If you're in a chat client and have a Zinc account, start with the MCP server. If you want an agent to buy with no Zinc account at all, use the skill on [MPP](/docs/v2/mpp).
## Prerequisites
* A **Zinc account** ([sign up at app.zinc.com](https://app.zinc.com)) with funds in your [wallet](/docs/v2/wallet).
* An **MCP client that supports remote servers with OAuth** — Claude, ChatGPT, Cursor, VS Code, and others.
## Next steps
Add the server in Claude, ChatGPT, Cursor, or VS Code.
Search, order, track, and top up through conversation.
# Using the MCP Server
Source: https://www.zinc.com/docs/v2/mcp/usage
Search products, place and track orders, and manage your wallet through an AI assistant connected to Zinc over MCP.
Once the server is [connected](/docs/v2/mcp/connect), you drive the whole shopping lifecycle in conversation. This guide covers the workflows and how to read what comes back.
## Finding a product
If you don't have a product URL, ask for one:
> "Find me a stainless steel French press under \$40."
> "Search for a USB-C hub with at least three ports."
The results card lists products across Amazon, Walmart, Target, Best Buy, and more, ranked by rating and review volume. Every result carries an **orderable URL** — pick one and keep going in the same conversation.
Search is metered against your wallet at **\$0.01 per search**. The assistant is instructed to search once with your best query rather than fanning out over variations, and to tell you rather than silently retry if a search fails for want of funds.
Already have a URL? Skip search entirely and paste it — that path is free.
## Placing an order
To prepare an order the assistant needs three things:
* A **product URL** from a supported retailer
* A **shipping address**
* A **maximum price** — the ceiling on the total, **including tax and shipping**
> "Buy this: `https://www.amazon.com/dp/B0EXAMPLE` — ship to Jane Doe, 123 Main St, San Francisco, CA 94105. Don't spend more than \$50."
A review card appears with the items, the destination, your price ceiling, and — if you granted `wallet:read` — whether your balance covers it. **Nothing has been spent at this point.** The order is placed only when you click **Place order** in that card.
`max_price` is the ceiling on the **total**, not the item price. Below a retailer's free-shipping threshold, shipping is added on top, so leave room for it or the order trips `max_price_exceeded`.
The assistant cannot click the button for you and cannot place the order on its own — see [how confirmation works](/docs/v2/mcp/overview#spending-money-always-takes-two-steps).
### Variants, quantity, and condition
* **Variants** — "Order the Nike Dri-FIT tee in Large, blue."
* **Multiple items** — "Order two packs of AA batteries and one USB-C cable together, shipped to…"
* **Trial runs** — a max price of \$0.00 asks the retailer to validate the order without placing it. Nothing is charged. The result is recorded as `order_failed` with `error_type: max_price_exceeded`, which is the expected outcome, not a problem.
## Checking status and tracking
Orders are asynchronous and usually take 5–10 minutes to reach the retailer.
> "What's the status of my last order?"
> "Where's order ``? Any tracking yet?"
The order list renders as cards you can expand in place for live carrier checkpoints; `get-order` gives one order in full detail, with the retailer's price breakdown and the shipment timeline.
| Status | Meaning |
| ----------------------- | ------------------------------ |
| `pending` | Received, not yet processing |
| `in_progress` | Being placed with the retailer |
| `order_placed` | Placed successfully |
| `order_failed` | Could not be completed |
| `cancelled` | Cancelled by you |
| `cancelled_by_retailer` | Cancelled by the retailer |
`order_placed` is a terminal **order** status, not a delivery signal. Whether a package has shipped or arrived lives on the tracking rows, so ask for tracking rather than reading the order status.
## Managing your wallet
> "What's my Zinc balance?"
The balance card shows your balance, your **spendable** balance, your per-order fee, and your saved payment methods.
Spendable is the number that matters. It sits below your raw balance while [Stripe Connect](/docs/v2/connect) holds are in flight, and it's what an order is checked against.
To add funds:
> "Add \$50 to my Zinc wallet."
A deposit card shows the amount credited, the processing fee for that payment method, the total you'll be charged, and your projected balance. As with orders, nothing is charged until you click **Add funds**.
* The self-serve **minimum deposit is \$15.00**, so a smaller shortfall still needs a \$15.00 top-up.
* Card deposits credit immediately; **bank transfers take a few business days** to settle, and the card tells you which applies.
* **Adding a new payment method isn't possible over MCP** — by design, those routes are closed to MCP connections. Add a card in the [dashboard](https://app.zinc.com) first.
See [Wallet](/docs/v2/wallet) for the full fee schedule.
## Reading the cards
A few things about Zinc's data model that the cards surface honestly rather than guessing at:
* **Money is exact.** Every amount is a cent-precise value with a preformatted display string; nothing is rounded on the way to you.
* **There are no product titles.** Zinc stores the retailer URL, not a product name, so line items are identified by URL. A title derived from the URL path was a guess and often wrong.
* **The retailer is inferred** from those URLs — Zinc has no retailer field.
* **A price breakdown appears only once the retailer order is placed.** Pending and failed orders have no total.
* **Order lists omit totals.** The list endpoint doesn't return them; ask for a specific order to see what was actually charged.
## When something goes wrong
Order placement is the one moment where the failure matters, so it's reported precisely:
| What you see | What it means |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| "Already placed" | Your confirmation was submitted twice. The order exists, nothing was charged again — ask for your order list to see it. |
| "Outcome unknown — check your orders" | The request timed out or the API errored mid-flight. It may well have succeeded. **Check your order list before trying again.** |
| A specific error | The API's own reason: insufficient funds, a missing scope, a rejected address. |
If an outcome is uncertain, re-submitting the **same** confirmation card is always safe — it carries the same idempotency key and cannot create a second order. Running a **new** `prepare-order` is not: a new confirmation is a new order.
Common order failures are the same ones the API reports — `max_price_exceeded`, `product_out_of_stock`, `invalid_shipping_address`, `product_variant_required`, `retailer_unavailable`. See [Error Handling](/docs/v2/api-reference/introduction/error-handling) for the full list.
## Not available over MCP
Some Zinc operations aren't exposed as MCP tools yet. Use the [dashboard](https://app.zinc.com) or the API for:
* [Cancelling an order](/docs/v2/api-reference/orders/cancel-order)
* [Returns](/docs/v2/api-reference/returns/create-return)
* [Bulk order uploads](/docs/v2/api-reference/orders/create-bulk-upload)
* [Managed accounts](/docs/v2/api-reference/managed-accounts/list-managed-accounts)
* Adding or removing payment methods
## Tips
* **Set the max price to cover the full cost** — item plus tax plus shipping.
* **Paste a product URL when you have one.** Search costs money; ordering from a URL doesn't.
* **Use direct product URLs**, not category or search-result pages.
* **Reading is free and safe.** Order lists, detail, tracking, and balance never spend anything.
* **Only your click spends money.** If no card with a button appeared, nothing was bought.
# Migrating from V1
Source: https://www.zinc.com/docs/v2/migrating-from-v1
Step-by-step guide to migrating your integration from the Zinc API v1 to v2.
Interested in migrating to v2? We’re happy to help. Reach out to [support@zinc.com](mailto:support@zinc.com) and we’ll work with you to make the transition smooth.
Zinc v2 has reached full feature parity with v1. Returns, cancellations, and product data are all available in v2, along with significantly expanded retailer coverage and a modern API. If you’re still on v1, now’s the time to switch.
**Is Zinc 1.0 going away?**
Not immediately. Zinc 1.0 (dash.zinc.io) is staying live for now, but all new development is happening on v2. We recommend migrating sooner rather than later.
**What’s different about 2.0?**
Zinc 2.0 is a completely new system: a new dashboard (app.zinc.com), new API, and new account. We rebuilt everything leveraging AI, which lets us support way more retailers than before.
**Can I use my existing account/API keys?**
No. 2.0 requires a new account and new API keys. Your 1.0 credentials won’t carry over. It’s super-easy to get started.
**How is pricing different?**
2.0 combines Zinc Managed Accounts funding and API fees into one system. You add funds to your wallet and everything is handled from there. You’ll be able to track every transaction and fee just like before.
**What features does 2.0 have that 1.0 doesn’t?**
More retailers, [Machine Payments Protocol](/docs/v2/mpp) support, [product data endpoints](/docs/v2/api-reference/products/search), and more flexibility in what you can do on those retailers (e.g. picking different delivery options).
**Should I switch to 2.0?**
Yes. v2 now has everything v1 has — including [returns](/docs/v2/api-reference/returns/create-return), cancellations, and product data — plus significantly more retailer coverage. The sooner you move to v2, the more we’ll prioritize your specific needs.
**I have more questions.**
[Email us](mailto:support@zinc.com) or [book time with Ian directly](https://cal.com/zinc-ian/15min)
## Ready to port your code?
Endpoint-by-endpoint mapping, request and response shape diffs, webhook consolidation, and validation steps. Read this if you're porting by hand.
Workflow for Claude Code (or any coding agent) to execute the port directly against your integration repository — discovery, auth swap, endpoint port, webhook consolidation, validation.
# V1 → V2 migration: agent runbook
Source: https://www.zinc.com/docs/v2/migration-agent-runbook
Workflow for Claude Code (or any coding agent) to execute the v1 → v2 port directly against your integration repository.
This page is the **agent prompt** that complements the [V1 → V2 migration reference](/docs/v2/migration-reference). Drop a coding agent into your integration repo, point it at this URL, and the agent ports your code phase-by-phase with human checkpoints. Want the human-readable port reference instead? [Start here](/docs/v2/migration-reference).
## Invoking the agent
[Claude Code](https://claude.ai/code) is the reference implementation. From your integration repo:
```bash theme={null}
cd your-zinc-integration-repo
claude
# Then in the Claude prompt:
> Read https://docs.zinc.com/v2/migration-agent-runbook and execute the v1 → v2 migration on this repository.
```
Other agents work too — the runbook is plain text and language-agnostic. Just paste the full content of this page as the prompt.
***
## Prime directives
1. **Never silently drop functionality.** Anything without a 1-to-1 v2 mapping gets a flag in `MIGRATION_GAPS.md` and a `TODO(zinc-migration)` comment at the call site.
2. **Commit per phase.** Each numbered phase below is a separate commit, so the human can review small diffs.
3. **Surface uncertainty.** If you can't tell whether a string is a product ID or a URL, whether `max_price` is in cents or dollars, whether a status branch should map to `order_placed` or `in_progress` — flag it instead of guessing.
4. **Do not invent credentials or secrets.** API keys, webhook secrets, retailer credentials, and the v2 base URL come from the human via env vars or the dashboard.
5. **The live docs are the source of truth.** Where this runbook and the live docs disagree, the live docs win — v2 evolves and your training data may be stale.
***
## Webhook Wire Reference
v2 delivers all events to a **single webhook URL** configured per account in the
dashboard. The agent must not invent this URL, the secret, or the header names —
they are fixed by the API and reproduced here so Phase 6 does not depend on
fetching live docs.
### Delivery
* Transport: HTTP `POST` to the account's configured `webhook_url`.
* `Content-Type: application/json`
* Headers on every delivery:
* `X-Webhook-Event` — the event name (e.g. `order.failed`)
* `X-Webhook-Signature` — hex-encoded HMAC-SHA256 of the **raw request body**
### Signature verification
The signature is `HMAC-SHA256(secret, raw_body)`, hex-encoded. The secret is the
account's **webhook signing secret** (prefix `zn_whsec_`), obtained from the
dashboard — never generated by the agent. Compute the HMAC over the exact bytes
received, before any JSON re-serialization, and compare timing-safely.
```python theme={null}
import hmac, hashlib
def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = hmac.new(
secret.encode("utf-8"), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature_header)
```
### Payload envelope
```json theme={null}
{
"event": "order.failed",
"order_id": "",
"return_id": null,
"status": "",
"timestamp": "",
"data": {}
}
```
* `return_id` is populated **only** on `return.*` events; it is `null` for order events.
* `status` is the order status for `order.*` events and the return status for `return.*` events.
* `data` carries event-specific fields. For `order.failed` it includes `error_type` and `error`.
### Complete event list
Order lifecycle:
`order.started`, `order.placed`, `order.failed`, `order.tracking_received`,
`order.delivered`, `order.cancelled`
Return lifecycle:
`return.created`, `return.approved`, `return.denied`, `return.credited`
When mapping v1 event names in Phase 6, map only to names in this list. Any v1
event with no equivalent here goes to `MIGRATION_GAPS.md` rather than a guessed name.
***
## Phase 0 — Ground in the current docs
Before writing any code, fetch the live v2 documentation. Do not rely on what you remember about the Zinc API; v2 ships changes frequently.
Fetch at minimum:
* `https://www.zinc.com/docs/llms.txt` — the full documentation index
* `https://www.zinc.com/docs/versions/latest.json` — the OpenAPI spec, **the source of truth for field names and types**
* This page and the [migration reference](/docs/v2/migration-reference)
* Endpoint pages for any endpoints this codebase uses. At minimum:
* `https://www.zinc.com/docs/v2/api-reference/introduction/authentication.md`
* `https://www.zinc.com/docs/v2/api-reference/orders/create-order.md`
* `https://www.zinc.com/docs/v2/api-reference/orders/get-order.md`
* `https://www.zinc.com/docs/v2/api-reference/orders/cancel-order.md`
* `https://www.zinc.com/docs/v2/api-reference/introduction/webhooks.md`
* `https://www.zinc.com/docs/v2/api-reference/introduction/error-handling.md`
* `https://www.zinc.com/docs/v2/api-reference/introduction/idempotency.md`
* `https://www.zinc.com/docs/v2/api-reference/introduction/sandbox.md`
* `https://www.zinc.com/docs/v2/api-reference/returns/create-return.md` (plus get-return, list-returns)
* `https://www.zinc.com/docs/v2/api-reference/products/get-product.md`, `get-product-offers.md`, `search.md`
If the codebase uses managed accounts, MPP, or cross-retailer search, read those pages too — they're linked from the index.
***
## Phase 1 — Repo setup
Read-only confirmation. Stop and ask the human if any of these are unclear:
* Identify the repo's language(s) (Python / Node / Ruby / Go / PHP / etc.) and HTTP client patterns (`requests`, `axios`, `fetch`, raw `curl`, an SDK).
* Identify the existing test harness — you'll run it as validation in Phase 8.
* Create a new branch `zinc-v2-migration`.
***
## Phase 2 — Discovery → `MIGRATION_INVENTORY.md`
Search the repo for every v1 touchpoint. Inventory includes code paths, config, **and persistence layer** — not just HTTP call sites.
**Code patterns:**
* URL strings containing `api.zinc.io` (any path)
* URL strings containing `/v0/`, `/v1/`, or `/v2/` near an HTTP call
* Headers / variables named `client_token`, `ZINC_CLIENT_TOKEN`, `zinc_token`, `Authorization: Basic …`
* Imports of any v1 SDK: `zinc`, `@zinc/api`, `zinc-api`, `zincapi`, etc.
* `request_id` polling loops — v1 returned `{"request_id": "..."}` from order creation and required polling `GET /v1/orders/{request_id}`; while in flight, that endpoint returned the `request_processing` error code rather than a real status
* Webhook handler routes whose payloads or paths reference v1 event names: `order_placed`, `order_failed`, `tracking_obtained`, `tracking_updated`, `status_updated`, `case_updated`, `return_placed`, `return_failed`, `request_succeeded`, `request_failed`
**v1 request fields to grep for** (many have no v2 equivalent and need flagging):
`retailer`, `product_id`, `seller_selection_criteria`, `shipping_method`, `retailer_credentials`, `payment_method`, `billing_address`, `is_gift`, `gift_message`, `client_notes`, `addax`, `webhooks` (v1 accepted per-request webhook URLs in the order body), `max_price`
`is_gift` and `gift_message` do carry over to v2 as pass-throughs — don't flag them as gaps — but `is_gift` is **stricter**: it now fails the order with `gift_option_unavailable` where the retailer has no free gift option. Flag call sites that set it opportunistically. See [Gift orders are stricter in v2](/docs/v2/migration-reference#gift-orders-are-stricter-in-v2).
**v1 error / status codes to grep for:**
`_type: "error"`, `request_processing`, `aborted_request`, `invalid_login`, `max_price_exceeded`
**Persistence:**
* Database columns or models storing `request_id`, v1 status strings (e.g. `pending`, `submitted`, `placed`), or v1 error codes — these may need a schema migration to hold v2's UUID order IDs and the new status enum.
* Migration shape: keep v1 columns readable during the transition window (existing in-flight v1 orders shouldn't break); add v2 columns alongside.
**Config / secrets:**
* Environment variables, `.env*` files, secret-manager templates, CI configs holding Zinc client tokens.
**Configuration:**
* Any per-environment config (Stripe-style keys, Slack channels, etc.) that points to Zinc — note which environments exist so the agent doesn't accidentally mix test and prod keys later.
Write `MIGRATION_INVENTORY.md` at the repo root:
```markdown theme={null}
# Zinc v1 → v2 Migration Inventory
Generated by Claude Code on . Do not edit by hand — re-run
discovery to refresh.
## API call sites
| File | Line | v1 endpoint | Phase | Status |
|------|------|-------------|-------|--------|
| src/orders.py | 42 | POST /v1/orders | 4 | pending |
| src/returns.py | 17 | POST /v1/returns | 4 | pending |
## Webhook handlers
| File | Line | v1 event(s) handled | Status |
|------|------|---------------------|--------|
| src/webhooks/order_placed.py | 5 | order_placed | pending |
## v1 credentials in code (potential leak — review)
| File | Line | Pattern matched |
|------|------|-----------------|
| config/.env.example | 3 | ZINC_CLIENT_TOKEN=… |
```
**Commit:** `chore(zinc): inventory v1 call sites for v2 migration`.
If the inventory contains zero rows, **stop** — there are no v1 calls to migrate; surface that to the human.
***
## Phase 3 — Auth and base URL
One commit, repo-wide:
Store the key in the customer's environment under a local name such as `ZINC_API_KEY` (the `zn_live_*` / `zn_test_*` bearer token) and add `ZINC_BASE_URL` (the v2 production host; default to a placeholder and flag for human confirmation in `MIGRATION_PREREQS.md`). These env var names are a convention for the customer's repo, not values the API defines. The key itself is **minted in the dashboard**, never invented by the agent.
Strip `ZINC_CLIENT_TOKEN` and any basic-auth password vars from `.env.example`, `config.{py,js,...}`, CI configs, and secret-manager templates. Add a deprecation comment.
`Authorization: Bearer ${ZINC_API_KEY}` instead of `Authorization: Basic `.
Drop `/v0`, `/v1`, `/v2` — v2 has no version prefix.
**Do not** rewrite request bodies in this phase. Endpoint paths beyond the prefix change still match in URL structure for most resources; bodies move in Phase 5.
**Two keys, never mixed.** Sandbox and production share the **same base URL** — there is no separate sandbox host. Provision a `zn_test_*` key for dev / staging / CI and a `zn_live_*` key for production only; the key prefix alone selects the environment (`zn_test_*` routes to the sandbox, `zn_live_*` to production). Add a config-time assertion that prevents `zn_live_*` from ever loading in non-prod environments, and vice-versa — and use timing-safe comparison for webhook signatures.
**Commit:** `feat(zinc): switch to v2 bearer auth and base URL`.
***
## Phase 4 — Pre-flight checklist → `MIGRATION_PREREQS.md`
Write `MIGRATION_PREREQS.md` with the human-required steps from the [pre-flight checklist](/docs/v2/migration-reference#pre-flight-checklist) in the reference. Tailor it to what discovery found.
```markdown theme={null}
# Zinc v2 Migration — Manual Pre-flight
The agent has scaffolded the code migration. Before any v2 calls will
succeed, you (a human) need to:
- [ ] Sign in to the v2 dashboard at https://app.zinc.com and create
an account via Stytch magic-link.
- [ ] Confirm the v2 production base URL and set ZINC_BASE_URL.
- [ ] Mint a `zn_live_*` API key and set ZINC_API_KEY.
- [ ] Mint a `zn_test_*` API key for CI and set ZINC_TEST_API_KEY.
- [ ] Top up your v2 wallet. The agent counted N order placements in
your repo; estimate budget accordingly.
- [ ] Register your webhook URL via POST /users. The agent identified
these existing v1 webhook URLs — pick one or consolidate:
- https://yours.example.com/wh/placed
- https://yours.example.com/wh/tracking
- [ ] Copy the account's webhook signing secret (prefix `zn_whsec_`)
from the dashboard into the customer's environment (e.g.
ZINC_WEBHOOK_SECRET). This is the HMAC key for verifying
X-Webhook-Signature. It is account-scoped and dashboard-issued —
the agent must not generate it.
- [ ] Create managed accounts for these (retailer, email) pairs the
agent found in your code:
- amazon / buyer1@example.com
- amazon / buyer2@example.com
- walmart / buyer1@example.com
- [ ] Record the returned `short_id` for each; set them in config
(the agent left placeholders: ZINC_MANAGED_ACCOUNT_AMAZON_BUYER1, …).
```
**Commit:** `docs(zinc): manual pre-flight checklist for v2 migration`.
***
## Phase 5 — Endpoint-by-endpoint port
For each row in `MIGRATION_INVENTORY.md`, apply the transformation from the [endpoint mapping](/docs/v2/migration-reference#endpoint-mapping) and [request shape diffs](/docs/v2/migration-reference#request-shape-diffs) in the reference. Group by resource and commit per resource:
* `feat(zinc): port order placement to v2 /orders`
* `feat(zinc): port returns to v2 /returns`
* `feat(zinc): port retailer credentials to managed-accounts model`
* `feat(zinc): port tracking reads to embedded order tracking`
### Per-call-site checklist
For each call site:
Apply the mapping from the [reference's endpoint mapping](/docs/v2/migration-reference#endpoint-mapping). If the mapping is *no equivalent*:
* Leave the calling code intact.
* Add `# TODO(zinc-migration): no v2 equivalent for {endpoint} — see MIGRATION_GAPS.md`.
* Append a row to `MIGRATION_GAPS.md`. Continue.
Apply the field-by-field rules from [Request shape diffs](/docs/v2/migration-reference#request-shape-diffs).
Apply [Response shape diffs](/docs/v2/migration-reference#response-shape-diffs) everywhere downstream code reads the response. Search the codebase for accesses to `request_id`, `merchant_order_ids`, `_status`, `price_components`, etc., and rewrite.
If you find `if status == "placed"` or similar, rewrite to the v2 enum value (`"order_placed"`). Centralise into a helper if there are >3 sites.
Set status to `done` and link the commit SHA.
### Hard rules
If you find a literal email/password/TOTP in the codebase that was previously passed to v1's `retailer_credentials`, replace it with a config reference (e.g. `config.ZINC_MANAGED_ACCOUNT_`) and add a row to `MIGRATION_PREREQS.md`. Do not move secrets between files.
v2 requires integer cents. If the customer's code multiplies or divides by 100 before sending to v1, that math probably stays. If it doesn't, flag the call site for the human to confirm.
If a `product_id` is being used and you can't confidently identify the retailer (e.g. from a sibling `retailer: "amazon"` argument), flag it.
`idempotency_key` is still supported on v2 `/orders`; pass it through. Drop it for endpoints where v1 had it and v2 doesn't (returns).
***
## Phase 6 — Webhook handlers
Typically one route per event type.
Match the existing URL the customer is most likely to register in v2 (the most common destination in the v1 `webhooks` objects).
Implement signature verification per the [Webhook Wire Reference](#webhook-wire-reference) above. Use `ZINC_WEBHOOK_SECRET` as the HMAC key, compute over the raw request body, and reject with 401 if the `X-Webhook-Signature` header doesn't match. Use `hmac.compare_digest` (or the language equivalent) to avoid timing leaks.
Use the [event name mapping](/docs/v2/migration-reference#event-name-mapping). If the customer's handler relied on `status_updated` / `case_updated` / `request_succeeded` / `request_failed` / `return_failed`, add `TODO(zinc-migration)` + a `MIGRATION_GAPS.md` row.
Now merged into the dispatcher.
**Commit:** `feat(zinc): consolidate webhook handlers for v2 single-URL model`.
***
## Phase 7 — Gaps review → `MIGRATION_GAPS.md`
By the end of Phase 6 you should have a populated `MIGRATION_GAPS.md`. Ensure each row has:
* File and line of the original v1 call
* The v1 endpoint or event involved
* A one-sentence description of what the customer was doing with it
* Your **suggested workaround** if any (e.g., "store `merchant_order_id` locally and look it up; v2 does not expose `insert_merchant_order_id`"), or `none — discuss with Zinc` if you have no good suggestion
**Commit:** `docs(zinc): document migration gaps for human review`.
Stop the migration here and surface `MIGRATION_GAPS.md` to the human. Do not proceed to Phase 8 until the human has acknowledged the gaps file — otherwise validation will run against a half-migrated integration and lie about success.
***
## Phase 8 — Validation
Drive scenarios with the **dedicated test product URLs** rather than placing real orders. Fetch the live catalog from `GET /orders/test-products` and exercise at minimum:
| Test product | What it exercises |
| ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `https://zinc.com/shop/products/test-success` | Full happy path through `order_placed`, tracking numbers, and price components |
| `https://zinc.com/shop/products/test-invalid-address` | **Synchronous** failure — rejected at order-creation time |
| `https://zinc.com/shop/products/test-url-unreachable` | Synchronous failure |
| `https://zinc.com/shop/products/test-insufficient-funds` | Synchronous failure |
| `https://zinc.com/shop/products/test-out-of-stock` | **Asynchronous** failure — order creates successfully, then fails during processing and arrives as an `order.failed` webhook |
| `https://zinc.com/shop/products/test-price-exceeded` | Asynchronous failure |
| `https://zinc.com/shop/products/test-invalid-variant` | Asynchronous failure |
| `https://zinc.com/shop/products/test-shipping-unavailable` | Asynchronous failure |
**The sync-vs-async failure split is the most common code path you'll get wrong.** Some errors are returned by the API immediately (4xx with a code/message); others come back asynchronously as `order.failed` webhooks after the order was successfully created. The application must handle both — code that only inspects the create-order response will silently drop the async failures.
All checkboxes in `MIGRATION_PREREQS.md` are ticked, env vars are set in `.env.test` or equivalent.
Use `ZINC_TEST_API_KEY` and `ZINC_BASE_URL`. Sandbox is the same base URL as production — the `zn_test_*` key prefix alone determines test mode.
Create → receive webhooks → tracking on `test-success` → every synchronous and asynchronous failure scenario → (if used) cancel and return flows. Confirm webhook signature verification passes on real sandbox deliveries, not just unit-test fixtures.
Confirm signature verification passes and event names map correctly.
Summary including:
* Number of call sites migrated
* Number of gap rows requiring human decisions
* Test suite pass/fail counts
* Outstanding TODOs
**Sandbox limitations** — test mode skips wallet balance, URL reachability, and address validation checks so you can test without funding real wallets or hitting real retailers. Plan a small set of real production orders as the final cutover check before declaring the migration done.
**Commit:** `docs(zinc): migration report`.
***
## What to do when stuck
Trust the reference (it's generated against v2's live OpenAPI schema). File an issue against the reference page with the specific discrepancy.
Add a row to `MIGRATION_GAPS.md` with `endpoint not documented in migration guide`. Do not guess.
Consider whether the customer has an internal SDK wrapper you can edit once instead of N times. Look in `lib/`, `clients/`, `services/zinc*` before mass-editing.
Stop and report — don't try to fix unrelated test infrastructure.
***
## Output files summary
By the end of the migration, the customer's repo has these new top-level files:
| File | Purpose |
| ------------------------ | ---------------------------------------------------------- |
| `MIGRATION_INVENTORY.md` | Every v1 call site, status per phase |
| `MIGRATION_PREREQS.md` | Human checklist for dashboard / wallet / credentials setup |
| `MIGRATION_GAPS.md` | Endpoints with no v2 equivalent + suggested workarounds |
| `MIGRATION_REPORT.md` | Final summary |
And the code is ported on a branch named `zinc-v2-migration`, ready for the customer's normal PR review process.
# V1 → V2 migration reference
Source: https://www.zinc.com/docs/v2/migration-reference
Endpoint-by-endpoint mapping, request and response shape diffs, webhook consolidation, and validation steps for porting an existing Zinc v1 integration to Zinc API v2.
Looking for the high-level "why" and "should I migrate"? Start with the
[V1 → V2 overview](/docs/v2/migrating-from-v1). This page is the **technical
detail** — port your integration code with confidence.
The migration is mechanical for \~80% of call sites; the remainder need human review because v2 drops or restructures a few v1 features. This page walks through each transformation. Most teams complete the port in a day or two.
If you'd rather have an AI agent do the mechanical edits for you, see the [agent runbook](/docs/v2/migration-agent-runbook) — it's a workflow for Claude Code (or any coding agent) to execute this guide against your integration codebase.
**Scope.** This page covers porting your **integration code**. It does not move historical orders, retailer credentials, or accounts from v1 to v2. Customers start v2 with a fresh account, API key, wallet, and managed accounts.
## What changed at a glance
| Area | v1 | v2 |
| -------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| Auth | HTTP Basic `client_token:password?` | `Authorization: Bearer zn_live_…` |
| Base URL | `https://api.zinc.io/v0` / `/v1` / `/v2` | New v2 host (published in the dashboard); **no version prefix in path** |
| Payment | Per-request `payment_method` block | Wallet-based — top up via Stripe, orders charge wallet automatically |
| Retailer credentials | Embedded in every order request | First-class `managed_accounts` objects; orders reference a `retailer_credentials_id` |
| Webhooks | Per-request `webhooks` object, 10 event types, URLs per event | One `webhook_url` per user, dotted event names, HMAC-SHA256 signature, `X-Webhook-Event` header |
| Idempotency | `idempotency_key` in request body | Still supported — `idempotency_key` (max 36 chars) on `POST /orders` |
| Order ID shape | Opaque `request_id` string | UUID |
| Tracking endpoint | `/v1/trackings` (separate async resource) | No dedicated endpoint — tracking embedded in `GET /orders/{id}` |
| Cancellations | `/v1/cancellations` + `/v1/orders/:id/cancel` + `/v1/orders/:id/abort` | Merged into `POST /orders/{id}/cancel` (pending orders only — post-placement, file a return) |
| Returns | Free-text `reason_code` string | Strict `reason` enum (10 values) |
***
## Behavioral changes worth knowing
A few v2 changes aren't pure renames — they narrow or restructure semantics in ways that bite if you treat the migration as a find-and-replace.
### Cancellation is narrower in v2
v1 *attempted* cancellation even after the order had been placed at the retailer. v2 only allows cancellation while the order is still **pending in the queue**:
* `POST /orders/{order_id}/cancel` returns `204` and works only while `status == "pending"`.
* Once the order is `in_progress` or `order_placed`, the endpoint returns `order_not_cancellable` and the cancellation path is closed.
* **Post-placement cancellations now happen retailer-side** and arrive as an `order.cancelled` webhook with an automatic wallet refund. Your app needs to handle that arrival path.
* For everything else (customer changed their mind after the retailer placed the order, item arrived wrong, etc.) the path is a [return](#returns), not a cancel.
Any v1 logic that tried to cancel placed or in-flight orders must be redesigned.
### Returns require persisted `order_item_id` UUIDs
v2 returns reference individual order items by UUID — not by `product_id` like v1. Those UUIDs come back in the `items[]` array of the `POST /orders` response, and there's no way to recover them later other than calling [`GET /orders/{order_id}`](/docs/v2/api-reference/orders/get-order).
If your application doesn't already persist per-item identifiers from the order response, **plan a schema migration**: add a column for the v2 `order_item_id` next to whatever you already store per line item.
### Gift orders are stricter in v2
Both `is_gift` and `gift_message` carry over from v1 unchanged, but v2 treats a gift request as a hard requirement rather than a preference. If the retailer's checkout offers no free gift option, the order **fails** with `gift_option_unavailable` instead of being placed as a normal order — on the reasoning that a gift arriving with prices visible to the recipient is worse than no order.
v1 code that set `is_gift` opportunistically (on the assumption it degrades silently) will now see failures on retailers without gift support. If gifting is an optional add-on in your flow, catch `gift_option_unavailable` and retry without `is_gift`.
`gift_message` keeps the lenient behavior: where a retailer has no gift-message field, the order is still placed without the message.
### `return.credited` is a new outcome with no v1 equivalent
When Zinc can't get a merchant RMA from the retailer (lost in the mail, retailer refuses, etc.) but the customer is owed a refund, v2 issues a wallet credit and the return resolves with `status = credited` (plus a `return.credited` webhook). v1 had no equivalent code path. Make sure your return-resolution code handles `credited` alongside `approved` / `denied`.
### Sync vs. async failures
Order failures in v2 surface in two distinct places:
* **Synchronous (immediate)** — failures detected at order-creation time return a 4xx from `POST /orders` with a `code` and `message`. Examples: `invalid_shipping_address`, `url_unreachable`, `insufficient_funds`, `unsupported_retailer`. Your create-order code path handles these.
* **Asynchronous (later)** — failures detected during processing arrive as `order.failed` webhooks with an `error_type` field. Examples: `product_out_of_stock`, `max_price_exceeded`, `invalid_variant`, `shipping_unavailable`, `checkout_blocked`. Your webhook handler handles these.
Code that only inspects the create-order response silently drops the async failures. Both code paths are required.
### Product data covers a narrower retailer set
v2's product endpoints (`/products/{id}`, `/products/{id}/offers`, `/products/search`) currently support a narrower retailer set than ordering does — the spec enumerates `amazon` and `walmart`. If your v1 integration fetches product data for other retailers, flag those call sites — they may need to drop to a different data source or be removed.
The new endpoints also gain freshness controls (`max_age` / `newer_than`, mutually exclusive) and an `async` mode that returns immediately with `status=processing`. Decide whether your existing call sites want the fresh-blocking default or the async fast-return.
***
## Authentication
```http v1 theme={null}
POST /v1/orders HTTP/1.1
Host: api.zinc.io
Authorization: Basic
Content-Type: application/json
```
```http v2 theme={null}
POST /orders HTTP/1.1
Host:
Authorization: Bearer zn_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
```
### How to get a v2 API key
Use Stytch magic-link via [app.zinc.com](https://app.zinc.com).
Create a `zn_live_*` key for production and a `zn_test_*` key for
sandbox testing — either from the dashboard UI or via
`POST /api-keys`.
Use your existing config key (e.g. `ZINC_API_KEY`) plus a new
`ZINC_BASE_URL` if you don't already have one.
***
## Pre-flight checklist
Before any code change, complete these manual steps in the v2 dashboard. Code-level edits can't do them for you:
Visit [app.zinc.com](https://app.zinc.com) and authenticate with the email you want to associate with your v2 account.
Use the dashboard or `POST /api-keys`. Test mode is per-key, not per-host — the same base URL works for both.
v2 charges per order from your wallet balance (not a credit card on file). Top up via the dashboard's Wallet page or `POST /wallet/top-up` / `POST /wallet/checkout`. See the [Wallet guide](/docs/v2/wallet) for the full payment model.
Set `webhook_url` via `POST /users`, then mint a webhook secret via `POST /users/webhook-secret`. Save the secret — it's shown once. See the [Webhooks introduction](/docs/v2/api-reference/introduction/webhooks) for payload + signature details.
For every retailer login your v1 integration uses, call [POST /managed-accounts](/docs/v2/api-reference/managed-accounts/create-managed-account) and record the returned `short_id` (format `zn_acct_XXXXXXXX`). Store these in your config — orders reference them by `short_id`.
***
## Endpoint mapping
### Orders
| v1 | v2 | Notes |
| ------------------------------------------------------ | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------- |
| `POST /v1/orders` | [`POST /orders`](/docs/v2/api-reference/orders/create-order) | Request body restructured — see [Request shape diffs](#post-orders) |
| `POST /v0/order` | [`POST /orders`](/docs/v2/api-reference/orders/create-order) | Same as `/v1/orders` |
| `GET /v1/orders/{request_id}` | [`GET /orders/{order_id}`](/docs/v2/api-reference/orders/get-order) | Response shape changes — see [Response shape diffs](#order-response) |
| `GET /v1/orders` | [`GET /orders?limit=&offset=`](/docs/v2/api-reference/orders/list-orders) | Pagination params; `limit` max 500 |
| `GET /v1/orders/{request_id}/shipments` | [`GET /orders/{order_id}`](/docs/v2/api-reference/orders/get-order) | Read `tracking_numbers[]` on the order response |
| `POST /v1/orders/{request_id}/cancel` | [`POST /orders/{order_id}/cancel`](/docs/v2/api-reference/orders/cancel-order) | **Pending orders only** in v2. After placement, file a return instead. |
| `POST /v1/orders/{request_id}/abort` | [`POST /orders/{order_id}/cancel`](/docs/v2/api-reference/orders/cancel-order) | Semantic merge into a single cancel endpoint |
| `GET /v1/orders/{request_id}/abort` | *(no equivalent)* | Pre-cancel check; not needed in v2's flow |
| `POST /v1/orders/{request_id}/retry` | *(no equivalent — see [Gap decisions](#gap-decisions))* | v2 handles retries server-side via order attempts |
| `POST /v0/order/{request_id}/danger_footgun_repeat` | *(no equivalent — see [Gap decisions](#gap-decisions))* | Place a new order with a new `idempotency_key` |
| `POST /v0/order/{request_id}/update` | *(no equivalent — see [Gap decisions](#gap-decisions))* | Customer-supplied status push not in v2 |
| `POST /v0/order/{request_id}/insert_merchant_order_id` | *(no equivalent — see [Gap decisions](#gap-decisions))* | Merchant order IDs are tracked internally |
| `GET /v1/queued_orders` | `GET /orders?status_filter=pending` | Status query supplants the dedicated queue endpoint |
### Returns
| v1 | v2 | Notes |
| --------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `POST /v1/returns` | [`POST /returns`](/docs/v2/api-reference/returns/create-return) | Request body restructured — see [Request shape diffs](#post-returns) |
| `GET /v1/returns/{request_id}` | [`GET /returns/{return_request_id}`](/docs/v2/api-reference/returns/get-return) | |
| `POST /v1/orders/{request_id}/return` | [`POST /returns`](/docs/v2/api-reference/returns/create-return) with `order_id` | Convenience route removed; pass `order_id` explicitly |
| `GET /v1/orders/{request_id}/return` | `GET /returns?order_id=…` | List and pick latest by `created_at` |
| `POST /v1/returns/{request_id}/status` | *(no equivalent — see [Gap decisions](#gap-decisions))* | Customer-driven status push not exposed; status updates flow via webhook |
| `POST /v1/returns/{request_id}/status/update` | *(no equivalent — see [Gap decisions](#gap-decisions))* | Same as above |
### Cancellations
| v1 | v2 | Notes |
| ------------------------------------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `POST /v1/cancellations` | [`POST /orders/{order_id}/cancel`](/docs/v2/api-reference/orders/cancel-order) | Resource removed; merged into orders |
| `GET /v1/cancellations/{request_id}` | [`GET /orders/{order_id}/timeline`](/docs/v2/api-reference/orders/get-order-timeline) | Cancellation outcome appears as a `cancelled` milestone on the order timeline |
| `POST /v1/cancellations/{request_id}/retry` | *(no equivalent — see [Gap decisions](#gap-decisions))* | |
### Tracking
| v1 | v2 | Notes |
| -------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `POST /v1/trackings` | *(removed)* | Tracking is sourced server-side; not customer-initiated. Read it via [`GET /orders/{id}`](/docs/v2/api-reference/orders/get-order). |
| `GET /v1/trackings/{request_id}` | [`GET /orders/{order_id}`](/docs/v2/api-reference/orders/get-order) | `tracking_numbers[]` on response |
| `POST /v1/trackings/completed` | *(no equivalent — see [Gap decisions](#gap-decisions))* | |
| `POST /v1/trackings/update` | *(no equivalent — see [Gap decisions](#gap-decisions))* | |
| `POST /v1/upload_trackings` | *(no equivalent — see [Gap decisions](#gap-decisions))* | Bulk customer-supplied tracking upload not in v2 |
| `GET /v0/tracking_history` | *(no equivalent)* | History is part of [`GET /orders/{id}`](/docs/v2/api-reference/orders/get-order) |
### Retailer credentials
| v1 | v2 | Notes |
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `PUT /v1/credentials` | [`POST /managed-accounts`](/docs/v2/api-reference/managed-accounts/create-managed-account) / [`PUT /managed-accounts/{short_id}`](/docs/v2/api-reference/managed-accounts/update-managed-account) | Semantic restructure — see [Retailer credentials](#retailer-credentials-restructure) |
| `GET /v1/account_status/{retailer}/{email}` | [`GET /managed-accounts`](/docs/v2/api-reference/managed-accounts/list-managed-accounts) + client-side filter | No per-account live-status endpoint; account state surfaces in order events |
| `GET /v1/account_locks`, `POST /v1/account_locks/update` | *(no equivalent — see [Gap decisions](#gap-decisions))* | Lock management not exposed |
### Payment
| v1 | v2 | Notes |
| ----------------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------- |
| `POST /v1/revise_payments` | *(no equivalent — see [Gap decisions](#gap-decisions))* | Per-order payment revision removed; manage at the wallet level |
| `POST /v1/vault/payment_info` | `POST /wallet/checkout` + Stripe SetupIntent (see [Wallet](/docs/v2/wallet)) | Vault replaced by Stripe-managed payment methods |
### Removed entirely
| v1 | Notes |
| -------------------------------------------------------------- | -------------------------------------- |
| `POST /v1/gift_balances`, `GET /v1/gift_balances/{request_id}` | Gift-card balance checks removed |
| `POST /v1/messages`, `GET /v1/messages/{request_id}` | Account messaging removed |
| `PUT/GET/DELETE /v1/proxies/byop` | BYOP proxies not exposed |
| `GET /v1/flags`, `POST /v1/flags` | Customer-toggled feature flags removed |
| `GET /v0/health_check`, `GET /v0/status` | Use `GET /health` |
### Concepts new in v2 (no v1 equivalent)
These don't appear in v1, but your integration may want to adopt them after the port:
Balance, top-up, payment methods, transactions, receipts.
Programmatic key rotation via `POST /api-keys`, separate live/test keys.
Single per-user webhook URL with HMAC-signed payloads.
First-class retailer credentials with TOTP, email forwarding.
Cross-retailer product search and details.
Place orders via the Machine Payments Protocol — no account required.
***
## Request shape diffs
### `POST /orders`
```json v1 theme={null}
{
"client_token": "XXXX",
"retailer": "amazon",
"products": [
{
"product_id": "B07JGBW826",
"quantity": 1,
"seller_selection_criteria": { "prime": true }
}
],
"shipping_address": {
"first_name": "Jane",
"last_name": "Doe",
"address_line1": "123 Main St",
"city": "San Francisco",
"state": "CA",
"zip_code": "94103",
"country": "US",
"phone_number": "555-555-5555"
},
"billing_address": { "...same shape..." },
"payment_method": {
"name_on_card": "Jane Doe",
"number": "4242...",
"expiration_month": 12,
"expiration_year": 2030,
"security_code": "123"
},
"retailer_credentials": {
"email": "jane@example.com",
"password": "...",
"totp_2fa_key": "..."
},
"webhooks": {
"order_placed": { "url": "https://yours.example.com/wh/placed" },
"tracking_obtained": { "url": "https://yours.example.com/wh/tracking" }
},
"idempotency_key": "your-key-123",
"max_price": 5000
}
```
```json v2 theme={null}
{
"products": [
{
"url": "https://www.amazon.com/dp/B07JGBW826",
"quantity": 1,
"variant": [{ "label": "Color", "value": "Black" }]
}
],
"shipping_address": {
"first_name": "Jane",
"last_name": "Doe",
"address_line1": "123 Main St",
"city": "San Francisco",
"state": "CA",
"postal_code": "94103",
"country": "US",
"phone_number": "+14155551234"
},
"max_price": 5000,
"idempotency_key": "your-key-123",
"retailer_credentials_id": "zn_acct_AB12CDEF",
"po_number": "PO-9001",
"metadata": { "your": "metadata" }
}
```
**Field-by-field transformation:**
| v1 field | v2 destination | Notes |
| ---------------------------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `client_token` | *drop* | Carried in `Authorization: Bearer …` |
| `retailer` | *drop* | Inferred from product URLs |
| `products[].product_id` | `products[].url` | Convert ID → full retailer URL (e.g. `https://www.amazon.com/dp/{id}`). **Max 10 products per order** — split larger baskets across requests. |
| `products[].quantity` | `products[].quantity` | 1 ≤ qty ≤ 100 |
| `products[].seller_selection_criteria` | *drop* | No v2 equivalent — see [Gap decisions](#gap-decisions) |
| `products[].variants` | `products[].variant` | List of `{label, value}` pairs |
| `shipping_address.zip_code` | `shipping_address.postal_code` | **Field renamed.** |
| `shipping_address.phone_number` | `shipping_address.phone_number` | v2 normalises to E.164. Pass through unchanged; v2 will reformat. |
| `billing_address` | *drop* | Wallet-based payment |
| `payment_method` | *drop* | Wallet-based payment; ensure wallet is funded |
| `retailer_credentials` (embedded object) | `retailer_credentials_id` (`short_id` string) | **Two-step migration** — see [Retailer credentials](#retailer-credentials-restructure) |
| `webhooks` | *drop* | Per-user webhook URL configured once; see [Webhook handler consolidation](#webhook-handler-consolidation) |
| `idempotency_key` | `idempotency_key` | Pass through (max 36 chars). v2 generates one if omitted. Duplicate keys return `409 already_exists` rather than placing a second order. |
| `max_price` | `max_price` | **Confirm units.** v2 requires integer cents. v1 examples and docs commonly use cents; double-check if your code passes dollars. |
| `po_number` | `po_number` | Pass through |
| `is_gift` | `is_gift` | Pass through. **Stricter in v2:** if the retailer offers no free gift option the order fails with `gift_option_unavailable` rather than being placed as a normal order. |
| `gift_message` | `gift_message` | Pass through (max 240 chars). Requires `is_gift: true`. |
| *(custom v1 metadata fields)* | `metadata: {...}` | Stash arbitrary v1 fields not otherwise mapped under `metadata` so they're queryable on the order response |
### `POST /returns`
```json v1 theme={null}
{
"retailer": "amazon",
"merchant_order_id": "111-2222222-3333333",
"products": [{ "product_id": "B07JGBW826", "quantity": 1 }],
"reason_code": "defective_item",
"method_code": "ups_dropoff",
"retailer_credentials": { "...embedded..." },
"webhooks": { "return_placed": { "url": "..." } },
"idempotency_key": "ret-key-123"
}
```
```json v2 theme={null}
{
"order_id": "01931f3c-cd6f-7f00-8000-1234567890ab",
"items": [{ "order_item_id": "01931f3c-...", "quantity": 1 }],
"reason": "defective",
"notes": "Arrived with cracked screen."
}
```
**Field-by-field transformation:**
| v1 | v2 | Notes |
| ------------------------------------ | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `retailer` + `merchant_order_id` | `order_id` (v2 UUID) | **Returns must reference a v2-tracked order.** Orders placed on v1 cannot be returned through v2 — handle in [Gap decisions](#gap-decisions). |
| `products[].product_id` + `quantity` | `items[].order_item_id` + `quantity` | Look up v2 order item UUIDs via [`GET /orders/{order_id}`](/docs/v2/api-reference/orders/get-order) → `items[].id` |
| `reason_code` (free string) | `reason` (enum) | See [reason code lookup](#return-reason-code-lookup) |
| `method_code` | *drop* | v2 picks return method automatically |
| `retailer_credentials` | *drop* | Inherited from the original order |
| `webhooks` | *drop* | Per-user webhook URL |
| `idempotency_key` | *drop* | Not exposed on the v2 returns endpoint |
### Retailer credentials restructure
v1 ships credentials **inside every order request**. v2 stores them once as a first-class resource and references by `short_id`.
For each `(retailer, account_email)` pair in your v1 integration:
```http theme={null}
POST /managed-accounts
Authorization: Bearer zn_live_…
Content-Type: application/json
{
"retailer": "amazon",
"email": "buyer@example.com",
"password": "…",
"totp_secret": "…",
"retailer_config": { ... }
}
```
Response includes a `short_id` like `zn_acct_AB12CDEF`. Save it to your config store.
```json theme={null}
{
"retailer_credentials_id": "zn_acct_AB12CDEF",
"..."
}
```
Omit `retailer_credentials_id` to let v2 auto-select an available managed account for the retailer (subject to availability and account type).
### Return reason code lookup
v1 returns accept any free-text string in `reason_code`. v2 enforces a 10-value enum. Use this mapping:
| v1 `reason_code` (representative) | v2 `reason` |
| ---------------------------------------------------------------- | ------------------------------------------------- |
| `defective_item`, `defective`, `broken`, `not_working` | `defective` |
| `damaged_item`, `damaged`, `arrived_damaged` | `damaged` |
| `wrong_item`, `not_what_i_ordered`, `incorrect_item` | `wrong_item` |
| `wrong_size`, `does_not_fit`, `size_too_small`, `size_too_large` | `wrong_size` |
| `not_as_described`, `misleading_description` | `not_as_described` |
| `empty_box`, `missing_contents` | `empty_box` |
| `not_delivered`, `lost_in_transit`, `package_never_arrived` | `not_delivered` |
| `no_longer_needed`, `bought_by_mistake`, `changed_mind` | `no_longer_needed` |
| `forced_cancellation`, `seller_cancelled`, `unable_to_ship` | `forced_cancellation` |
| *(anything else)* | `other` (preserve the original string in `notes`) |
***
## Response shape diffs
### Order response
| v1 | v2 | Notes |
| ---------------------------------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `request_id` | `id` (UUID) | Type changes from opaque string to UUID — your storage and any code path matching `^[0-9a-f-]+$` must accept it |
| `_request_status` / `_status` / various v1 statuses | `status` (string enum) | See [status mapping](#status-mapping) |
| `merchant_order_ids[].merchant_order_id` | `metadata.merchant_order_id` | Embedded under `metadata` rather than as a sibling field |
| `price_components.{subtotal,tax,shipping,total}` | `metadata.price_components.{...}` | Shape preserved, nested under `metadata` |
| `tracking[].{merchant_order_id, carrier, tracking_number}` | `tracking_numbers[].{carrier, tracking_number, id}` | Field renamed |
| `password_changed` | *not exposed* | Surfaces via order events / webhooks |
### Status mapping
v1's `_status` field can take many forms across versions. Map to v2's strict enum:
| v1 status (representative) | v2 `status` |
| --------------------------------------- | ----------------------- |
| `pending`, `queued`, `submitted` | `pending` |
| `in_progress`, `processing`, `running` | `in_progress` |
| `placed`, `ordered`, `succeeded` | `order_placed` |
| `failed`, `error` | `order_failed` |
| `aborted`, `cancelled_by_user` | `cancelled` |
| `cancelled_by_retailer`, `out_of_stock` | `cancelled_by_retailer` |
If your code uses `if status == "placed"` style branching, rewrite to the v2 enum value. Centralise the comparison in a single helper if more than three call sites match.
***
## Webhook handler consolidation
### Configuration
v1 registered URLs **per order request** in a `webhooks` object on each call. v2 registers **one URL per user**:
```bash theme={null}
# Set the webhook URL once
curl -X POST https:///users \
-H "Authorization: Bearer zn_live_…" \
-H "Content-Type: application/json" \
-d '{"webhook_url": "https://yours.example.com/zinc-webhooks"}'
# Mint a webhook secret (shown once)
curl -X POST https:///users/webhook-secret \
-H "Authorization: Bearer zn_live_…"
```
Every event for orders and returns belonging to that user is POSTed to that single URL with an HMAC-SHA256 signature. The webhook secret has the prefix `zn_whsec_*` so it's easy to spot in logs/config and distinguish from API keys.
### Event name mapping
| v1 event (per-request key) | v2 event name |
| --------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `order_placed` | `order.placed` |
| `order_failed` | `order.failed` |
| `tracking_obtained`, `tracking_updated` | `order.tracking_received` |
| *(none — new in v2)* | `order.started` |
| *(none — new in v2)* | `order.delivered` |
| *(none — new in v2)* | `order.cancelled` |
| `return_placed` | `return.created` |
| `return_failed` | *(no direct equivalent)* — return lifecycle uses `return.approved` / `return.denied` / `return.credited` |
| `status_updated`, `case_updated` | *(no equivalent)* — surfaced via order event stream, not as a webhook |
| `request_succeeded`, `request_failed` | *(dropped)* — these were envelope-level events; v2 fires resource-level events directly |
### Payload + signature verification
Every v2 webhook POST carries:
* **Header** `X-Webhook-Event: ` — e.g. `order.placed`
* **Header** `X-Webhook-Signature: `
* **Body** JSON: `{ "event": "", "order_id": "…", "data": {…} }` (and `"return_id"` for return events)
**Hash the raw body, not the parsed-and-re-serialised body.** JSON re-serialisation may reorder keys or change whitespace, which breaks the signature. Capture the raw request body before parsing.
The HMAC is computed over the **raw request body** with `hmac.new(secret, body, sha256).hexdigest()`. Reject any request whose signature doesn't match, using a constant-time comparison (`hmac.compare_digest` or your language's equivalent — `===` leaks timing).
### Handler hardening
A few operational rules to keep webhook delivery healthy under load:
* **Respond 2xx fast.** Zinc retries on non-2xx and on timeouts. Do the minimum work synchronously (signature check, dedupe, enqueue) and return; defer heavy processing to a background job. A handler that does its full processing inline will pile up retries during transient backend slowness.
* **Dedupe by `(order_id, event)` or `(return_id, event)`.** Network retries can deliver the same event twice. Keep a short-lived seen-set (Redis with TTL, or a unique constraint on a webhook-events table) and short-circuit duplicates.
* **Tolerate out-of-order arrival.** `order.placed` and `order.tracking_received` can arrive in either order if a retry is involved. Make handlers idempotent and don't assume sequence.
### Refactor pattern
The v1 handler was typically a switch on URL path (one route per event). Collapse to a single route + dispatch on the `X-Webhook-Event` header value (or the `event` field in the body):
```python Python theme={null}
import hmac, hashlib
def zinc_webhook(request):
sig = request.headers["X-Webhook-Signature"]
expected = hmac.new(
WEBHOOK_SECRET.encode(),
request.body,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(sig, expected):
return 401
event = request.headers["X-Webhook-Event"]
payload = request.json()
dispatch[event](payload)
return 200
```
```javascript Node theme={null}
import crypto from "crypto";
function zincWebhook(req, res) {
const sig = req.headers["x-webhook-signature"];
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(req.rawBody)
.digest("hex");
if (
sig.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))
) {
return res.status(401).end();
}
const event = req.headers["x-webhook-event"];
dispatch[event](JSON.parse(req.rawBody));
res.status(200).end();
}
```
***
## Sandbox testing
* Mint a `zn_test_*` key in the dashboard or via `POST /api-keys { "is_test": true }`. The [Sandbox guide](/docs/v2/api-reference/introduction/sandbox) walks through how test mode behaves.
* Use the **same v2 base URL** with the test key — test mode is per-key, not per-host.
* Orders placed with `zn_test_*` keys don't actually purchase products; webhooks still fire with realistic timing.
Never mix `zn_test_*` and `zn_live_*` keys in the same environment. A `zn_live_*` key in your CI config places real orders. Add a config-time assertion that rejects the wrong prefix per environment.
### Test product catalog
Drive your test scenarios with the dedicated test product URLs. Fetch the current list from `GET /orders/test-products`. At minimum exercise:
| URL | Mode | What it exercises |
| ---------------------------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------- |
| `https://zinc.com/shop/products/test-success` | — | Happy path through `order_placed`, tracking numbers, and price components |
| `https://zinc.com/shop/products/test-invalid-address` | **Synchronous** | Rejected at order-creation time (4xx) |
| `https://zinc.com/shop/products/test-url-unreachable` | Synchronous | 4xx at creation |
| `https://zinc.com/shop/products/test-insufficient-funds` | Synchronous | 4xx at creation |
| `https://zinc.com/shop/products/test-out-of-stock` | **Asynchronous** | Order creates successfully; `order.failed` webhook arrives later with `error_type="product_out_of_stock"` |
| `https://zinc.com/shop/products/test-price-exceeded` | Asynchronous | `order.failed` with `error_type="max_price_exceeded"` |
| `https://zinc.com/shop/products/test-invalid-variant` | Asynchronous | `order.failed` |
| `https://zinc.com/shop/products/test-shipping-unavailable` | Asynchronous | `order.failed` |
The synchronous-vs-asynchronous split matters: see [Behavioral changes worth knowing](#sync-vs-async-failures) above. Make sure your test suite covers both arrival paths.
### Sandbox limitations
Test mode intentionally skips a few checks so you can develop without a funded wallet or real product URLs:
* **No wallet balance check** — orders go through even if the wallet would be insufficient in production
* **No URL reachability check** — fake product URLs are accepted
* **No address validation** — addresses aren't validated against Google's address-validation API
Plan a small set of **real production orders** as the final cutover check — sandbox can't catch problems with any of those three.
***
## Gap decisions
A few v1 endpoints have no v2 equivalent. For each, capture your decision in a `MIGRATION_GAPS.md` (or wherever you track porting decisions) so they don't get lost during code review:
| File | Line | v1 endpoint | What we use it for | Decision |
| ------------------------- | ---- | ---------------------------- | ----------------------------- | ----------------------------- |
| `src/handlers/balance.js` | 42 | `GET /v1/gift_balances` | Display gift balance in admin | drop |
| `src/api/client.ts` | 117 | `POST /v1/messages` | Send account-recovery message | workaround |
| `src/jobs/retry.py` | 23 | `POST /v1/orders/{id}/retry` | Auto-retry stuck orders | drop (v2 retries server-side) |
Email [support@zinc.com](mailto:support@zinc.com) for any gap where you can't find a workaround — most have one, and a few may justify keeping a small v1 dependency until v1 sunset.
***
## Validation
After the port, run this end-to-end smoke test against the v2 sandbox using a `zn_test_*` key:
Either via `GET /managed-accounts` or by creating one inline.
Assert 201 response; capture the returned `id`.
Typically completes in under a minute in sandbox mode.
Both events should arrive with valid HMAC signatures.
Assert 201 response.
The return lifecycle flows from here.
If your existing v1 test suite covered these flows, port the tests first and use them as the validation harness.
***
The agent runbook turns this guide into a workflow Claude Code can execute directly against your integration repo — discovery → auth swap → endpoint port → webhook consolidation → validation.
# Machine Payments Protocol (MPP)
Source: https://www.zinc.com/docs/v2/mpp
Enable AI agents to place orders and pay via HTTP 402 — no Zinc account required.
The [Machine Payments Protocol (MPP)](https://mpp.dev) is an open standard for machine-to-machine payments via HTTP 402, developed by Tempo and Stripe. Zinc supports MPP on the `/agent/orders` endpoint, allowing AI agents to place orders and pay in a single request flow — no Zinc account or API key required.
## Why MPP?
Traditional API integrations require account setup, API keys, and a pre-funded balance. MPP removes these steps by embedding payment directly into the HTTP request cycle:
* **No account needed** — Agents pay per-request using standard HTTP headers
* **Multiple payment methods** — Stripe cards/wallets, Tempo stablecoins, and more
* **Built for agents** — Designed for automated, machine-to-machine payment flows
## How It Works
MPP uses an HTTP 402 challenge-credential-receipt flow:
The agent sends a `POST /agent/orders` request without payment credentials.
Zinc responds with HTTP `402 Payment Required` and `WWW-Authenticate` headers describing available payment methods and the amount due.
The MPP client completes payment through the chosen method (e.g., Stripe checkout, Tempo transfer) and receives a credential.
The agent resubmits the original request with the payment credential in the `Authorization` header.
Zinc validates the payment receipt and processes the order, returning a `201` response with order details and two important headers:
* `X-Api-Key` — A temporary API key the agent can use to check order status via `GET /orders/{id}` without repeating the payment flow.
* `Payment-Receipt` — The MPP payment receipt confirming the charge.
## Checking Order Status
The `201` response includes an `X-Api-Key` header containing a temporary API key. Use this key to poll the order status without going through the 402 payment flow again:
```bash theme={null}
curl https://api.zinc.com/orders/{order_id} \
-H "Authorization: Bearer "
```
The `X-Api-Key` is scoped to the order it was issued for. Store it after placing the order so your agent can track fulfillment progress.
## Quick Start
### Install an MPP Client
MPP client libraries handle the 402 flow automatically. Official SDKs are available in TypeScript, Python, and Rust.
```bash npm theme={null}
npm install mppx viem
```
```bash pip theme={null}
pip install pympp
```
### Place an Order
The easiest way to test is with the `mppx` CLI, which handles the 402 payment flow automatically:
```bash theme={null}
npx mppx https://api.zinc.com/agent/orders \
--method POST \
--body '{
"products": [{"url": "https://www.amazon.com/dp/B0EXAMPLE"}],
"shipping_address": {
"first_name": "Jane",
"last_name": "Smith",
"address_line1": "123 Main St",
"city": "Seattle",
"state": "WA",
"postal_code": "98101",
"country": "US",
"phone_number": "2065551234"
}
}'
```
To integrate programmatically, use the client SDKs:
```typescript TypeScript theme={null}
import { Mppx, tempo } from "mppx/client";
import { privateKeyToAccount } from "viem/accounts";
const mppx = Mppx.create({
methods: [
tempo({
account: privateKeyToAccount("0x..."),
maxDeposit: "1",
}),
],
});
// The MPP client handles the 402 challenge flow automatically
const response = await mppx.fetch(
"https://api.zinc.com/agent/orders",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
products: [
{ url: "https://www.amazon.com/dp/B0EXAMPLE" }
],
shipping_address: {
first_name: "Jane",
last_name: "Smith",
address_line1: "123 Main St",
city: "Seattle",
state: "WA",
postal_code: "98101",
country: "US",
phone_number: "2065551234",
},
}),
}
);
console.log(await response.json());
```
## Supported Payment Methods
| Method | Description |
| ---------- | ----------------------------- |
| **Stripe** | Cards and wallets via Stripe |
| **Tempo** | Stablecoin payments via Tempo |
The available payment methods are returned in the `WWW-Authenticate` headers of the 402 response. Your MPP client will automatically select a compatible method.
## Using Stripe with MPP
The MPP client libraries (`mppx`, `pympp`) abstract over payment methods. If you'd rather integrate with Stripe directly — for example, minting Shared Payment Tokens (SPTs) via the [Stripe Link API](https://link.com/agents) or `link-cli` — here's the wire-level flow.
### 1. Request the challenge
POST your order to `/agent/orders` with no `Authorization` header:
```http theme={null}
POST /agent/orders HTTP/1.1
Content-Type: application/json
{
"products": [{"url": "https://www.amazon.com/dp/B0EXAMPLE", "quantity": 1}],
"shipping_address": { ... },
"max_price": 5000
}
```
Zinc responds with **HTTP 402** and one `WWW-Authenticate` header per configured payment method. Pick the one containing `method="stripe"`:
```
WWW-Authenticate: Payment realm="zinc", method="stripe", id="...", intent="charge", request="", ...
```
Many HTTP clients collapse repeated response headers into a single comma-joined string, which corrupts the `WWW-Authenticate` param list. Read the **raw** header list and select by `method="stripe"`. In Python with `httpx`, use `resp.headers.raw`.
The `request` value is a base64-encoded JSON blob whose `methodDetails.networkId` is the Stripe agentic-commerce profile ID — you'll pass it to Stripe when minting the SPT.
### 2. Mint a Shared Payment Token
Use the Stripe Link API — directly or via `link-cli` — to mint an SPT bound to this challenge:
```bash theme={null}
# Decode the challenge to extract the network_id
link-cli mpp decode --challenge ""
# Create the spend request (prompts the user to approve in the Link app)
link-cli spend-request create \
--credential-type shared_payment_token \
--network-id \
--payment-method-id \
--amount \
--currency usd \
--context "" \
--line-item "name:,quantity:1,unit_amount:" \
--total "type:total,display_text:Total,amount:" \
--request-approval
# After approval, retrieve the SPT
link-cli spend-request retrieve --include shared_payment_token
```
The spend-request amount must equal `max_price + $1.00`. The extra dollar is the Zinc API fee, baked into the challenge so that the full `max_price` remains available to the retailer.
### 3. Resubmit with the credential
Reissue the original POST, this time with an `Authorization: Payment ` header:
```http theme={null}
POST /agent/orders HTTP/1.1
Content-Type: application/json
Authorization: Payment
{ ...same body as step 1... }
```
The credential is **not** just the SPT — it's an MPP `Credential` object containing:
* A `ChallengeEcho` echoing the exact fields from the challenge in step 1 (`id`, `realm`, `method`, `intent`, `request`, `expires`, `digest`)
* A `payload` of `{"type": "spt", "shared_payment_granted_token": ""}`
Constructing and serializing this by hand is error-prone. Use `pympp`'s helpers:
```python theme={null}
from mpp import (
ChallengeEcho, Credential, format_authorization, parse_www_authenticate,
)
challenge = parse_www_authenticate(stripe_www_authenticate_value)
credential = Credential(
challenge=ChallengeEcho(
id=challenge.id,
realm=challenge.realm,
method=challenge.method,
intent=challenge.intent,
request=challenge.request_b64,
expires=challenge.expires,
digest=challenge.digest,
),
payload={"type": "spt", "shared_payment_granted_token": spt},
)
auth_header = format_authorization(credential)
```
On success, Zinc returns **201** with the order details, an `X-Api-Key` header for polling status, and a `Payment-Receipt` header confirming the verified MPP receipt.
**Save the `X-Api-Key` from the 201 response.** It's a temporary API key scoped to this order, and it's the only way to check fulfillment status after placement without repeating the 402 payment flow. See [Checking Order Status](#checking-order-status) for the polling request format.
## Order Lifecycle and Refunds
MPP orders include automatic safeguards so agents aren't charged for orders Zinc can't (or doesn't) fulfill.
### Validation runs before payment
When you submit an order with a payment credential, Zinc validates the order data — product URLs, retailer support, shipping country, and address — **before** charging the credential. A validation failure returns **HTTP 400** with no charge applied to the credential, leaving the SPT untouched and reusable for a corrected retry.
This applies to all MPP methods, not just Stripe.
### Automatic refunds (Stripe)
For orders paid via Stripe, the agent authorizes `max_price + $1.00 API fee` upfront. Zinc then refunds the credential automatically in two cases:
* **Partial refund** — When the retailer's actual total comes in below `max_price`, Zinc refunds the difference via the Stripe PaymentIntent. The agent effectively pays `actual_total + $1.00`.
* **Full refund** — If the order ultimately fails (status `failed` — e.g., the retailer rejects the order, the item is out of stock, etc.), Zinc refunds the entire charged amount.
Refunds are issued server-side against the original Stripe PaymentIntent and follow Stripe's standard settlement timelines. No agent-side action is required.
## Next Steps
Try placing an MPP order in our interactive playground.
See the full API spec for the MPP order endpoint.
Read the full MPP protocol specification.
# Wallet
Source: https://www.zinc.com/docs/v2/wallet
Prefund a Zinc Wallet balance and let Zinc draw each order's cost and fee automatically.
The **wallet** is Zinc's default way to pay. You pre-fund a balance, and each order draws its cost plus a flat \$1 API fee from that balance. Fund once, then place orders without attaching any payment details per request.
## Funding Your Wallet
Add funds from the **Wallet** button in the top navigation. Enter the amount you want to add; that full amount is credited to your wallet, and the processing fee for your chosen method is added on top of what you're charged.
| Method | Processing fee |
| -------------------- | ---------------------- |
| Credit or debit card | 2.9% + \$0.30 |
| ACH bank transfer | 0.8%, capped at \$5.00 |
| Wire transfer | \$15.00 per wire |
Self-serve top-ups run from \$15.00 up to \$100,000.00; for larger amounts, contact us to arrange a wire. Test and production wallets are kept separate.
## Placing Orders
Place orders as normal, with no `payment` object. Each order draws `order_cost + $1` from your balance. An order placed without enough balance to cover it is rejected with `insufficient_funds` (HTTP 402), so keep a buffer.
## Refunds
Zinc credits your wallet automatically when an order doesn't go through:
* **Cancelled by the retailer:** the full charge is refunded to your wallet.
* **Return credited:** when a return is resolved by crediting rather than a merchant RMA, the credit lands in your wallet.
## Other Ways to Pay
If you'd rather not carry a balance, charge your own end-customer instead with [Stripe Connect](/docs/v2/connect), or pay per request with no Zinc account using [MPP](/docs/v2/mpp).
# What is Zinc?
Source: https://www.zinc.com/docs/what-is-zinc
Zinc is building the universal API for ecommerce automation. With a single integration, you can buy products, retrieve pricing and product data, and automate order management across the world's top online retailers.
**Zinc 2.0 is in active development.** We're rebuilding from the ground up with AI to support dramatically more retailers and flexibility. Some features from v1 (account automation, tracking, returns, cancellations) are coming soon. For production use cases requiring these features today, see [v1 documentation](/docs/v1/quickstart).
## Why Zinc?
* **Universal API:** We're building one consistent, modern API to access millions of products across retailers worldwide.
* **Simplified Ordering:** Place orders across a growing list of retailers with a unified interface.
* **Developer-First:** Clear documentation, fast onboarding, and responsive support.
## Who Uses Zinc?
* **Cross Border:** Enable global e-commerce by purchasing and shipping products internationally.
* **AI:** Power intelligent agents and automation platforms that buy, compare, and manage products programmatically.
* **Gifts + Rewards:** Automate the delivery of gifts, incentives, and rewards to customers or employees at scale.
* **Price Tracking:** Monitor product prices and availability across retailers for dynamic pricing, alerts, or analytics.
* **Social Shopping:** Build platforms where users can discover, share, and purchase products collaboratively.
# Agent Product Details
Source: https://www.zinc.com/docs/v2/api-reference/agent/product-details
versions/latest.json POST /agent/products/details
Retrieve full product details for a single product, paid per call via the Machine Payments Protocol (MPP).
`POST /agent/products/details` returns the full product details for a specific product on a retailer (`amazon` or `walmart`) — title, images, description, specs, and more. It's the agent-native, MPP-paid counterpart to the authenticated [Get Product Details](/docs/v2/api-reference/products/get-product) endpoint.
No Zinc account is required. Each call is paid per request via the [Machine Payments Protocol (MPP)](/docs/v2/mpp).
## Pricing
| | |
| ----------- | ---------------------------------------- |
| **Price** | \$0.01 per call (fixed) |
| **Payment** | MPP (HTTP 402 challenge-credential flow) |
| **Account** | Not required |
## Caching and freshness
Retailer data is cached. Use these optional query parameters to control freshness:
* **`max_age`** — accept a cached response only if it is no older than this many seconds.
* **`newer_than`** — accept a cached response only if it was retrieved at or after this Unix timestamp.
* **`async`** — return immediately with `status: "processing"` instead of waiting for a fresh fetch. Poll again to retrieve the completed result.
## 402 Payment Required
If no valid payment credential is provided, the API returns `402 Payment Required` with one `WWW-Authenticate` header per payable method (per RFC 9110 §11.6.1). Your MPP client uses these to complete payment and resubmit the request. See the [MPP guide](/docs/v2/mpp) for details.
# Agent Product Offers
Source: https://www.zinc.com/docs/v2/api-reference/agent/product-offers
versions/latest.json POST /agent/products/offers
Retrieve offers and pricing for a product, paid per call via the Machine Payments Protocol (MPP).
`POST /agent/products/offers` returns the offers and pricing for a specific product on a retailer (`amazon` or `walmart`). It's the agent-native, MPP-paid counterpart to the authenticated [Get Product Offers](/docs/v2/api-reference/products/get-product-offers) endpoint.
No Zinc account is required. Each call is paid per request via the [Machine Payments Protocol (MPP)](/docs/v2/mpp).
## Pricing
| | |
| ----------- | ---------------------------------------- |
| **Price** | \$0.01 per call (fixed) |
| **Payment** | MPP (HTTP 402 challenge-credential flow) |
| **Account** | Not required |
## Caching and freshness
Retailer data is cached. Use these optional query parameters to control freshness:
* **`max_age`** — accept a cached response only if it is no older than this many seconds.
* **`newer_than`** — accept a cached response only if it was retrieved at or after this Unix timestamp.
* **`async`** — return immediately with `status: "processing"` instead of waiting for a fresh fetch. Poll again to retrieve the completed result.
## 402 Payment Required
If no valid payment credential is provided, the API returns `402 Payment Required` with one `WWW-Authenticate` header per payable method (per RFC 9110 §11.6.1). Your MPP client uses these to complete payment and resubmit the request. See the [MPP guide](/docs/v2/mpp) for details.
# Get My Usage
Source: https://www.zinc.com/docs/v2/api-reference/usage/get-my-usage
versions/latest.json GET /usage
Retrieve your Zinc API usage and order volume over a trailing time window.
Retrieve your own data-API usage over a trailing window, broken down per endpoint.
Use this endpoint to monitor your request volume and track usage against your plan limits.