Skip to main content
Direction: VaultGraph → merchant. Your storefront exposes one HTTPS endpoint; the gateway POSTs to it on every tool invocation. Your storefront never calls VaultGraph.
Status: v1.0 (stable). Backwards-incompatible changes will bump the major version. This document is the canonical reference for merchants implementing a VaultGraph commerce backend over HTTPS. It defines the request/response envelope, signing scheme, error model, idempotency contract, retry/timeout behavior, and pagination convention. The @vaultgraph/sdk/adapter helper wraps everything below in a single typed factory — most merchants should use it instead of implementing the wire contract by hand.

1. Transport

  • Direction: VaultGraph → merchant. The merchant exposes one HTTPS endpoint; the gateway calls it.
  • Method: POST.
  • URL shape: {endpoint_url}/{method} — the configured endpoint_url joined with the method name as a single path segment. The path prefix is the merchant’s choice — VaultGraph does not reserve or require any particular name. Example: https://shop.example.com/webhooks/vaultgraph/createCheckout.
  • TLS: required. The gateway will not call plain http:// endpoints.
  • Content-Type: application/json; charset=utf-8 on both directions.

2. Request envelope

{
  "protocol": "1.0",
  "method": "createCheckout",
  "params": { "input": { "currency": "GBP" } }
}
FieldTypeNotes
protocolstringWire protocol version. Currently "1.0". Merchants MUST reject requests with an unsupported protocol.
methodstringOne of the methods in §6. Mirrors the path segment.
paramsobjectMethod-specific arguments. Always an object (never an array or primitive).
contextobjectOptional string → string map of caller-supplied request context — see Request context. Omitted when the caller set none.

Request context

Requests carry opaque key/value context — most importantly customer identity — which the gateway forwards verbatim as the envelope’s context so your endpoint can scope reads (most importantly listOrders) and attribute writes. Context enters a request in two ways:
  • Transport headers. A caller that controls its HTTP transport sets x-vg-ctx-* headers on its MCP or REST requests. The gateway lower-cases each name, strips the prefix, and maps hyphens to underscores:
    x-vg-ctx-customer: cus_42            →   "context": { "customer": "cus_42" }
    x-vg-ctx-session-token: <token>      →   "context": { "session_token": "<token>" }
    
    Use hyphens in header names: some proxies (e.g. nginx’s default config) silently drop header fields containing underscores. Key names are otherwise yours to choose; the gateway never reads, validates, or adds entries.
  • The session_token tool argument. Every commerce tool accepts an optional opaque session_token string — the path for agent platforms whose connector configuration is static (no per-conversation headers). Supply the value to the conversation (e.g. as a vg_session variable the agent is instructed to copy verbatim), and the gateway strips it from the tool input — it never appears in params — and forwards it as context.session_token. The token itself is provisioned by your application outside the conversation, or minted mid-conversation via Customer sign-in.
context is part of the signed body (§3), so it cannot be altered between VaultGraph and you. It is asserted, not authenticated: anyone holding the deployment API key can claim any value, and a session_token additionally travels through the agent conversation. Make the value verifiable — a short-lived signed token bound to the customer and an expiry, not a bare customer id — and verify it in your handler before trusting it. The typed RequestContext ships from @vaultgraph/sdk/adapter: a session_token?: string named field plus an open [key: string]: string | undefined index for the other keys you chose.

Authorizing owner-scoped reads

getOrder, getCheckout, and listOrders return data that belongs to one customer. The ids an agent holds are asserted, not secret, so authorize every such read against a verified caller:
  1. Verify context.session_token and resolve the owning customer. An unverifiable or absent token is an anonymous caller.
  2. Confirm that customer owns the requested resource, then signal the outcome with whatever error you choose (§7) — e.g. throw order_not_found (404) when the id is unknown or not the caller’s, or authentication_required (401) for listOrders from an anonymous caller so the agent can prompt the user to sign in. (An authenticated customer who genuinely has no orders still returns an empty page, { "orders": [] }.)
Best practice (optional): for single-resource reads, throw the same *_not_found whether the id is unknown or simply not the caller’s — a distinct “exists but forbidden” answer lets a caller enumerate ids they don’t own.
The @vaultgraph/sdk/adapter handler turns step 1 into a single hook. Pass verifySession(context) to createAdapterHandler; it runs once per request, and its return value — your verified identity, of any shape, or null for an anonymous caller — is handed to every method as the final session argument. The hook is the sole consumer of session_token: the handler strips it from the context methods receive, so owner-scoped reads gate on session directly and a method can’t trust the raw token by mistake.

