§ GUIDE · REST API · v1

REST API integration guide.

Three HTTP calls to verified agent action. Step-by-step from API key provisioning to production cut-over — with copy-paste cURL, error envelopes, and a pre-ship checklist.

Audience · Backend engineers Time · ~30 minutes Stack · Any HTTP client

§ 01 · Prerequisites

Before the first call, you need a tenant and a set of credentials. Paphwey is provisioned per relying party — one tenant maps to one RP, one set of keys, one audit chain.

  • A Paphwey tenant and an X-API-Key with the agent:delegate scope.
  • Your RP identifier (the audience you will set on every challenge — e.g. merchant.example).
  • At least one policy attached to the tenant — created via the Paphwey admin or provisioned by your account manager.
  • A cryptographically-secure HTTP client with TLS 1.2+ and Host/SNI correctly set.
Never embed the API key in client code. All calls must originate from your backend. The key signs off on the agent's behalf — it is not bound to an end-user session.

§ 02 · Authentication

Every request carries an X-API-Key header. Production keys are prefixed sk_live_; sandbox tenants (RP is_sandbox=true) are issued sk_test_ keys against the same endpoints, so test traffic is visibly distinct from production at a glance. Rotate keys with a one-week overlap window — rotate before expiry, not after.

X-API-Key: sk_live_8f3a2b4c.deadbeefcafef00d1234567890abcdef
Content-Type: application/json
Accept: application/json
User-Agent: your-service/1.4.2 (+https://yourco.example)
Scopes on the key. The agent:delegate scope is required for create_delegation, present_delegation, and verify_outcome. Revocation-only services can use an agent:revoke key instead.

Sanity checks before you build

Two unauthenticated /authenticated probes confirm you are pointed at the right host with the right key — useful as a first call from a new integration.

# 1. Liveness (no auth, returns 200 if the gateway is up).
curl https://www.paphwey.com/healthz

# 2. Identify yourself (auth required, returns RP + scopes + policies).
curl https://www.paphwey.com/api/v1/whoami \
  -H "X-API-Key: sk_live_..."

/api/v1/whoami returns the relying party your key resolves to, the credential's scopes and expiry, the policy codes you may pass as challenge_type, and the canonical base URL. If your tenant is brand-new and policies.codes comes back empty, ping your account manager — Paphwey seeds HIGH_VALUE_PURCHASE_REQUIRED, AGE_VERIFICATION_REQUIRED, and LOGIN_ASSURANCE by default but the tenant's allowed_policy_codes may be empty.

§ 03 · Step 1 — Create a delegation

A delegation is a signed authority from the principal to a named agent, bounded by scopes, a spending ceiling, and an expiry. Creating one does not prompt the user — it only registers the authority.

POST /api/v1/agent/delegations

Request body

FieldTypeNotes
principal_emailstringOne of principal_email or principal_id is required. Matches the wallet-bound identity.
providerstringFree-form provider label (e.g. openai, anthropic, google, your own internal id). Recorded verbatim on the audit chain.
agent_idstringStable identifier for the agent binding (e.g. shopping-agent).
allowed_scopesstring[]Subset of the policy's permitted scopes.
allowed_challenge_typesstring[]The policy codes this delegation may exercise (e.g. HIGH_VALUE_PURCHASE_REQUIRED, AGE_VERIFICATION_REQUIRED). The string you pass here in §04 as challenge_type is the policy lookup key — it must match a policy code that exists on the tenant.
max_amount_minorintegerPer-action ceiling in minor units (pence / cents). Required for monetary scopes; pass 0 for non-monetary flows like age verification.
currencyISO-4217Three-letter currency code.
valid_untilRFC-3339UTC; inclusive. Must be ≤ tenant-max TTL.
metadataobjectOptional. Opaque key/value annotations retained on the audit record.

Example — cURL

curl -X POST https://www.paphwey.com/api/v1/agent/delegations \
  -H "X-API-Key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "principal_email": "alice@example.com",
    "provider": "openai",
    "agent_id": "shopping-agent",
    "allowed_scopes": ["purchase"],
    "allowed_challenge_types": ["HIGH_VALUE_PURCHASE_REQUIRED"],
    "max_amount_minor": 2500,
    "currency": "GBP",
    "valid_until": "2026-05-01T00:00:00Z",
    "metadata": {"channel": "web-chat", "order_hint": "gift"}
  }'

