API reference
Delegation
Operations under the delegation group, generated from the API description.
POST/v1/delegationsGET/v1/delegations/service-pubkeyGET/v1/delegations/service-pubkeysPOST/v1/delegations/verify
POST /v1/delegations
Sign an order-bound delegation token
Signs a payload under a key derived from the principal chain in the order given, and binds that key to the chain with the service identity.
The same names in another order produce a different key, a different chain_pubkey and a different binding. The root secret never leaves the server; the token carries everything a verifier needs.
Parameters
None.
Request body
chainarray of string- No description in the schema.
payloadobject- No description in the schema.
Example request
curl
curl -sS -X POST "https://api.afaprotocol.com/v1/delegations" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
-H "Content-Type: application/json" \
-d '{
"chain": [
"alice",
"bob",
"worker-1"
],
"payload": {
"expires_at": "2026-09-01T18:00:00+00:00",
"scope": "staging",
"task": "deploy"
}
}'
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {
"chain": [
"alice",
"bob",
"worker-1"
],
"payload": {
"expires_at": "2026-09-01T18:00:00+00:00",
"scope": "staging",
"task": "deploy"
}
}
r = requests.post(f"{API}/v1/delegations", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"token": {
"alg": "ed25519-sha256-json-v1",
"binding": "ed25519:sPHo5uNJgvrLFP80vgnUJoQAfSMHcwwYdpwTpfEQqaTpNvvuDAA3LHfsJRMnWO0XeGCYyUKyEdeskceIi1JbUg",
"chain": [
"alice",
"bob",
"worker-1"
],
"chain_fingerprint": "sha256:97a891c8f73797af4fc8a2c619aacf378153c11c35d28b2c367bbcbd3d5446b3",
"chain_pubkey": "F0sqkRw5bYv4o0xRD5XeJ6ue30JMQ575_fmbt51vRGw",
"payload": {
"expires_at": "2026-09-01T18:00:00+00:00",
"scope": "staging",
"task": "deploy"
},
"scheme": "ordered-delegation-v1",
"signature": "ed25519:rcdluzWwRwPH5g2iR8kWzB8TGqQGj4qI3kFmgmW140qIBs6gn7DpVGktxmdUpCI9r9ZygSUe1KPxANpRe2hUsw"
}
}
Errors
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_token / invalid_or_expired_api_key | No credential, an expired session, or a revoked or expired key. |
| 403 | scope_missing | A key limited by scopes lacks delegation; the body names it. |
| 422 | validation error | A required field is missing or a value has the wrong type. |
| 422 | invalid_chain | chain must be a non-empty list of non-empty strings. |
What would show this is false
Sign the same payload with the chain reversed: chain_pubkey differs. Verify the first token after swapping two chain names: chain_order_mismatch.
GET /v1/delegations/service-pubkey
The service identity key
Returns the public key that signs every token's binding, so verification can run without us.
Fetch once and pin it. A changed value means a changed service identity and voids the pin.
Parameters
None.
Request body
None.
Example request
curl
curl -sS -X GET "https://api.afaprotocol.com/v1/delegations/service-pubkey" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs"
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
r = requests.get(f"{API}/v1/delegations/service-pubkey", headers=headers, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"alg": "ed25519",
"service_pubkey": "WC_iTVveV1LW0d6cf_XTAaYkRZ__8MpC9p2gUcCUPqA",
"verifies": [
"binding (chain <-> chain_pubkey, order-exact)",
"signature (payload under chain_pubkey)"
]
}
Errors
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_token / invalid_or_expired_api_key | No credential, an expired session, or a revoked or expired key. |
What would show this is false
With the pinned key, the offline verifier on the Verify page gives the same reason as POST /v1/delegations/verify for the same token, with the network off.
GET /v1/delegations/service-pubkeys
Service signing keys, with history
Lists every key the service has signed delegation tokens with, current and retired, each with its key id, so a token signed before a rotation still verifies offline.
Parameters
None.
Request body
None.
Example request
curl
curl -sS -X GET "https://api.afaprotocol.com/v1/delegations/service-pubkeys" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs"
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
r = requests.get(f"{API}/v1/delegations/service-pubkeys", headers=headers, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"active_kid": "4ce1d7e4f549f12f",
"alg": "ed25519",
"keys": [
{
"active": true,
"kid": "4ce1d7e4f549f12f",
"public_key_b64": "Jb_rq0ElquQECrPE8NW3QQobGXTb4faAjhLcrUR_aWc",
"public_key_pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAJb/rq0ElquQECrPE8NW3QQobGXTb4faAjhLcrUR/aWc=\n-----END PUBLIC KEY-----\n",
"retired_at": null,
"valid_from": null
}
],
"kid_scheme": "first 16 hex characters of sha256 over the public key DER (SubjectPublicKeyInfo)"
}
Errors
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_token / invalid_or_expired_api_key | No credential, an expired session, or a revoked or expired key. |
What would show this is false
Recompute the kid from the PEM you receive. A token that names a kid absent from this list is refused with unknown_kid, never verified against a guess.
POST /v1/delegations/verify
Verify a delegation token
Runs the binding check and the payload signature check on a token and names the first that fails.
Reasons: ok, chain_order_mismatch, payload_tampered, binding_invalid, malformed. The hosted answer and the offline answer are the same arithmetic; compare them.
Parameters
None.
Request body
tokenobject required- No description in the schema.
Example request
curl
curl -sS -X POST "https://api.afaprotocol.com/v1/delegations/verify" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
-H "Content-Type: application/json" \
-d '{
"token": {
"alg": "ed25519-sha256-json-v1",
"binding": "ed25519:sPHo5uNJgvrLFP80vgnUJoQAfSMHcwwYdpwTpfEQqaTpNvvuDAA3LHfsJRMnWO0XeGCYyUKyEdeskceIi1JbUg",
"chain": [
"alice",
"bob",
"worker-1"
],
"chain_fingerprint": "sha256:97a891c8f73797af4fc8a2c619aacf378153c11c35d28b2c367bbcbd3d5446b3",
"chain_pubkey": "F0sqkRw5bYv4o0xRD5XeJ6ue30JMQ575_fmbt51vRGw",
"payload": {
"expires_at": "2026-09-01T18:00:00+00:00",
"scope": "staging",
"task": "deploy"
},
"scheme": "ordered-delegation-v1",
"signature": "ed25519:rcdluzWwRwPH5g2iR8kWzB8TGqQGj4qI3kFmgmW140qIBs6gn7DpVGktxmdUpCI9r9ZygSUe1KPxANpRe2hUsw"
}
}'
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {
"token": {
"alg": "ed25519-sha256-json-v1",
"binding": "ed25519:sPHo5uNJgvrLFP80vgnUJoQAfSMHcwwYdpwTpfEQqaTpNvvuDAA3LHfsJRMnWO0XeGCYyUKyEdeskceIi1JbUg",
"chain": [
"alice",
"bob",
"worker-1"
],
"chain_fingerprint": "sha256:97a891c8f73797af4fc8a2c619aacf378153c11c35d28b2c367bbcbd3d5446b3",
"chain_pubkey": "F0sqkRw5bYv4o0xRD5XeJ6ue30JMQ575_fmbt51vRGw",
"payload": {
"expires_at": "2026-09-01T18:00:00+00:00",
"scope": "staging",
"task": "deploy"
},
"scheme": "ordered-delegation-v1",
"signature": "ed25519:rcdluzWwRwPH5g2iR8kWzB8TGqQGj4qI3kFmgmW140qIBs6gn7DpVGktxmdUpCI9r9ZygSUe1KPxANpRe2hUsw"
}
}
r = requests.post(f"{API}/v1/delegations/verify", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"reason": "ok",
"valid": true
}
Errors
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_token / invalid_or_expired_api_key | No credential, an expired session, or a revoked or expired key. |
| 403 | scope_missing | A key limited by scopes lacks delegation; the body names it. |
| 422 | validation error | A required field is missing or a value has the wrong type. |
What would show this is false
Change one character of payload.scope: payload_tampered. Swap two chain entries: chain_order_mismatch.