Headers

HeaderRequiredNotes
Content-Typeyesapplication/json.
User-AgentyesStable identifier VaultGraph-Commerce/<version>. Allowlist it at your WAF — see Bot protection.
X-VaultGraph-VersionyesSame value as protocol in the body. Lets edge proxies short-circuit rejection.
X-VaultGraph-SignatureyesSee §3.
X-VaultGraph-Idempotency-Keyon mutating methods (§5)UUID v4 generated per logical operation by the gateway.

Bot protection

Gateway requests originate from datacenter IPs with the User-Agent above. WAF and bot-filtering products (e.g. Cloudflare Super Bot Fight Mode) may classify such traffic as a bot and answer with an HTML challenge or 403, which the gateway surfaces as a failed connection. If your endpoint sits behind one, exempt the adapter path: add a rule that skips bot mitigation for requests to your {endpoint_url} path that carry the X-VaultGraph-Signature header.

3. Signing

Authentication is HMAC-SHA256 over ${timestamp}.${idempotencyKey}.${rawBody}.
  1. The gateway takes the current Unix timestamp in seconds.
  2. It computes signature = HMAC_SHA256(secret, "${timestamp}.${idempotencyKey}.${rawBody}") and hex-encodes it. ${idempotencyKey} is the value of the X-VaultGraph-Idempotency-Key header (§5); on non-mutating methods, which carry no idempotency key, it is the empty string.
  3. It sets X-VaultGraph-Signature: t=<timestamp>,v1=<hex_signature>.
Merchants MUST:
  1. Reject requests where X-VaultGraph-Signature is missing or malformed.
  2. Reject requests where |now - timestamp| > 5 minutes. This is the replay window.
  3. Recompute the HMAC over the timestamp, the X-VaultGraph-Idempotency-Key header value (empty string when absent), and the raw body bytes (not a re-serialized JSON), and reject on mismatch. Use a constant-time comparison.
The t=…,v1=… shape leaves room for v2 to ship a new digest in the future without breaking existing verifiers.

Secret rotation

VaultGraph issues the signing secret when you create the commerce backend in the portal and lets you rotate it from the backend’s settings page. After a rotation the gateway begins signing with the new secret within 60 seconds. The protocol does not multiplex signatures, so coordinating a dual-accept window on your verifier (accept both old and new during the cutover) is the merchant’s responsibility.

4. Response envelope

Success:
{
  "data": {/* method-specific result */}
}
Failure:
{
  "error": {
    "code": "checkout_not_open",
    "status": 409,
    "detail": "Checkout ck_abc has already completed."
  }
}
FieldTypeNotes
dataanyRequired on success. Method-specific shape (§6).
error.codestringRequired on failure. Any string you choose — forwarded to the agent. See §7.
error.statusintegerOptional. HTTP-style status the gateway should surface. Defaults to the HTTP response status.
error.detailstringOptional. Human-readable message safe for upstream display; do not include secrets or PII.
The HTTP status code SHOULD mirror error.status. Returning a 2xx with an error field is tolerated (the gateway prefers the envelope) but discouraged. A response with neither data nor error — or an HTTP error with no JSON body — is treated as commerce_backend_unavailable (HTTP 502).

5. Idempotency

The gateway sends X-VaultGraph-Idempotency-Key: <uuid> on every mutating method:
  • createCheckout
  • addLineItems
  • updateLineItem
  • removeLineItem
  • applyDiscount
  • setFulfillment
  • setBuyer
  • completeCheckout
  • requestAuthentication
  • verifyAuthentication
Reads (searchProducts, getProduct, listCategories, getCheckout, listFulfillmentMethods, getOrder, listOrders) do not carry an idempotency key. The contract:
  • The merchant SHOULD store the full response (status + body) keyed by the idempotency key.
  • If the same key arrives a second time within the merchant’s TTL, the merchant SHOULD return the stored response verbatim instead of re-running the operation.
  • Cache both success AND error responses. A mutating call that fails after committing a side effect (e.g. order written, downstream notification failed) must NOT re-execute on retry. The gateway treats a well-formed error envelope as a deterministic answer and will not retry it, but a network blip mid-response can still cause a retry — the cache is what protects against the duplicate.
  • Recommended TTL: 24 hours. Retries beyond that window are vanishingly rare; storage costs grow if you keep them forever.
