API reference
Verify exported records
Stateless verification of exported records: intent envelopes and human-pause checkpoints. Nothing is stored or read; the same arithmetic runs offline.
POST /v1/verify/checkpoint
Verify a human-pause checkpoint record
Checks a checkpoint's signature and responder binding, and whether it resolves exactly the pause you name.
Three checks, each named: the signature, the responder binding (responder_id must equal the hash of the supplied key), and, when you name a pause, whether this record resolves exactly that envelope_id and operator_index.
Parameters
None.
Request body
checkpointobject required- No description in the schema.
expect_envelope_idstring or null- No description in the schema.
expect_operator_indexinteger or null- No description in the schema.
responder_pubkeystring required- b64url of the raw 32-byte Ed25519 public key
Example request
curl
curl -sS -X POST "https://api.afaprotocol.com/v1/verify/checkpoint" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
-H "Content-Type: application/json" \
-d '{
"checkpoint": {
"decision": "approve",
"envelope_id": "env-7d3c1a9e5b2f4068",
"operator_index": 2,
"responded_at": "2026-09-01T14:15:40+00:00",
"responder_id": "rZ2h3kLm9Qp4Vt7Wb1Xc6A",
"signature": "ojBMMJZXLHtKKGlHs-qgpJ9Z15CbP_JS3QRTciXbugiRFbvlR06D_eQqaB0_bMbUQi1vG7lm41rHUTTLyn_usA"
},
"expect_envelope_id": "env-7d3c1a9e5b2f4068",
"expect_operator_index": 2,
"responder_pubkey": "2zLCoIQX_0RVjg6mV25hzMLGOI2aVqRthf8YsS-Ox3Y"
}'
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {
"checkpoint": {
"decision": "approve",
"envelope_id": "env-7d3c1a9e5b2f4068",
"operator_index": 2,
"responded_at": "2026-09-01T14:15:40+00:00",
"responder_id": "rZ2h3kLm9Qp4Vt7Wb1Xc6A",
"signature": "ojBMMJZXLHtKKGlHs-qgpJ9Z15CbP_JS3QRTciXbugiRFbvlR06D_eQqaB0_bMbUQi1vG7lm41rHUTTLyn_usA"
},
"expect_envelope_id": "env-7d3c1a9e5b2f4068",
"expect_operator_index": 2,
"responder_pubkey": "2zLCoIQX_0RVjg6mV25hzMLGOI2aVqRthf8YsS-Ox3Y"
}
r = requests.post(f"{API}/v1/verify/checkpoint", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"reason": "ok",
"responder_bound": true,
"satisfies": true,
"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 verify; 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
Pass expect_operator_index 3 for a checkpoint that answered pause 2: valid stays true and satisfies is false. An approval for one pause never satisfies another.
POST /v1/verify/envelope
Verify an intent envelope record
Checks an exported envelope's Ed25519 signature against the signer's public key; stores and reads nothing.
Reasons: ok, unsigned, bad_public_key, signature_invalid, malformed. The same check runs offline with the same inputs; compare the two reasons.
Parameters
None.
Request body
envelopeobject required- No description in the schema.
public_key_pemstring required- No description in the schema.
Example request
curl
curl -sS -X POST "https://api.afaprotocol.com/v1/verify/envelope" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
-H "Content-Type: application/json" \
-d '{
"envelope": {
"action_class": "tool-call",
"actor": "orchestrator-1",
"envelope_id": "env-7d3c1a9e5b2f4068",
"scope": [
"deploy:staging",
"repo:read"
],
"signature": "ojBMMJZXLHtKKGlHs-qgpJ9Z15CbP_JS3QRTciXbugiRFbvlR06D_eQqaB0_bMbUQi1vG7lm41rHUTTLyn_usA",
"signer_id": "sha256:b06ac05c8158d444bc27754779144c5b",
"temporal": "now"
},
"public_key_pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAExampleKeyBytesOnlyReplaceMe0000000000=\n-----END PUBLIC KEY-----\n"
}'
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {
"envelope": {
"action_class": "tool-call",
"actor": "orchestrator-1",
"envelope_id": "env-7d3c1a9e5b2f4068",
"scope": [
"deploy:staging",
"repo:read"
],
"signature": "ojBMMJZXLHtKKGlHs-qgpJ9Z15CbP_JS3QRTciXbugiRFbvlR06D_eQqaB0_bMbUQi1vG7lm41rHUTTLyn_usA",
"signer_id": "sha256:b06ac05c8158d444bc27754779144c5b",
"temporal": "now"
},
"public_key_pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAExampleKeyBytesOnlyReplaceMe0000000000=\n-----END PUBLIC KEY-----\n"
}
r = requests.post(f"{API}/v1/verify/envelope", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"envelope_id": "env-7d3c1a9e5b2f4068",
"reason": "ok",
"signer_id": "sha256:b06ac05c8158d444bc27754779144c5b",
"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 verify; 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 scope entry and resubmit: signature_invalid. Remove the signature: unsigned, which is a different answer from invalid.
POST /v1/verify/scientific-campaign
Verify a research record chain
Checks a chain of signed research records you supply: hashes, signatures under the public key you name, contiguous sequence numbers, parent links, and the declared stage and claim-state transitions. Payload-free: artifact bytes stay with you.
Parameters
None.
Request body
campaign_idstring required- No description in the schema.
public_key_pemstring required- No description in the schema.
recordsarray of object required- No description in the schema.
Example request
curl
curl -sS -X POST "https://api.afaprotocol.com/v1/verify/scientific-campaign" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
-H "Content-Type: application/json" \
-d '{
"campaign_id": "study-2026-09",
"public_key_pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA...\n-----END PUBLIC KEY-----\n",
"records": [
{
"artifact_digests": {
"protocol.pdf": "sha256:3b5d0f1e9a7c2b4d6e8f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d"
},
"campaign_id": "study-2026-09",
"claim_state": "proposed",
"prev_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
"record_id": "rec-0001",
"recorded_at": "2026-09-01T09:00:00+00:00",
"schema": "afa.scientific-campaign-record.v1",
"sequence": 0,
"signature": "base64url-ed25519-signature",
"stage": "preregistered"
}
]
}'
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {
"campaign_id": "study-2026-09",
"public_key_pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA...\n-----END PUBLIC KEY-----\n",
"records": [
{
"artifact_digests": {
"protocol.pdf": "sha256:3b5d0f1e9a7c2b4d6e8f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d"
},
"campaign_id": "study-2026-09",
"claim_state": "proposed",
"prev_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
"record_id": "rec-0001",
"recorded_at": "2026-09-01T09:00:00+00:00",
"schema": "afa.scientific-campaign-record.v1",
"sequence": 0,
"signature": "base64url-ed25519-signature",
"stage": "preregistered"
}
]
}
r = requests.post(f"{API}/v1/verify/scientific-campaign", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"artifact_bytes_verified": false,
"artifact_verification_boundary": "Record signatures and declared artifact digests were verified. Artifact bytes were not supplied and were not checked.",
"campaign_id": "study-2026-09",
"failures": [],
"latest_claim_state": {
"record_id": "rec-0001",
"state": "proposed"
},
"raw_payload_stored": false,
"record_count": 1,
"schema": "afa.scientific-campaign-verification.v1",
"signer_fingerprint": "sha256:1cc3ccccd1311190b895e4cc1ad792d053fdf2462527c6ff3af266645060b3d7",
"signer_trust_assessed": false,
"signer_trust_boundary": "The caller supplied the public key. Cryptographic validity does not establish who controls it.",
"stages": [
"preregistered"
],
"tip": "sha256:8e1f0a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f",
"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 verify; the body names it. |
| 422 | validation error | A required field is missing or a value has the wrong type. |
| 422 | invalid_input | the key does not parse, is not Ed25519, or the record list is empty or over 10000 |
What would show this is false
Change one byte of one record and resend: valid turns false and failures names the record. The service stores nothing from this call; run it twice and nothing differs.