Gateway
The gateway is the one piece of Lipafy that integrators must understand. Everything else — controls, mandates, approvals, receipts — is supporting infrastructure that this endpoint orchestrates.
For a step-by-step guide with curl examples, see Call a capability. This page documents the protocol in detail.
URL shape
<capability-slug>— exactly matches a registered capability’s slug.<everything-else>— appended to the capability’s configuredendpoint_urlwhen forwarding. Empty is fine (POST /gateway/<slug>works).
Multiple sub-paths under one capability are common — eats.local-lunch.order might accept /orders (place order), /orders/:id (status), /orders/:id/cancel. Each is one capability charge per call, all going to the same upstream base.
Methods
The route accepts GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS. The method is forwarded to the upstream unchanged.
OPTIONS is forwarded too — useful for upstreams that do real CORS preflight, though most Lipafy integrations are server-to-server and don’t need it.
Headers
Headers Lipafy strips before forwarding
The agent’s authentication is to Lipafy, not to the provider. We strip:
AuthorizationX-Payment-AuthorizationCookie- Hop-by-hop headers (
Connection,Keep-Alive,Proxy-*,TE,Trailer,Transfer-Encoding,Upgrade) - Lipafy’s own
X-Payment-*headers (in case clients echo them by mistake)
Headers Lipafy injects
Based on the capability’s configured provider_config.auth_type:
Plus a User-Agent: Lipafy-Gateway/<version> for upstream observability.
Headers the agent should pass through
Everything else is forwarded verbatim — Content-Type, Accept, If-Match, Idempotency-Key, custom upstream-specific headers, all unchanged.
Body
The request body is streamed through byte-for-byte. Lipafy does not parse JSON, does not validate against a schema, does not modify. Whatever the agent sends, the upstream receives.
Gateway requests use the server’s global Fastify body-size limit, which defaults to 1 MB.
Response
Lipafy returns the upstream’s response — status, headers, body — with two additions:
X-Payment-*headers prepended to the response.Content-Lengthis recalculated if Lipafy buffered the upstream body (it usually doesn’t — streams pass through).
X-Payment-* headers
X-Payment-Reference—capability_requestid. Stable; use as your idempotency anchor when correlating with the upstream’s response payload.X-Payment-Amount+X-Payment-Currency— what was charged. May differ from the capability’s listed price in future if pricing dimensions evolve (today they always match).X-Payment-Receipt— URL to fetch the signed receipt. See Receipts.
These headers are present only when Lipafy actually charged the caller — i.e. on 2xx upstream responses. A 4xx or 5xx from upstream releases the hold and returns the response without payment headers.
Failure cases (with error codes)
Lipafy can fail the request before forwarding (no money moved) or during forwarding (hold released).
Pre-forward failures (no charge)
During-forward failures (hold released, caller not charged)
Upstream 4xx (forwarded as-is, no charge)
If the upstream returns a 4xx (e.g. the agent sent bad input), Lipafy forwards the response unchanged — but no payment headers, and the hold is released. The provider’s “you sent invalid input” answer is the integrator’s signal; there’s no Lipafy-specific 4xx mapping.
Auth bypass for connected apps
If the credential is an OAuth access token (third-party connected app calling on behalf of a user), the same gateway URL works. The token’s oauth_grant_id claim binds the call to that user’s wallet and the grant’s controls. See Connected apps via OAuth.
Mandate-aware calls
For autonomous spending bounded by a signed mandate, don’t call the gateway directly. Call POST /v1/intent-mandates/:id/fulfill instead, which:
- Re-validates the request against the signed mandate terms (slug allow-list, merchant allow-list, per-fulfillment cap, total budget remaining, expiry).
- Forwards through the gateway internally on validation success.
- Records the call as a
fulfillmentrow linking the gateway request back to the mandate.
The benefit: the user’s signed AP2-style mandate provides cryptographic evidence of what they pre-authorized, even though the agent is calling autonomously. See Mandates.
Idempotency
The gateway is not idempotent out of the box — each call is a distinct charge. Patterns for at-most-once semantics:
- Approvals: above the escalation threshold, the gateway returns
approval_required. The agent submits the call once the approval is granted; theapproval_idis consumed atomically. Re-using the approval errors withapproval_used. - Mandates with
max_fulfillments=1: the count is enforced at fulfill time; the second call errorsmandate_exhausted. - Idempotency-Key forwarding: if your upstream supports its own
Idempotency-Keyheader, send it from the agent — it’s passed through unchanged. Lipafy doesn’t dedupe at its own layer; the upstream’s check is what saves you.
Timeouts
Default upstream timeout: 30 seconds. Configurable per capability via provider_config.timeout_ms (in milliseconds). If the upstream exceeds this, Lipafy returns 502 upstream_error and releases the hold.
Streaming responses
Large responses (server-sent events, file downloads) stream through unchanged. Lipafy doesn’t buffer the full body before returning to the agent. The hold is settled on the upstream’s final status (after stream close).
Internal: how the gateway figures out who’s calling
The auth plugin attaches req.auth at the onRequest hook based on Bearer / session cookie / OAuth token. By the time the gateway handler runs, the account, grant, and account_type are all known. Spending controls and approvals are evaluated against the grant (not the account) — the same account can have different limits for its dashboard session vs. each API key vs. each connected app.