The SDK helper exposes an idempotencyStore option that handles this for you given a { get(key), set(key, { status, body }) } interface — most merchants will plug in their existing cache layer (Redis, Postgres unlogged table, KV).

6. Methods

MethodMutatingparamsdata
searchProductsno{ input: SearchCatalogInput } (see Catalog search)ProductPage ({ products: Product[], pagination?: { … } })
getProductno{ id: string }Product. Throw product_not_found (404) when no product matches.
listCategoriesno{}Category[] ({ value: string, count?: integer }[])
createCheckoutyes{ input: CheckoutCreateInput }Checkout
getCheckoutno{ id: string }Checkout. Throw checkout_not_found (404). Owner-scoped — see Authorizing owner-scoped reads.
addLineItemsyes{ checkout_id: string, line_items: LineItem[] }Checkout
updateLineItemyes{ checkout_id: string, line_item_id: string, quantity: integer ≥ 0 }Checkout
removeLineItemyes{ checkout_id: string, line_item_id: string }Checkout
applyDiscountyes{ checkout_id: string, code: string }Checkout
listFulfillmentMethodsno{ checkout_id: string }FulfillmentMethod[] (see Fulfillment)
setFulfillmentyes{ checkout_id: string, fulfillment_method_id: string, shipping_address?: ShippingDestination }Checkout
setBuyeryes{ checkout_id: string, buyer: Buyer, billing_address?: BillingAddress }Checkout
completeCheckoutyes{ checkout_id: string }Order
getOrderno{ id: string }Order. Throw order_not_found (404). Owner-scoped — see Authorizing owner-scoped reads.
listOrdersno{ input: ListOrdersInput } (see Pagination)OrderPage ({ orders: Order[], pagination?: { … } }). Owner-scoped — see Authorizing owner-scoped reads.
requestAuthenticationyes{ email: string }AuthChallenge ({ challenge_id: string }) — see Customer sign-in.
verifyAuthenticationyes{ challenge_id: string, code: string }AuthSession ({ session_token: string, expires_at?: string }) — see Customer sign-in.
TypeScript types for every params and data shape ship from @vaultgraph/sdk/adapter — install it for types even if you don’t use the handler factory in §10. The examples below show each shape with realistic values; the types remain the exhaustive reference. All money is integer minor units (e.g. 12900 = £129.00) paired with an ISO 4217 currency.

Data shapes

Inputs

CheckoutCreateInputcreateCheckout. line_items may be empty; buyer is optional; payment may be {}:
{
  "currency": "GBP",
  "line_items": [{ "item": { "id": "var_ridge_32l" }, "quantity": 2 }],
  "buyer": { "email": "alice@example.com", "full_name": "Alice Smith" },
  "payment": {},
}
LineItem — the element type of addLineItems. Send the variant id and quantity; id is optional (the backend assigns one). A line may also carry properties: an optional array of { name, value } custom fields you define (e.g. an engraving, a gift message, a monogram). The gateway never interprets them — it stores them on the line and echoes them on the Checkout and Order — so you own their meaning. Two lines of the same variant with different properties are distinct lines, not a quantity merge:
{
  "item": { "id": "var_ridge_32l" },
  "quantity": 2,
  "properties": [
    { "name": "Engraving", "value": "Best Dad Ever" },
    { "name": "Gift wrap", "value": "Yes" },
  ],
}
Buyer + optional BillingAddresssetBuyer. A buyer email is what later unlocks completion:
{
  "buyer": {
    "email": "alice@example.com",
    "full_name": "Alice Smith",
    "phone_number": "+447911123456",
  },
  "billing_address": {
    "full_name": "Alice Smith",
    "street_address": "42 Baker Street",
    "address_locality": "London",
    "postal_code": "NW1 6XE",
    "address_country": "GB",
  },
}
ShippingDestinationsetFulfillment’s shipping_address (required for shipping methods; see Fulfillment):
{
  "full_name": "Alice Smith",
  "street_address": "10 Test Street",
  "address_locality": "London",
  "postal_code": "EC1A 1AA",
  "address_country": "GB"
}
SearchCatalogInputsearchProducts, all fields optional — see Catalog search.

