Embedded routing
Embedded routing 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 it and its sub-resources under /v1/customers, and you attach it to an LLM request with the end_user_id field. Everything is scoped to your organization automatically, because the organization is derived from your key.
Conventions
- Base URL
https://api-gateway.merge.dev. - Headers Send
Authorization: Bearer mg_<your_production_key>on every request. AddContent-Type: application/jsonon any request with a JSON body (POST and PATCH). No other headers are required. - Ids are Merge UUIDs. Timestamps are
created_atandmodified_at(ISO-8601). - Enums are UPPERCASE.
- Relations are returned as id lists (
routing_policies,budgets,byok_configurations). Fetch a relation’s own endpoint to read the full objects. - The organization comes from your key, so there is no
org_idfield.
Data models
Customer
Routing policy
Path: /v1/customers/{customer_id}/routing-policies.
Customer key
Path: /v1/customers/{customer_id}/keys. Lists every BYOK-capable vendor, and for each shows the resolved key: the customer’s own key if they have one, else your org’s key, else Merge-managed. Add a customer key (POST) to override a vendor to CUSTOMER.
Budget
Path: /v1/customers/{customer_id}/budgets. One budget per customer today.
Usage
Path: /v1/customers/{customer_id}/usage. A spend report for a date range.
Provisioning a customer
Create the customer, give it a routing policy, add a provider key, then set a budget.
Create a customer
Add a routing policy
PRIORITY tries priority_order in order; INTELLIGENT picks per request from the allowed set.
Deactivate a policy with PATCH to stop routing through it without deleting it, then reactivate it later. A policy that is the default cannot be set inactive.
Add a provider key
Set a budget
Sending requests
Send to /v1/responses as usual, plus end_user_id set to the customer’s id, which is the Merge UUID returned when you created the customer, 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.
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 fields from the Usage model, including the customer_byok_spend, org_byok_spend, and managed_spend split.
Managing the lifecycle
Deactivate a customer to stop serving its traffic without losing its configuration, then reactivate it later. Delete is irreversible, and a deleted customer’s id stays fail-closed.
Status codes
Request errors carry a machine-readable type (and, where useful, a code) so you can branch on them.
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 end_user_id) 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:
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.