§ 01 · What you need
This walkthrough assumes you have already received your tenant's credentials. If you have not, ask your Paphwey contact for these three things — nothing else.
- An
X-API-Keywith theagent:delegatescope. Production keys are prefixedsk_live_…; sandbox tenants are issuedsk_test_…against the same endpoints. - The email of a principal you can approve on (your own phone is fine — the wallet flow is at
/link-device/). - A terminal with
curlon it.
https://www.paphwey.com. There is no
separate api. subdomain — the gateway
serves both the public site and the API from the
same canonical host.
§ 02 · Pre-flight (~30 seconds)
Before you mint anything, two probes confirm the
gateway is up and your key resolves to a tenant. Skip
these and you'll spend an hour debugging
permission_denied errors that turned out
to be wrong-key copy-paste.
1. Liveness — GET /healthz
No auth. 200 if the gateway is up.
$ curl -i https://www.paphwey.com/healthz
HTTP/1.1 200 OK
Content-Type: application/json
{"status": "live", "message": "Backend is responsive"}
2. Identify yourself — GET /api/v1/whoami
Auth required. Returns the relying party your key
resolves to, the credential's scopes, and — most
importantly — the policy codes you may pass
as challenge_type.
$ curl https://www.paphwey.com/api/v1/whoami \
-H "X-API-Key: sk_live_abcd.deadbeef..."
{
"relying_party": {
"id": "549ea1ac-b19f-4cf8-9691-f0a787582491",
"name": "InternalDemo",
"slug": "internal-demo",
"status": "active",
"is_sandbox": false,
"is_active": true
},
"credential": {
"prefix": "sk_live_abcd",
"name": "primary",
"scopes": ["agent:delegate", "challenge:create"],
"expires_at": null,
"last_used_at": "2026-04-25T08:42:00Z"
},
"environment": {"base_url": "https://www.paphwey.com"},
"policies": {
"count": 3,
"codes": [
"AGE_VERIFICATION_REQUIRED",
"HIGH_VALUE_PURCHASE_REQUIRED",
"LOGIN_ASSURANCE"
]
}
}
policies.codes is empty,
your tenant has an allowed_policy_codes
allowlist that filters out the global defaults — ping
your Paphwey contact to widen it. Don't waste time
guessing codes; the rest of the flow will fail with
policy_not_found until this is resolved.
§ 03 · Mint a delegation (~30 seconds)
A delegation is a signed authority you grant to a named agent — bounded by scope, a spending ceiling, and an expiry. Creating it does not prompt the user; it only registers the authority.
Use one of the policy codes you saw in
policies.codes above. We'll use
HIGH_VALUE_PURCHASE_REQUIRED — a
£25.00-ceiling purchase delegation valid for one
week.
$ curl -X POST https://www.paphwey.com/api/v1/agent/delegations \
-H "X-API-Key: sk_live_abcd.deadbeef..." \
-H "Content-Type: application/json" \
-d '{
"principal_email": "you@example.com",
"provider": "openai",
"agent_id": "quickstart-agent",
"allowed_scopes": ["purchase"],
"allowed_challenge_types": ["HIGH_VALUE_PURCHASE_REQUIRED"],
"max_amount_minor": 2500,
"currency": "GBP",
"valid_until": "2026-05-02T00:00:00Z"
}'
{
"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-25T09:30:00Z",
"principal_type": "HUMAN_DELEGATED"
}
Save delegation_id — you will reference
it on the next call. The other fields are useful for
audit and offline verification but are not needed
for the rest of this quickstart.
max_amount_minor.
For monetary scopes, this is your hard ceiling per
action, in minor units (pence / cents). For
non-monetary policies like
AGE_VERIFICATION_REQUIRED, pass
0.
§ 04 · Open a challenge (~30 seconds)
Now present the delegation against an
actual action. The response carries an
approval_url — a single-use link your
user opens on their phone.
$ DELEGATION_ID="c5a3b2e1-7d4f-4e9a-b1c2-3d4e5f6a7b8c"
$ curl -X POST \
https://www.paphwey.com/api/v1/agent/delegations/$DELEGATION_ID/challenge \
-H "X-API-Key: sk_live_abcd.deadbeef..." \
-H "Content-Type: application/json" \
-d '{
"challenge_type": "HIGH_VALUE_PURCHASE_REQUIRED",
"audience": "quickstart.example",
"nonce": "first-call-nonce",
"payload": {
"action_context": {
"scope": "purchase",
"amount_minor": 1500,
"currency": "GBP"
},
"minimum_assurance": 10
}
}'
{
"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-25T09:35:00Z"
}
approval_url is the user-facing link
— open it on a phone or render a QR. The Paphwey
link-device flow lives on the same host, so there
is no extra wallet domain to whitelist.
poll_url is your machine-facing
endpoint — poll it (1-second floor) until status
flips.
Save the approval_url and
poll_url for the next two steps.
§ 05 · Approve on the phone (~60 seconds)
Open approval_url on your phone — or
paste it into a second browser tab if that's quicker
for the first run-through. You'll see the Paphwey
link-device page asking the principal to approve the
action.
Meanwhile, your backend polls
poll_url. The status flow is:
AWAITING_APPROVAL— initial state, waiting for the user.APPROVED— terminal happy-path. Response carries theattestation_token.DENIED— the user explicitly declined.EXPIRED— TTL elapsed before approval.
$ POLL_URL="https://www.paphwey.com/api/v1/orchestration/challenges/a277c8e4-.../"
$ curl $POLL_URL -H "X-API-Key: sk_live_abcd.deadbeef..."
# Before approval:
{"challenge_id": "a277...", "status": "AWAITING_APPROVAL", ...}
# After you tap "Approve" on the phone:
{
"challenge_id": "a277...",
"status": "APPROVED",
"attestation_token": "eyJhbGciOiJSUzI1NiIs..."
}
callback_url in the request above —
Paphwey can't reach your laptop. Polling is the
correct shape for development. In production, set
callback_url on the challenge and have
Paphwey POST the outcome envelope to a public
endpoint.
§ 06 · Verify the outcome (~30 seconds)
The attestation_token you got back is a
JWS — never trust its claims without re-verifying
against the gateway. The
/api/v1/agent/outcomes/verify endpoint
does the verification for you and returns a clean,
structured envelope.
$ TOKEN="eyJhbGciOiJSUzI1NiIs..."
$ curl -X POST https://www.paphwey.com/api/v1/agent/outcomes/verify \
-H "X-API-Key: sk_live_abcd.deadbeef..." \
-H "Content-Type: application/json" \
-d "{
\"attestation_token\": \"$TOKEN\",
\"audience\": \"quickstart.example\",
\"expected_delegation_id\": \"$DELEGATION_ID\",
\"expected_nonce\": \"first-call-nonce\"
}"
{
"valid": true,
"claims": {
"sub": "principal-uuid",
"challenge_id": "a277...",
"nonce": "first-call-nonce"
},
"delegation": {
"id": "c5a3...",
"agent_did": "did:key:...",
"allowed_scopes": ["purchase"],
"status": "active"
},
"actor_chain": {
"principal_id": "...",
"agent": {"provider": "openai", "agent_id": "quickstart-agent"}
},
"receipt_id": "rcpt-...",
"kyc_reference": ""
}
valid field is your gate.
If it's true, the principal approved
the action under the delegation, and the audience /
nonce / delegation match what you expected. If it's
false, reject the action — the body
carries an error_code describing why.
That's the complete loop. Probe → Mint → Approve → Verify. Everything else in the Paphwey API is composition over these four shapes.
§ 07 · Where it goes wrong
The five errors first-time integrators hit, and what each one really means.
| error_code | What it actually means | Fix |
|---|---|---|
authentication_failed401 |
Bad or missing X-API-Key. The header is wrong, the key was rotated, or you copied the truncated prefix instead of the full prefix.secret. |
Re-run GET /api/v1/whoami with the same key — if that 401s, the key itself is bad. |
permission_denied403 |
The credential authenticates but lacks the agent:delegate scope. |
Check credential.scopes in the whoami response. Ask your account manager to add the missing scope. |
policy_not_found400 |
The challenge_type string you passed has no matching active policy on the tenant. |
Check policies.codes in whoami. The exact string must appear there. Codes are case-sensitive. |
policy_requirement_not_met400 |
The policy is found, but the request doesn't satisfy it — usually a missing scope on the delegation, or a minimum_assurance below the policy floor. |
Compare your delegation's allowed_scopes with what the policy requires. Bump minimum_assurance if the response details mention assurance. |
validation_error400 |
Field is missing, malformed, or fails an expected-vs-actual check (e.g. audience mismatch on verify). |
The response's details object lists the offending field. Most often: forgot audience on verify, or passed amount_minor as a decimal instead of integer minor units. |
Every error response carries a
correlation_id — quote it if you ask
Paphwey to debug a specific failure. Don't pattern-
match on the human-readable message;
key off error_code.
§ 08 · Where to go next
You've completed the loop. Here's where to go for the production wiring.
Full REST guide
Webhooks, key-bound delegations, revocation, error envelopes, pre-ship checklist.
Python SDK
Typed sync + async clients, FastAPI / Django / Celery patterns.
Web SDK
Server-side TypeScript for Node, Next.js, Bun, Edge.
MCP server
Drop the paphwey-mcp tool set into Claude Desktop or any MCP client.
Swagger UI
Live, machine-readable contract for every endpoint.
Agent API reference
One-page text reference for the triad — shapes, errors, rate limits.