Outputs

Checkout — returned by createCheckout and every checkout mutation. Returned line_items are enriched (each item carries price/title/image_url and a per-line totals) — unlike the bare LineItem you send in. totals is the cart-level breakdown; include each row that applies (subtotal, discount, fulfillment, tax, total):
{
  "id": "ck_abc123",
  "currency": "GBP",
  "status": "ready_for_complete", // see Checkout lifecycle below
  "line_items": [
    {
      "id": "li_1",
      "item": {
        "id": "var_ridge_32l",
        "price": 12900,
        "title": "Ridge Backpack (32L)",
        "image_url": "https://cdn.example.com/ridge.png",
      },
      "quantity": 2,
      "totals": [{ "type": "subtotal", "amount": 25800 }],
    },
  ],
  "totals": [
    { "type": "subtotal", "amount": 25800 },
    { "type": "discount", "amount": 1000, "display_text": "WELCOME10" },
    {
      "type": "fulfillment",
      "amount": 500,
      "display_text": "Standard shipping",
    },
    { "type": "tax", "amount": 1984 },
    { "type": "total", "amount": 27284 },
  ],
  "buyer": { "email": "alice@example.com", "full_name": "Alice Smith" },
  "payment": { "handlers": [], "instruments": [] },
}
Order — returned by completeCheckout and getOrder. Like a Checkout plus a checkout_id, a permalink_url, a created_at placement timestamp, an optional display_id, an optional order-level status and payment_status, per-line fulfillment status + quantity tracking, a fulfillment block, and optional messages. display_id is the short, customer-facing order reference (use for display only, not for lookups). The order status is processing, completed, or cancelled; payment_status is pending, authorized, paid, partially_refunded, refunded, or failed — omit either when you do not track it. A line’s status is processing, partial, or fulfilled. fulfillment holds two lists. expectations is the plan — one entry per shipment, each carrying how it ships (method: { id, name, method_type }) and its destination (where the goods physically go); split an order into several expectations when its lines go out separately. destination is the shopper’s address for shipping and the collection point’s address (store or carrier locker) for pickup, and is {} only for digital; its optional instructions is free text for the shopper about the destination (e.g. “InPost locker no. 345”, “Click & collect in store”). events is the history — milestones that have already happened (dispatched, delivered, …), each with an occurred_at and, once handed to a carrier, carrier / tracking_number / tracking_url. There is no fulfillment charge inside this block — it is a line in totals.
{
  "id": "ord_def456",
  "display_id": "ORD_1042",
  "created_at": "2026-06-23T10:15:00Z",
  "checkout_id": "ck_abc123",
  "currency": "GBP",
  "permalink_url": "https://shop.example.com/orders/ord_def456",
  "status": "processing",
  "payment_status": "paid",
  "line_items": [
    {
      "id": "li_1",
      "status": "processing",
      "quantity": { "total": 2, "fulfilled": 0 },
      "item": {
        "id": "var_ridge_32l",
        "price": 12900,
        "title": "Ridge Backpack (32L)",
      },
      "totals": [{ "type": "subtotal", "amount": 25800 }],
    },
  ],
  "totals": [
    { "type": "subtotal", "amount": 25800 },
    { "type": "fulfillment", "amount": 500 },
    { "type": "total", "amount": 27284 },
  ],
  "fulfillment": {
    "expectations": [
      {
        "id": "fe_ord_def456",
        "method": {
          "id": "standard",
          "name": "Standard shipping",
          "method_type": "shipping",
        },
        "line_items": [{ "id": "li_1", "quantity": 2 }],
        "destination": {
          "full_name": "Alice Smith",
          "street_address": "10 Test Street",
          "address_locality": "London",
          "postal_code": "EC1A 1AA",
          "address_country": "GB",
        },
        "fulfillable_on": "2026-06-25",
      },
    ],
    "events": [
      {
        "id": "fev_1",
        "type": "dispatched",
        "occurred_at": "2026-06-25T09:00:00Z",
        "line_items": [{ "id": "li_1", "quantity": 2 }],
        "carrier": "Royal Mail",
        "tracking_number": "RM123456789GB",
      },
    ],
  },
  "messages": [
    {
      "id": "msg_1",
      "body": "Your order has shipped.",
      "created_at": "2026-06-25T09:01:00Z",
    },
  ],
}
A pickup expectation puts the collection point’s address in destination, with instructions telling the shopper where to go:
{
  "id": "fe_ord_ghi789",
  "method": { "id": "locker", "name": "InPost locker", "method_type": "pickup" },
  "line_items": [{ "id": "li_1", "quantity": 1 }],
  "destination": {
    "street_address": "12 Old Street",
    "address_locality": "London",
    "postal_code": "EC1V 9BD",
    "address_country": "GB",
    "instructions": "InPost locker no. 345",
  },
}
OrderPage — returned by listOrders: { "orders": Order[], "pagination"?: { … } } — see Pagination. Product — returned by getProduct and inside ProductPage.products. A product groups one or more purchasable Variants; a variant’s id is what goes in a LineItem’s item.id. Product-level media is the optional gallery / fallback; set a variant’s own media when variants differ visually (e.g. colorways) so agents can render the chosen variant. The gateway flattens a top-level image_url onto the product (its first image-typed media) and onto each variant (the variant’s first image, falling back to the product’s) in the response it serves to agents, so you normally leave image_url unset; if you do set it, the gateway keeps your value verbatim as an override — and an image-less product can omit media entirely:
{
  "id": "prod_ridge",
  "title": "Ridge Backpack",
  "brand": "Ridge", // optional
  "description": "32L technical daypack.",
  "media": [{ "type": "image", "url": "https://cdn.example.com/ridge.png" }], // optional
  "categories": ["bags"],
  "url": "https://shop.example.com/products/ridge-backpack", // optional
  "price_range": {
    "min": { "amount": 12900, "currency": "GBP" },
    "max": { "amount": 12900, "currency": "GBP" },
  },
  "variants": [
    {
      "id": "var_ridge_32l",
      "product_id": "prod_ridge",
      "title": "32L",
      "sku": "RB-32",
      "price": { "amount": 12900, "currency": "GBP" },
      "availability": { "available": true },
      "media": [
        { "type": "image", "url": "https://cdn.example.com/ridge-32l.png" },
      ], // optional
      "url": "https://shop.example.com/products/ridge-backpack?variant=32l", // optional
    },
  ],
}
ProductPage — returned by searchProducts: { "products": Product[], "pagination"?: { … } } — see Pagination. Category — the element type of listCategories: { "value": string, "count"?: integer }. value is a category label usable verbatim in SearchCatalogInput.filters.categories; the optional count is store-global (every product carrying the category), never scoped to a search. Return the store’s whole vocabulary so an agent can discover what categories exist and recover from a search that matched nothing. FulfillmentMethod — the element type of listFulfillmentMethods — see Fulfillment.

