UCP Security
This page covers how UCP requests are authenticated, verified, and protected.
Request authentication
UCP requests carry a set of standard headers. Firmly supports two caller-authentication schemes — RFC 9421 HTTP message signatures (agent signing) and a destination API key — plus the headers UCP uses for idempotency and tracing:
| Header | Purpose |
|---|---|
Signature |
RFC 9421 HTTP message signature (ES256) proving the request’s authenticity |
Signature-Input |
RFC 9421 signature metadata — covered components, keyid, created, and algorithm |
Content-Digest |
RFC 9530 SHA-256 digest of the request body (present on requests that carry a body) |
UCP-Agent |
Agent profile URL identifying the caller (e.g., profile="https://<agent-host>/ucp-agent") |
X-API-Key |
Destination API key (an alternative to agent signing) |
Idempotency-Key |
Prevents duplicate processing of retried mutations (see Idempotency) |
Request-Id |
Unique identifier for request tracing |
Firmly evaluates the API key first: if an X-API-Key is present it is validated and, on success, the request proceeds without signature verification. Otherwise the request is handled on the agent-signing path below.
Agent signature verification (RFC 9421)
When a request carries Signature and Signature-Input, Firmly verifies it per RFC 9421:
- Parse
UCP-Agentfor the caller’sprofileURL. A signature with noUCP-Agentheader is rejected with400 missing_agent_header— the profile URL is the only way to locate the signing keys. - Fetch the agent profile document from that HTTPS URL and read its
signing_keys(a JWKS). Profiles are cached at the edge (~1 hour), fetched with a 3-second timeout and a 64 KB size cap. A profile that cannot be fetched, is malformed, or is missingsigning_keysfails with502 agent_profile_fetch_failed. - Verify the ES256 (ECDSA P-256 / SHA-256) signature. The
keyidinSignature-Inputmust match a JWKkidin the profile.
Covered components:
| Component | When required |
|---|---|
@method, @authority, @path |
Always |
@query |
When the request URL has a query string |
ucp-agent |
When the UCP-Agent header is present |
idempotency-key |
On state-changing methods (POST/PUT/PATCH/DELETE) that send the header |
content-digest |
When the request carries a body — verified against the SHA-256 of the body |
A signature that fails any check is rejected with 401 agent_signature_invalid.
Destination API-key authentication
Destinations that integrate over an API key present it in X-API-Key (keys are prefixed fucp_live). Firmly resolves the merchant-and-destination configuration, hashes the presented key (SHA-256), and matches it against that destination’s active keys:
| Outcome | Response |
|---|---|
| A key is required for this merchant/destination but none was presented | 401 api_key_required |
| Key is malformed, unknown, inactive, or not valid for this merchant | 401 api_key_invalid |
| Key is configured for a different UCP version than the requested route | 401 api_key_invalid |
A merchant/destination can be configured to require an API key as its public default; in that case an unsigned, keyless request is rejected with 401 api_key_required.
Response signing
Firmly signs its own successful (2xx) responses with RFC 9421, setting Signature, Signature-Input, and Content-Digest. The signature uses an ES256 (ECDSA P-256) key whose public JWK is published in the discovery manifest’s signing_keys array (key id ucp-firmly-key-1), so callers can verify Firmly’s responses. Discovery (/.well-known/ucp) responses and streaming (text/event-stream) responses are not signed.
Domain validation
Before building a manifest or translating a call, Firmly resolves and validates the merchant domain:
| Step | Description |
|---|---|
| 1. Resolve domain | Discovery reads the merchant domain from the x-firmly-host header (or the domain query parameter); REST and MCP take it from the URL path (/domain/{domain}) |
| 2. Validate merchant | Load the shop configuration; an unknown merchant fails with 404 merchant_not_found |
| 3. Check UCP status | A merchant with UCP disabled (ucp_disabled) fails with 404 ucp_disabled |
Abuse prevention
| Measure | How it works |
|---|---|
| Agent signatures | RFC 9421 signatures prove request authenticity and integrity, with the body covered by content-digest |
| Idempotency | Duplicate mutations with the same Idempotency-Key return the cached response; a reused key with a different body returns 409 (see Idempotency) |
| Replay dedup | Handled at the idempotency layer rather than the signature layer — the verifier does not enforce a created freshness window beyond rejecting timestamps more than 24 hours in the future |
| Audit logging | Every request records the caller (agent profile or destination), request id, and outcome on the request trace |
Rate limits
Rate limiting is not currently enforced by the UCP service. Firmly-wide rate limits may apply as a platform policy; where enforced, thresholds are communicated during onboarding and exceeding them returns 429 Too Many Requests with a retry-after header.
Transport security
- HTTPS only (enforced at the edge)
- All communication encrypted in transit
- TLS 1.2+ required
Related
- UCP overview
- UCP checkout flow
- UCP implementation — service architecture, idempotency handling, signing keys
- Rate Limits — Firmly-wide rate limiting