Deployment onboarding
Partner (write-surface) access to a Merchant Binding deployment is granted by the merchant, not self-served: the deployment provisions a machine-to-machine OAuth client and seeds an agent row for you. This page documents the pattern; every concrete value below uses the spec's fictional merchant (Aurora Motors Group) — a real deployment publishes its own specifics.
1. Contact the merchant
Onboarding starts with the merchant's published partner contact — the
merchant.contact block of its /.well-known/ucp manifest (in the example
manifest: partners@auroramotors.example). Include your legal entity, a named technical
contact, your use case, and the scopes you need.
2. The credential bundle
The deployment delivers these values over a secure channel (a time-bounded secret share — never plain email or chat).
| Field | Value |
|---|---|
| Auth tenant | auth.auroramotors.example (from the deployment's RFC 9728 metadata) |
| Token endpoint | https://auth.auroramotors.example/oauth/token |
| Grant type | client_credentials |
| Audience | the deployment's resource identifier (RFC 9728 resource) |
| Signing alg | RS256 |
| Token TTL | 3600 s (1 hour) |
| client_id / client_secret | unique per partner — delivered securely |
Your sub at the service | <client_id>@clients |
| A2A endpoint | MCP_BASE/a2a/ — from the manifest, e.g. https://mcp.auroramotors.example/a2a/ |
3. Mint a token
A2A_BEARER=$(curl -s -X POST https://auth.auroramotors.example/oauth/token \
-H 'content-type: application/json' \
-d '{
"client_id": "<your client_id>",
"client_secret": "<your client_secret>",
"audience": "<the deployment resource identifier>",
"grant_type": "client_credentials"
}' | jq -r .access_token)
The authorization server returns a standard client-credentials token response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6…", // the RS256 JWT — send as Bearer
"expires_in": 3600, // seconds; re-mint before this elapses
"token_type": "Bearer",
"scope": "inventory:read quote:write reservation:write deal:handoff pricing:read"
}
| Field | Type | Description |
|---|---|---|
| access_token | string | The RS256 JWT. Send as Authorization: Bearer <access_token> on every /a2a/ call. |
| expires_in | int | Lifetime in seconds (3600). Cache and re-mint before it lapses. |
| token_type | string | Always Bearer. |
| scope | string | Space-delimited granted scopes. Your effective set at call time is this ∩ your agent row (see §5). |
Cache the token for most of its TTL and re-mint before expiry. Minting on every request will get you rate-limited at the authorization server.
4. Call a tool
curl -X POST https://mcp.auroramotors.example/a2a/ \
-H "Authorization: Bearer $A2A_BEARER" \
-H 'content-type: application/json' \
-d '{
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": { "name": "search_inventory",
"arguments": { "query": "2024 Outback AWD under 35k", "limit": 5 } }
}'
The dry-run that completes onboarding is exactly this call: you mint a token and hit search_inventory; the deployment confirms your trace_id in its audit log and flips your agent row to active = true.
5. The scope model
The default partner bundle is five scopes (negotiation-enabled deployments add
quote:negotiate). Deployments grant the minimum that covers the stated use case.
| Scope | Unlocks | PII? |
|---|---|---|
| inventory:read | search_inventory, get_vehicle_by_vin, list_inventory, check_availability | no |
| quote:write | request_quote | no (opaque buyer_ref) |
| reservation:write | create_reservation, release_reservation, get_reservation_status | no (opaque customer_ref) |
| deal:handoff | initiate_deal_handoff | yes — name, phone, email |
| pricing:read | get_pricing_disclosure (+ OTD on quotes) | no |
inventory:read
cannot reserve, even if your row grants reservation:write. Mint tokens with exactly the scopes
the session needs. An empty intersection returns NOT_AUTHORIZED.
6. Rate limits & audit
| Budget | Default | Ceiling (on request) |
|---|---|---|
| Requests / minute (burst) | 60 | 600 |
| Reservations / day | 5 | 100 |
| Audit retention | 90 days | 365 days (with agreement) |
Rate-limit denials come back in the envelope as RATE_LIMITED (retryable), not as a
JSON-RPC error. Every response carries _metadata.trace_id — log it; it is the
only way the merchant can correlate a complaint against its audit trail. Quote your trace_id
and any error_id in incident reports. The budgets above are the pattern's reference defaults —
each deployment publishes its own.
RATE_LIMITED as retryable: exponential backoff with jitter (e.g. ~1s, 2s,
4s … capped, plus a random fraction) and a sane max-attempt ceiling — don't hot-loop. Stay under your
per-minute burst rather than relying on retries. Note the reservation budget is separate from the rate limit:
create_reservation charges your daily reservation budget, and a
same-UTC-day release_reservation refunds that slot — so a held-then-released VIN
within the day costs nothing against the budget. See the error taxonomy for which
codes are retryable.
7. Lifecycle
- Rotate (planned): mint a new secret in the authorization server's dual-credential window and send the deployment the new fingerprint; the agent row updates with no downtime.
- Revoke (compromise): contact the merchant; the deployment revokes the client — effective within seconds.
- Deactivate (end of agreement): the deployment sets
active = false; existing tokens are cleanly rejected at the middleware.