Checkout lifecycle

A checkout carries a status that moves through three states:
  • incomplete — the state on createCheckout, and any time the checkout is missing something completion needs.
  • ready_for_complete — reached once the checkout has all of: at least one line item, a buyer with an email (setBuyer), and a fulfillment method (setFulfillment).
  • completed — set by completeCheckout; the checkout is now an order and no longer mutable.
Recompute status on every mutation: it is ready_for_complete exactly when the three conditions hold, otherwise incomplete — e.g. removing the last line item drops a checkout back to incomplete. completeCheckout succeeds only from ready_for_complete, returning the Order. It rejects:
  • an empty cart with checkout_empty,
  • a cart still missing a buyer email or fulfillment method with checkout_not_ready,
  • an already-completed cart with checkout_not_open.

Cart mutations

  • addLineItems merges by item.id and properties: adding an item already in the cart with the same properties increases that line’s quantity; the same variant with different properties becomes a separate line.
  • updateLineItem with quantity: 0 removes the line — equivalent to removeLineItem.
searchProducts takes { input: SearchCatalogInput }, where every field of SearchCatalogInput is optional — an empty input returns the first page of the whole catalog:
{
  "query": "running shoes", // optional free-text search
  "filters": {
    // optional
    "categories": ["footwear"], // OR logic across the list
    "price": { "min": 5000, "max": 20000 }, // inclusive, integer minor units
  },
  "pagination": { "cursor": "…", "limit": 20 }, // optional; limit is clamped to ≤ 100
}
The merchant SHOULD honor query and filters; a merchant that cannot run a given filter MAY ignore it, but SHOULD NOT error.

Pagination

