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.

ANY-METHOD /gateway/<slug>/<sub-path>
Authorization: Bearer <credential>

For a step-by-step guide with curl examples, see Call a capability. This page documents the protocol in detail.

URL shape

/gateway/<capability-slug>/<everything-else>
  • <capability-slug> — exactly matches a registered capability’s slug.
  • <everything-else> — appended to the capability’s configured endpoint_url when 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:

  • Authorization
  • X-Payment-Authorization
  • Cookie
  • 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:

auth_typeInjected header
nonenothing
bearerAuthorization: Bearer <auth_value>
api_key<header_name>: <auth_value> (header name comes from provider_config.header_name, default X-API-Key)
basicAuthorization: Basic <base64(auth_value)>

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:

  1. X-Payment-* headers prepended to the response.
  2. Content-Length is recalculated if Lipafy buffered the upstream body (it usually doesn’t — streams pass through).

X-Payment-* headers

1X-Payment-Reference: cr_8f3a92...
2X-Payment-Amount: 500
3X-Payment-Currency: KES
4X-Payment-Receipt: https://api.lipafy.xyz/v1/receipts/cr_8f3a92...
  • X-Payment-Referencecapability_request id. 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)

StatuserrorCause
401unauthenticatedNo / invalid credential
401invalid_api_key / api_key_revoked / api_key_expiredAPI key issue
402approval_requiredSpending control escalation; retry with approval
402insufficient_balanceWallet under-funded
402daily_limit_exceeded / transaction_limit_exceededCaller’s controls
402capability_blocked / capability_not_allowedCaller’s allow/block list
404capability_not_foundSlug doesn’t exist
422capability_disabledSlug is draft / disabled
429rate_limitedLipafy global rate limit

During-forward failures (hold released, caller not charged)

StatuserrorCause
502upstream_errorUpstream 5xx, timeout, connection refused
502provider_errorLipafy couldn’t even reach the upstream (DNS, TLS)

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:

  1. Re-validates the request against the signed mandate terms (slug allow-list, merchant allow-list, per-fulfillment cap, total budget remaining, expiry).
  2. Forwards through the gateway internally on validation success.
  3. Records the call as a fulfillment row 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; the approval_id is consumed atomically. Re-using the approval errors with approval_used.
  • Mandates with max_fulfillments=1: the count is enforced at fulfill time; the second call errors mandate_exhausted.
  • Idempotency-Key forwarding: if your upstream supports its own Idempotency-Key header, 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.