Embedded routing

Provision routing, keys, budgets, and usage for each of your end customers

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. Add Content-Type: application/json on any request with a JSON body (POST and PATCH). No other headers are required.
  • Ids are Merge UUIDs. Timestamps are created_at and modified_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_id field.

Data models

Customer

FieldDescriptionType
idMerge UUID. Use as end_user_id at request time.string
origin_idYour unique id for the customer, immutablestring
nameCustomer namestring
routing_policiesThe customer’s routing policy idslist of ids
budgetsThe customer’s budget idslist of ids
byok_configurationsThe customer’s provider key idslist of ids
key_usage_defaultUSE_ANY, PREFER_BYOK, or BYOK_ONLYenum
statusACTIVE or INACTIVE. An inactive customer’s requests are rejectedenum
created_at, modified_atTimestampsdatetime

Routing policy

Path: /v1/customers/{customer_id}/routing-policies.

FieldDescriptionType
idMerge UUIDstring
namePolicy namestring
strategyPRIORITY or INTELLIGENTenum
is_defaultUsed when a request names no explicit policyboolean
is_activeWhether the policy is usable; deactivate to stop routing through it without deleting itboolean
allowed_providersProvider slugs (INTELLIGENT only, else null)list
allowed_modelsModel slugs (INTELLIGENT only, else null)list
priority_orderOrdered fallback models (PRIORITY only, else null)list of {model, priority}
created_at, modified_atTimestampsdatetime

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.

FieldDescriptionType
idMerge UUIDstring
vendorProvider slug, e.g. openaistring
key_ownershipThe resolved key’s owner: CUSTOMER, ORGANIZATION, or MERGE. Null under BYOK_ONLY when no customer or org key exists (no usable key).enum, nullable
created_at, modified_atTimestampsdatetime

Budget

Path: /v1/customers/{customer_id}/budgets. One budget per customer today.

FieldDescriptionType
idMerge UUIDstring
spending_limitLimit in USDnumber
reset_periodDAILY, WEEKLY, MONTHLY, QUARTERLY, or YEARLYenum
spending_limit_typeHARD blocks over-budget requests, SOFT alerts onlyenum
created_at, modified_atTimestampsdatetime

Usage

Path: /v1/customers/{customer_id}/usage. A spend report for a date range.

FieldDescriptionType
total_spendTotal spend for the rangenumber
byok_spendSpend billed against a bring-your-own key (customer or org)number
customer_byok_spendBYOK spend on the customer’s own keynumber
org_byok_spendBYOK spend on your organization’s keynumber
managed_spendSpend billed against Merge-managed credentialsnumber
request_countNumber of requestsinteger
by_model / by_provider / by_routing_policySpend breakdownslist of {…, spend}

Provisioning a customer

Create the customer, give it a routing policy, add a provider key, then set a budget.

Create a customer

cURL
$curl -X POST https://api-gateway.merge.dev/v1/customers \
> -H "Authorization: Bearer mg_<your_production_key>" \
> -H "Content-Type: application/json" \
> -d '{ "name": "Acme", "origin_id": "acme-corp" }'

Add a routing policy

PRIORITY tries priority_order in order; INTELLIGENT picks per request from the allowed set.

$curl -X POST https://api-gateway.merge.dev/v1/customers/{customer_id}/routing-policies \
> -H "Authorization: Bearer mg_<your_production_key>" \
> -H "Content-Type: application/json" \
> -d '{
> "name": "workflow-a",
> "strategy": "PRIORITY",
> "is_default": true,
> "priority_order": [
> { "model": "openai/gpt-5.5", "priority": 1 },
> { "model": "anthropic/claude-opus-4-8", "priority": 2 }
> ]
> }'

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.

Activate or deactivate
$curl -X PATCH https://api-gateway.merge.dev/v1/customers/{customer_id}/routing-policies/{policy_id} \
> -H "Authorization: Bearer mg_<your_production_key>" \
> -H "Content-Type: application/json" \
> -d '{ "is_active": false }'

Add a provider key

cURL
$curl -X POST https://api-gateway.merge.dev/v1/customers/{customer_id}/keys \
> -H "Authorization: Bearer mg_<your_production_key>" \
> -H "Content-Type: application/json" \
> -d '{ "vendor": "openai", "api_key": "sk-..." }'

Set a budget

cURL
$curl -X POST https://api-gateway.merge.dev/v1/customers/{customer_id}/budgets \
> -H "Authorization: Bearer mg_<your_production_key>" \
> -H "Content-Type: application/json" \
> -d '{ "spending_limit": 50, "reset_period": "MONTHLY", "spending_limit_type": "HARD" }'

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.

Request bodyRoutes via
end_user_id, no modelThe customer’s default policy
end_user_id and routing_policy_id, no modelThat specific policy
end_user_id 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" }],
> "end_user_id": "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 fields from the Usage model, including the customer_byok_spend, org_byok_spend, and managed_spend split.

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>"

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.

$curl -X PATCH https://api-gateway.merge.dev/v1/customers/{customer_id} \
> -H "Authorization: Bearer mg_<your_production_key>" \
> -H "Content-Type: application/json" \
> -d '{ "status": "INACTIVE" }'

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 end_user_id behaves exactly as before.
400invalid_request_errorMalformed request. Also when you send end_user_id 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 end_user_id (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 end_user_id.
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 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:

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