searchProducts returns a ProductPage ({ "products": Product[], "pagination"?: { … } }) and listOrders returns an OrderPage ({ "orders": Order[], "pagination"?: { … } }) — neither is a bare array. Both share one cursor-based, caller-driven convention:
  • The merchant chooses the cursor encoding (offset, keyset, base64 JSON — whatever fits the underlying store). The gateway treats it as an opaque blob: it never inspects or generates the value.
  • When more pages remain, the merchant returns pagination: { "has_next_page": true, "cursor": "<token>" }. cursor MUST be present whenever has_next_page is true.
  • On the last page the merchant either omits pagination entirely or returns pagination: { "has_next_page": false }. total_count MAY be included on any page.
  • The caller passes the returned cursor straight back as input.pagination.cursor on the next call; the gateway forwards it unchanged. input.pagination.limit must be ≤ 100 — the gateway rejects a larger value before the call reaches the merchant.
  • The gateway does not auto-paginate — each call returns exactly one page and the caller drives the loop.
For listOrders, page on top of the owner-scoping: resolve the caller’s customer from the request’s context first, then page that customer’s orders.

Fulfillment

listFulfillmentMethods returns the methods selectable for a checkout — each a FulfillmentMethod ({ id, name, description, amount, currency, method_type }), where amount is integer minor units and method_type is shipping, pickup, or digital. setFulfillment accepts an optional shipping_address (a ShippingDestination — a postal address). When the chosen method’s method_type is shipping, a deliverable destination is required: street_address, address_locality, postal_code, and address_country (ISO 3166-1 alpha-2, e.g. GB, US) must all be present. Reject a missing or incomplete address with fulfillment_address_required (400). pickup and digital methods carry no address.

Customer sign-in

requestAuthentication and verifyAuthentication let a customer sign in mid-conversation, so an agent can recover from authentication_required (see Authorizing owner-scoped reads) without leaving the chat:
  1. requestAuthentication{ email: string }. Deliver a one-time code out of band (e.g. to the customer’s verified email) and return an AuthChallenge: { "challenge_id": "<opaque>" }. You own the id encoding and where the pending challenge lives.
  2. The customer reads the code and gives it to the agent.
  3. verifyAuthentication{ challenge_id: string, code: string }. Check the code against the challenge and return an AuthSession: { "session_token": "<opaque>", "expires_at"?: "<ISO 8601>" }. Reject a wrong code with your own 4xx (e.g. invalid_code, 401).
  4. The agent sends the token on the customer’s later calls; it reaches you as context.session_token, where your verifySession hook authenticates it — exactly as a token provisioned outside the conversation would.
Both methods are mutating (sending a code and minting a token are side effects), so they carry an idempotency key (§5). Security is yours to own; the gateway forwards, it never inspects:
  • Don’t leak which emails have accounts. Respond to an unknown email exactly as to a known one — return a challenge_id and simply skip the send. A distinct error is an account-enumeration oracle.
  • Rate-limit code requests per email and per caller.
  • Cap attempts and expire fast. A handful of wrong codes should kill the challenge; a challenge should live minutes, not hours. Make challenges single-use.
  • Scope the token down. Both the code and the token travel through the agent conversation, so mint a short-lived token good for the least number of required features only.

7. Error codes

You own your error codes. Emit any error.code with a 4xx error.status and the gateway forwards it to the agent verbatimcode, status, and detail unchanged. Pick codes your agents can act on.
{
  "error": {
    "code": "store_closed",
    "status": 403,
    "detail": "The store is closed for the holiday."
  }
}
The agent sees the detail string (falling back to the code when you omit detail), so write detail for a human reader and never put secrets or PII in it. Gateway-owned codes. Any 5xx handling is on the VaultGraph gateway side (not the merchant side): a 5xx error.status — or a missing/out-of-range one — is normalized to commerce_backend_unavailable (HTTP 502). The same applies to handshake/dispatch codes (invalid_signature, unsupported_protocol, unknown_method, idempotency_key_required, not_implemented, internal_error) at any status — the SDK raises these around dispatch, not as a business answer — and to a response with neither data nor error, a non-JSON body, a transport failure, or a timeout (§4, §8). On all of these the gateway also drops your detail and substitutes a generic message — a normalized error’s detail is diagnostic (often an exception dump), not an agent-facing message, so only a deliberate 4xx detail reaches the agent.

8. Timeout & retry

