Merchant integration guide
Download the complete public OpenAPI schema
Merchant integration guide
Connect your backend to OpenMerchant to price quote requests and confirm or reject orders. Start with a test-mode merchant, a catalog item and variant, and a test secret API key from the dashboard.
Building an agent that buys for customers? See the Buyer integration guide under Purchasing.
1. Read requirements and create a destination
Call GET /v1/merchant with Authorization: Bearer <your secret API key>. It returns merchant setup requirements, blockers, and existing verification results without starting checks. The key selects the account and mode; X-Mode cannot switch a test key to live.
Create an HTTPS destination using POST /v1/merchant/destinations:
{
"name": "Merchant backend",
"responds_to_quotes": true,
"event_types": ["quote.requested", "order.requested", "order.confirmed", "order.rejected"],
"webhook": {"endpoint_url": "https://merchant.example/webhooks/openmerchant"}
}
Store the returned secret.secret in your backend's environment. Normal destination reads contain only a secret prefix. Create and rotate operations return secret material; keep it out of prompts, browser code, and logs.
Only one destination can be the designated quote responder per merchant and mode. Order-response authority follows from subscribing to all three order lifecycle events above. This setup does not require a decision connection.
2. Verify incoming deliveries
OpenMerchant POSTs the event envelope to your endpoint. Verify the exact raw request bytes before parsing JSON.
| Header | Purpose |
|---|---|
OpenMerchant-Signature |
Timestamped HMAC-SHA256 delivery signature. |
OpenMerchant-Webhook-Id |
Immutable event ID for correlation and duplicate detection. |
OpenMerchant-Webhook-Event |
Event type, matching the envelope's type. |
OpenMerchant-Destination-Id |
Destination authorized for this delivery. |
OpenMerchant-Response-Url |
URL for a separate business-response POST, when this destination may respond. |
The signature format is t=<epoch>,v1=<digest>. Sign the timestamp, a period, and the raw body bytes using the destination secret. Reject timestamps more than 60 seconds from the current time and use constant-time digest comparison. During the 24-hour rotation overlap, a header can include multiple v1 digests; either current or previous valid secret may match.
Return 2xx to acknowledge delivery after durably accepting the event. The acknowledgement body is not a quote or order decision. Start processing promptly: quote responses have a 10-second window. Orders must still be awaiting merchant confirmation when answered; inspect the order after any ambiguous response or timeout.
3. Submit a signed business response
Serialize JSON once and retain those exact bytes for retries. Generate a fresh response signature for each attempt:
import hashlib
import hmac
import time
def response_signature(raw_body: bytes, secret: str) -> str:
"""Sign the exact JSON bytes sent to OpenMerchant."""
timestamp: int = int(time.time())
signed_body: bytes = str(timestamp).encode('ascii') + b'.' + raw_body
digest: str = hmac.new(
secret.encode('utf-8'), signed_body, hashlib.sha256
).hexdigest()
return f't={timestamp},v1={digest}'
POST those bytes as application/json to the received OpenMerchant-Response-Url. Send OpenMerchant-Response-Signature and echo OpenMerchant-Destination-Id, OpenMerchant-Webhook-Id, and OpenMerchant-Webhook-Event. Response signatures have a 60-second tolerance.
Use your configured OpenMerchant API origin to validate the response URL before sending credentials. Check the expected quote or order path and the identifier in the signed envelope. Do not follow redirects when posting a response.
A signed receiver needs only its destination secret. A server using a merchant secret API key can also use the same /respond endpoints; API-key quote bodies use outcome and offers, as documented in the operation reference.
4. Accept or decline a quote
On quote.requested, read price and availability from your own system. Copy event_id from the envelope's id and quote_id from data.quote.id. Return a future expiry you can honor. Quoting availability does not reserve inventory.
For a request naming one catalog item and variant, this signed response offers two units for a complete total of USD 50.00. quoted_amount_cents is the complete quoted total in minor units in the merchant currency. Bookings additionally require the exact resolved booking_snapshot.
{
"event_id": "om_agev_quote_example",
"event_type": "quote.requested",
"quote_id": "om_agq_example",
"decision": "accepted",
"quoted_amount_cents": 5000,
"currency": "USD",
"expires_at": "2030-01-01T00:15:00Z",
"available_quantity": 2
}
A signed quote response returns an acknowledgement:
{
"event_id": "om_agev_quote_example",
"event_type": "quote.requested",
"target_id": "om_agq_example",
"duplicate": false
}
If unavailable, decline with an optional explanation and omit price, expiry, quantity, and booking fields:
{
"event_id": "om_agev_quote_example",
"event_type": "quote.requested",
"quote_id": "om_agq_example",
"decision": "declined",
"message": "The requested quantity is unavailable."
}
For alternatives or a complete subtotal ledger, use the full offer format in Resolve a pending merchant quote.
A buyer may propose a price. When they do, data.quote.price_proposal carries { "unit_amount": 1600, "currency": "USD" }: a price for one unit, in your market's currency, before fulfillment and tax. It is yours to accept, counter, or ignore, and you answer it with the price you quote; nothing else is needed. It is repeated on every refresh of that quote, so the same proposal can be answered the same way. The buyer sees negotiation.outcome: "forwarded" and pays the price your offer states, never the price they proposed.
5. Confirm or reject an order
On an authorized order.requested delivery, check the order and its lines against your inventory or booking system. Persist an idempotent reservation/fulfillment decision before replying. A confirmation authorizes OpenMerchant to settle the order and capture its payment; only confirm what you can fulfill.
{
"outcome": "confirmed",
"reservation_reference": "RES-88213",
"confirmation_reference": "SO-10045"
}
If you cannot fulfill it, reject:
{
"outcome": "rejected",
"note": "The requested inventory is no longer available."
}
These standard signed order responses return the updated order, just like API-key order responses. They omit the connector-specific decision object. Treat order.confirmed and order.rejected as informational lifecycle updates and acknowledge them without posting another business response.
The order response reference describes per-line availability and optional repricing. subtotal_amount_cents is the merchandise subtotal before OpenMerchant recomputes tax. It differs from the quote's complete total. Increasing the total above the buyer's authorization or changing currency requires a new authorization; reject rather than attempting to charge more.
6. Verify and diagnose
POST /v1/merchant/destinations/{destination_id}/testchecks signed delivery only. It is available in test and live mode and creates no purchase.POST /v1/merchant/integration_checkswithkind: "quote",destination_id, and aquote_requestusing the quote-create schema sends a real test-mode quote to your responder, bypassing cached answers. It creates no order or payment.- To verify orders, explicitly run a test-mode buyer/payment flow, then start an integration check with
kind: "order",destination_id, and itsorder_id. A passing result requires a signed response from that destination and captured test payment. The check itself never creates orders or moves money. - Poll
GET /v1/merchant/integration_checks/{check_id}.GET /v1/merchantshows the latest checks and separates configuration, delivery, quote responses, and order responses. Configuration changes make old evidence stale. - Inspect
GET /v1/merchant/destinations/{destination_id}/eventsand/events/{event_id}. Each response includes only that destination's delivery history. - Persist responses by destination and event ID. Exact signed quote retries return
duplicate: true; exact standard order retries return the current order. A different body for an accepted event returns409. Regenerate the signature timestamp while keeping the response body identical. - After an uncertain timeout, read the quote/order before doing inventory or fulfillment work again. Do not turn a transport retry into a second reservation or order.
| Symptom | Check |
|---|---|
| Delivery succeeds but the buyer waits | Submit the separate response; check responder authority and the quote deadline/order state. |
Response returns 401 or 403 |
Verify the destination, raw bytes, timestamp, echoed event headers, and signing secret. |
Response returns 422 |
Compare the response with the documented schema and required fields. |
Response returns 409 |
Inspect current state; the event may already have an accepted response or the quote may have expired. |
| Order confirmation fails | Inspect payment and order state before retrying; confirmation can trigger capture. |
Use the integration kit for copyable prompts, skills, TypeScript/Python responders, and the CLI. Keep credentials in environment variables or your secret store.