Response · 201 Created

{
  "delegation_id": "c5a3b2e1-7d4f-4e9a-b1c2-3d4e5f6a7b8c",
  "status": "active",
  "credential_jwt": "eyJhbGciOiJSUzI1NiIs...",
  "credential_kid": "plan3-abcd",
  "agent_did": "did:key:z6Mk...",
  "present_token": "eyJ... short-lived hint ...",
  "present_token_expires_at": "2026-04-18T09:25:13Z",
  "principal_type": "HUMAN_DELEGATED"
}
What you get back. credential_jwt is a W3C VC-JWT describing the delegation — verify it locally against the gateway JWKS if you don't trust Paphwey blindly. present_token is a 5-minute bearer hint that can be surfaced to the relying party before a challenge opens. There is no created_at, principal_did, or agent_provider in the response — query GET /api/v1/agent/delegations/{id} if you need the full envelope.

§ 04 · Step 2 — Present the delegation

Presenting opens a challenge bound to the delegation and the specific action. The response carries an approval_url — a single-use link your agent surfaces on the user's phone (via SMS, email, or the RP's own UI).

POST /api/v1/agent/delegations/{delegation_id}/challenge

Request body

FieldTypeNotes
challenge_typestringThe policy code to evaluate. Must match a policy active on the tenant and appear in the delegation's allowed_challenge_types. There is no fixed enum at the gateway level — what's "valid" depends entirely on the tenant's policy table.
audiencestringYour RP identifier. Must match the audience you later verify against.
payload.action_contextobjectDeclarative description of the action (scope, amount_minor, currency, merchant).
payload.minimum_assuranceintegerMinimum NIST-aligned assurance level you will accept (0–30).
payload.agent_contextobjectOptional — auto-filled. If you omit it, Paphwey injects {"provider": delegation.provider, "agent_id": delegation.agent_id} from the delegation. Pass it explicitly only when you need to override.
noncestringOptional. Echoed in the verified token; use to bind a session to a specific challenge.
callback_urlURLOptional. Paphwey POSTs the outcome envelope here when the user approves. For local development, ngrok or a public tunnel is required — Paphwey cannot reach localhost; for those flows, omit callback_url and use the poll_url path described in §07.
agent_popstringRequired for key-bound delegations. JWS produced by the agent's bound key, asserting possession.

Example — cURL

curl -X POST \
  https://www.paphwey.com/api/v1/agent/delegations/dlg_01HZ3VJT1M8T8.../challenge \
  -H "X-API-Key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "challenge_type": "HIGH_VALUE_PURCHASE_REQUIRED",
    "audience": "merchant.example",
    "payload": {
      "action_context": {
        "scope": "purchase",
        "amount_minor": 1500,
        "currency": "GBP",
        "merchant": "acme.example"
      },
      "minimum_assurance": 10
    },
    "callback_url": "https://merchant.example/hooks/paphwey",
    "ttl_seconds": 900
  }'

Response · 201 Created

{
  "challenge_id": "a277c8e4-1234-4abc-b9d0-fedcba012345",
  "status": "AWAITING_APPROVAL",
  "approval_url": "https://www.paphwey.com/link-device/?challenge_id=a277c8e4-...&approval_id=ap_8b2...",
  "poll_url": "https://www.paphwey.com/api/v1/orchestration/challenges/a277c8e4-.../",
  "expires_at": "2026-04-18T09:35:13Z"
}
Two URLs, two jobs. approval_url is the user-facing link (open it on their phone, scan the QR). It hosts the Paphwey link-device flow — there is no separate wallet domain. poll_url points at /api/v1/orchestration/challenges/{id}/; poll it (1-second floor) until status flips to APPROVED or DENIED.

§ 05 · Step 3 — Verify the outcome

When the user approves, you receive an attestation token — a JWS containing the delegation ID, the audience, the action context, a key-binding confirmation (cnf.jwk), and the assurance score. Verify it on every request that depends on it.

POST /api/v1/agent/outcomes/verify

Request body