The gateway defaults:
  • Per-attempt timeout: 10 seconds (configurable per backend in the portal).
  • Retries: up to 2 (configurable per backend in the portal), with exponential backoff starting at 100 ms.
Retries fire on:
  • network errors (DNS, TCP, TLS, socket close mid-response),
  • response timeouts (the request aborted before headers),
  • HTTP 5xx without a recognized error envelope.
Retries do not fire on a well-formed error envelope — a 4xx is forwarded verbatim and a 5xx normalizes to commerce_backend_unavailable, both deterministic answers. They also do not fire on 4xx without an envelope (treated as a 502 once, then surfaced). Because every mutating method carries an X-VaultGraph-Idempotency-Key, the merchant can safely dedupe. Reads are naturally idempotent.

9. Versioning

  • Envelope version is the protocol field plus the X-VaultGraph-Version header. Additive payload changes (new methods, new optional fields on data) ship under the same envelope.
  • Backwards-incompatible envelope changes will bump the major version (1.02.0) and use a new header value. The gateway will continue to send 1.0 to backends that have not opted in.
  • Merchants that don’t implement a method may return {"error":{"code":"not_implemented","status":501}} — the gateway surfaces it as commerce_backend_unavailable. The @vaultgraph/sdk/adapter helper emits this envelope automatically for any method you don’t supply, so you can ship a partial implementation and grow into the surface incrementally.

10. Capability discovery

The gateway discovers which §6 methods you implement by calling a reserved capabilities method, then advertises only the matching tools to agents. Methods you don’t report stay hidden until you implement them and the set is re-probed (re-saving the backend, or the refresh control on its settings page). capabilities is a read: same envelope and signing as §2–§3, carrying no idempotency key. Request to {endpoint_url}/capabilities:
{ "protocol": "1.0", "method": "capabilities", "params": {} }
Response data:
{ "methods": ["searchProducts", "getProduct", "createCheckout"] }
methods is the subset of §6 method names your endpoint serves. Names the gateway doesn’t recognize are ignored. The @vaultgraph/sdk/adapter handler answers capabilities automatically by reporting exactly the methods you supplied — you do not implement it yourself. If you hand-roll the wire contract, you MUST respond to capabilities: an endpoint that rejects it (e.g. with unknown_method) reports no methods, so the gateway advertises no tools.

11. Reference implementation

The @vaultgraph/sdk/adapter helper wraps signing, replay rejection, dispatch, idempotency caching, and error serialization in a single typed factory:
import { createAdapterHandler } from "@vaultgraph/sdk/adapter";

const handler = createAdapterHandler({
  signingSecret: process.env.VAULTGRAPH_SIGNING_SECRET!,
  idempotencyStore: kvBackedStore,

  // Authenticate the caller once per request. Verify `context.session_token`
  // and return a typed identity (any shape) or `null` for an anonymous caller.
  // The result is handed to every method as its final `session` argument.
  // Throw to reject the request outright.
  verifySession: (context) => verifySessionToken(context?.session_token),

  async searchProducts({ query, filters, pagination }) {
    /* ... */
  },
  async getProduct(id) {
    /* ... */
  },
  async createCheckout(input) {
    /* ... */
  },
  async getOrder(id, _context, session) {
    // Owner-scoped read: return the order only when the verified caller owns
    // it. Throw the same `order_not_found` for a missing OR non-owned id so
    // existence isn't leaked to a non-owner.
    const order = session ? await orderById(id) : null;
    if (!order || order.customerId !== session.customerId)
      throw { code: "order_not_found", status: 404 };
    return order;
  },
  async listOrders(input, _context, session) {
    // The verified `session` from `verifySession` is the final argument of
    // every method; scope the read to it. Reject an anonymous caller with
    // `authentication_required` (401) so the agent prompts the user to sign in,
    // rather than an empty page that reads as "you have no orders".
    // `input.pagination` carries the optional cursor/limit.
    if (!session)
      throw {
        code: "authentication_required",
        status: 401,
        detail: "Sign in to view your orders.",
      };
    return ordersForCustomer(session.customerId, input?.pagination);
  },
  // ...one method per row in §6 — every method is optional, anything you
  // skip auto-responds with `not_implemented` (501).
});

// Plug into your HTTP framework — handler is just (req) => res.
See @vaultgraph/sdk on npm for the full surface.