Embedded Routing Stack

For platforms reselling Gateway: provision routing, keys, budgets, and usage for each of your end customers

The Embedded Routing Stack lets a platform built on Gateway give each of its own end customers their own routing policies, BYOK provider keys, budget, and usage, provisioned with your production key. Use it when you resell Gateway to your customers and need per-customer control without a dashboard seat for each one. A Customer is the object you manage under /v1/customers; you attach it to an LLM request with the customer field, and everything is scoped to your organization automatically because the organization is derived from your key.

All requests go to https://api-gateway.merge.dev with Authorization: Bearer mg_<your_production_key>, plus Content-Type: application/json on any request that has a JSON body. IDs are Merge UUIDs, timestamps are created_at and modified_at, and there is no org_id field.

Data models

Each object has its own reference page.

ModelWhat it is
CustomerAn end customer you provision and scope requests to. Owns the policies, keys, and budget below
Routing policyHow a customer’s requests pick a model: a ranked priority list or an intelligent strategy
Customer keyA customer’s own BYOK provider key, overriding your org key for that vendor
BudgetA per-customer spend limit, enforced hard or alerted soft
UsageA per-customer spend report over a date range

Sending requests

Send to /v1/responses as usual, plus a customer field set to the Customer’s id (the Merge UUID returned on create, not your own origin_id). Sending anything that isn’t a UUID is rejected with 422, and a well-formed UUID that matches no Customer is rejected with 404 (see Status codes). The Customer’s provider key, budget, and usage attribution then apply automatically.

To let a policy route, omit model. If you pin a model, that model is used directly and the policy (including the default) is bypassed. Budget, keys, and usage attribution still apply, only routing is skipped.

Request bodyRoutes via
customer, no modelThe customer’s default policy
customer and routing_policy_id, no modelThat specific policy
customer and modelThat model directly, policy bypassed
cURL
$curl -X POST https://api-gateway.merge.dev/v1/responses \
> -H "Authorization: Bearer mg_<your_production_key>" \
> -H "Content-Type: application/json" \
> -d '{
> "input": [{ "type": "message", "role": "user", "content": "hi" }],
> "customer": "9f8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
> "routing_policy_id": "2a1b3c4d-5e6f-4708-9a0b-1c2d3e4f5a6b"
> }'

Non-streaming responses echo which policy served the request in the x-merge-routing-policy-id response header. Add -i to your curl (or -D -) to include the response headers and see it.

Checking usage

Fetch a spend report for a date range. Pass start and end as YYYY-MM-DD. The response carries the Usage fields: the customer_byok_spend / organization_byok_spend / merge_spend split (their sum is the customer’s total) plus per-model, per-provider, and per-routing-policy breakdowns.

cURL
$curl -X GET "https://api-gateway.merge.dev/v1/customers/{customer_id}/usage?start=2026-01-01&end=2026-01-31" \
> -H "Authorization: Bearer mg_<your_production_key>"

Status codes

Request errors carry a machine-readable type (and, where useful, a code) so you can branch on them.

CodetypeWhen
200, 201Success. A request with no customer behaves exactly as before.
400invalid_request_errorMalformed request. Also when you send customer but embedded routing is not enabled for your organization.
401authentication_errorMissing or invalid production key.
402budget_exceededThe customer’s HARD budget is exhausted. Checked after status.
403permission_errorThe customer is inactive or deleted. Checked before budget.
404not_found_errorUnknown customer (never created, or deleted); the code is customer_not_found. A stale or mistyped ID is rejected here, not silently served with your organization’s defaults. Under BYOK_ONLY with no usable key, the code is instead provider_credentials_missing.
409Duplicate origin_id on create, or a second budget for a customer that already has one.
422Validation, such as a missing origin_id, an unknown enum, or a non-UUID customer.
503The customer’s configuration could not be read. Fail-closed, so retry.

Fail-closed by design: BYOK_ONLY never falls back to managed credentials, and a deactivated or deleted customer’s requests are rejected rather than silently served.

Errors on streaming requests

A streaming request ("stream": true) returns HTTP 200 the moment the stream opens, before the model runs. A rejection caught before then (inactive customer, exhausted budget, unknown customer) still returns the real HTTP status from the table above. A failure that surfaces after the stream has opened (no usable provider key, no route, or a provider fault mid-response) can no longer change the status code, so it arrives in-band as a terminal error frame:

data: {"object": "response.error", "error": {"type": "not_found_error", "code": "provider_credentials_missing", "status_code": "404", "message": "..."}}

Treat any frame whose object is response.error (or that carries a top-level error) as a failed stream and stop reading. A successful stream instead ends with a frame whose object is response.done. The error payload carries the same type, code, and status_code you would have received non-streamed, so you can branch on them the same way.

Next steps