FieldTypeNotes
attestation_tokenJWSThe compact-serialized token the agent presents to your RP.
audiencestringMust match your RP identifier.
expected_delegation_idstringOptional but strongly recommended. Rejects replay across delegations.
expected_actionobjectOptional. Assert the scope / amount / currency you expect.

Example — cURL

curl -X POST https://www.paphwey.com/api/v1/agent/outcomes/verify \
  -H "X-API-Key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "attestation_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6I...",
    "audience": "merchant.example",
    "expected_delegation_id": "dlg_01HZ3VJT1M8T8...",
    "expected_action": {
      "scope": "purchase",
      "amount_minor": 1500,
      "currency": "GBP"
    }
  }'

Response · 200 OK

{
  "valid": true,
  "claims": {
    "sub": "principal-uuid",
    "challenge_id": "a277c8e4-...",
    "nonce": "session-nonce-123"
  },
  "delegation": {
    "id": "c5a3b2e1-...",
    "agent_did": "did:key:z6Mk...",
    "allowed_scopes": ["purchase"],
    "status": "active"
  },
  "actor_chain": {
    "principal_id": "principal-uuid",
    "agent": {"provider": "openai", "agent_id": "shopping-agent"}
  },
  "receipt_id": "rcpt-01HZ3VKZ...",
  "kyc_reference": ""
}
Read the claim, not the wrapper. The signed values live under claims (sub, challenge_id, nonce) and delegation (agent_did, allowed_scopes, status). The top-level valid only tells you Paphwey accepted the signature — your business logic still needs to check the actor_chain matches what you expected.

Offline verification (optional)

For latency-sensitive paths you can verify the JWS locally against the gateway JWKS. Fetch https://www.paphwey.com/.well-known/jwks.json and cache with a max-age of 5 minutes.

HEADER  { "alg": "ES256", "kid": "2026-04-key-1" }
PAYLOAD {
  "iss": "https://www.paphwey.com",
  "aud": "merchant.example",
  "sub": "did:web:paphwey.com:users:alice",
  "cnf": { "jwk": { "kty":"EC","crv":"P-256","x":"...","y":"..." } },
  "delegation_id": "dlg_...",
  "action_context": { "scope":"purchase", "amount_minor":1500, "currency":"GBP" },
  "assurance_score": 22,
  "iat": 1745000000,
  "exp": 1745000600
}

§ 06 · Revoke & query

Two management endpoints round out the contract. A daily snapshot of all revocations is also served at /api/v1/delegations/revocations.json for offline reconciliation.

GET /api/v1/agent/delegations/{delegation_id}

Returns the current status (active, revoked, expired) plus the full scope envelope.

POST /api/v1/agent/delegations/{delegation_id}/revoke
curl -X POST \
  https://www.paphwey.com/api/v1/agent/delegations/dlg_01HZ.../revoke \
  -H "X-API-Key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"reason": "user_request", "actor": "support:agent:42"}'
GET /api/v1/agent/policies

Lists the challenge types and scope envelopes enabled for your tenant. Read-only.

§ 07 · Webhooks & polling

Two delivery models — pick per environment. Callback URLs are preferred in production; polling is simpler in local development.

Callback payload

If you set callback_url, Paphwey POSTs a signed envelope to that URL within ~500 ms of user approval. The envelope is itself a JWS — treat it exactly like an attestation token and verify against the gateway JWKS before accepting.

POST https://merchant.example/hooks/paphwey
Content-Type: application/jose
Paphwey-Signature: t=1745000000,v1=eyJhbGciOiJFUzI1NiIs...
Paphwey-Delivery: dlv_01HZ3VL1...

eyJhbGciOiJFUzI1NiIsImtpZCI6IjIwMjYtMDQta2V5LTEifQ.eyJldmVudCI6Im91dGNvbWUuYXBwc...

Polling the challenge

Use the poll_url you got from §04 — it resolves to GET /api/v1/orchestration/challenges/{id}/ and is the canonical state source for the user's decision. Poll at a 1-second floor.

curl https://www.paphwey.com/api/v1/orchestration/challenges/a277c8e4-.../ \
  -H "X-API-Key: sk_live_..."

Status enum (uppercase, stable): PENDING, AWAITING_APPROVAL, APPROVED, DENIED, EXPIRED, CANCELLED, ISSUED. Treat anything other than APPROVED as a terminal "no go" once expires_at has passed.

PII. The challenge body may include the principal's email, the wallet's verified claims, and the merchant context you posted. Treat the whole payload as PII — do not log it raw, do not surface it in client UI beyond what the user already saw on their phone.
Idempotent delivery. Webhooks use at-least-once semantics. Dedupe on Paphwey-Delivery — not on the payload.

§ 08 · Error envelopes

All errors share one shape. No stack traces, no library-specific messages, no PII. The code is stable; the detail is human-readable and safe to log.

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json

{
  "error": {
    "code": "delegation.scope_exceeds_policy",
    "detail": "Requested scope 'purchase' exceeds tenant policy ceiling.",
    "trace_id": "01HZ3VKZ...",
    "hint": "Reduce max_amount_minor or raise the policy ceiling."
  }
}
HTTPCode prefixMeaning
400request.*Malformed JSON, missing fields, unknown enum value.
401auth.*Missing, invalid, or revoked API key.
403scope.*Key lacks the required scope, or tenant blocked.
404not_found.*Delegation, challenge, or policy not found.
409state.*Delegation already revoked, challenge already consumed.
422delegation.*Business-rule violation (scope/ceiling/TTL).
429rate_limit.*Rate limit tripped. See Retry-After.
5xxgateway.*Transient. Retry with exponential backoff + jitter.

§ 09 · Rate limits & retries

Rate limits are tenant-scoped. Default production tier is 120 req/s per endpoint; bursts up to 600 in a 10-second window. Responses always carry X-RateLimit-* headers.

  • Retry only on 429 or 5xx. Never retry 4xx.
  • Exponential backoff starting at 250 ms, cap 30 s, full jitter.
  • Use Idempotency-Key on retries. Send the same opaque key (e.g. a UUID per logical action) on the original call and every retry. Paphwey caches the response for 24 hours per (tenant, endpoint, key) tuple, so a network-flap retry returns the same delegation_id / challenge_id instead of creating a duplicate. Replays carry an Idempotency-Replayed: true response header.
  • Budget ~150 ms P50 for verify_outcome from UK/EU egress.

Idempotency-Key in detail

Send Idempotency-Key: <opaque> on POST /api/v1/agent/delegations and POST /api/v1/agent/delegations/{id}/challenge. Keys must be 1-255 printable-ASCII characters; a UUID is the conventional choice. The server's behaviour:

ScenarioServer response
Header absentEndpoint runs as normal. Idempotency-Replayed: false.
Header present, first time seen on this tenant + endpointEndpoint runs; success response is cached for 24h. Idempotency-Replayed: false.
Header present, same key + same bodyCached response is returned verbatim — same status, same body. Idempotency-Replayed: true. The underlying mutation does not run again.
Header present, same key + different body409 Conflict with error_code: idempotency_key_conflict. Pick a new key.
Header present, malformed (empty / control chars / >255 chars)400 with error_code: validation_error and details.idempotency_key.
Tenant isolation. The cache key is namespaced by your relying-party id. Two tenants that happen to pick the same opaque key never collide.
Failures are not cached. Only 2xx responses are stored. A 4xx or 5xx leaves the key available, so you can fix the request and retry with the same key — once it succeeds, that's the response future replays will see.

§ 10 · Pre-production checklist

  • API key is stored in a secrets manager, never in source or CI logs.
  • Sandbox keys (sk_test_…, issued for tenants with is_sandbox=true) never appear in production config. Confirm the active environment with GET /api/v1/whoami before going live.
  • Callback URL is served over TLS and verifies the signature header.
  • Verification re-runs against the gateway on every incoming agent request — no cached "valid=true".
  • Confirmation JWK is pinned for the session after first verification.
  • Retry policy covers 429 and 5xx with full jitter, and sends an Idempotency-Key on every create_delegation / present_delegation call so retries dedupe at the gateway.
  • Revocation snapshot is reconciled daily against your local cache.
Regulatory mapping. See the Compliance & Regulatory Mapping document for how these controls satisfy PSD2 SCA, UK MLR 2017, NIST SP 800-63, and NYDFS Part 500.

Questions

Stuck on a shape? Ping us.