API reference
Notifications
The dispatch log, your channel preferences, and two operator routes. A row records that a notice went out on a channel, not the message text.
GET/v1/notificationsPOST/v1/notifications/digest/runGET/v1/notifications/preferencesPUT/v1/notifications/preferencesPOST/v1/notifications/test
GET /v1/notifications
The dispatch log
Lists the rows written when notices went out, one per channel, newest first.
A notice sent in-app and by webhook is two rows. The message text is not joined here; the console banner carries it.
delivery_status is sent, pending, queued, failed or suppressed_digest. cursor is the created_at of the last row of the previous page; next_cursor is set only when a full page came back.
Parameters
cursorquery, string or null- No description in the schema.
limitquery, integer- Default
50. severityquery, string or null- No description in the schema.
Request body
None.
Example request
curl
curl -sS -X GET "https://api.afaprotocol.com/v1/notifications?severity=warning&limit=50" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs"
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
params = {
"limit": "50",
"severity": "warning"
}
r = requests.get(f"{API}/v1/notifications", headers=headers, params=params, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"next_cursor": null,
"notifications": [
{
"channel": "webhook",
"created_at": "2026-09-01T14:20:00+00:00",
"delivery_status": "sent",
"digest": false,
"id": "d1e2f3a4-5b6c-4d7e-8f90-a1b2c3d4e5f6",
"sent_at": "2026-09-01T14:22:03+00:00",
"severity": "warning",
"type": "grant.revoked"
},
{
"channel": "in_app",
"created_at": "2026-09-01T14:20:00+00:00",
"delivery_status": "sent",
"digest": false,
"id": "e2f3a4b5-6c7d-4e8f-9a01-b2c3d4e5f6a7",
"sent_at": "2026-09-01T14:20:00+00:00",
"severity": "warning",
"type": "grant.revoked"
}
]
}
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 notifications; the body names it. |
What would show this is false
Revoke a grant with a webhook registered: an in_app row and a webhook row appear, and the webhook row moves from queued to sent or failed within one drain cycle.
POST /v1/notifications/digest/run
Run the digest for one account
Rolls the last 24 hours of digest-eligible email notices for one account into a single email.
Parameters
None.
Request body
user_idstring required- No description in the schema.
Example request
curl
curl -sS -X POST "https://api.afaprotocol.com/v1/notifications/digest/run" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
-H "Content-Type: application/json" \
-d '{
"user_id": "2f6e1a0c-9b4d-4e8a-8c31-5d7f0a2b9c14"
}'
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {
"user_id": "2f6e1a0c-9b4d-4e8a-8c31-5d7f0a2b9c14"
}
r = requests.post(f"{API}/v1/notifications/digest/run", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"digest_sent": true,
"grouped": {
"chain.repair_run": 1,
"rate.limit_hit": 3
},
"user_id": "2f6e1a0c-9b4d-4e8a-8c31-5d7f0a2b9c14"
}
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 notifications; the body names it. |
| 422 | validation error | A required field is missing or a value has the wrong type. |
| 403 | admin_only | The caller is not an administrator. |
What would show this is false
Run it twice in a row: the second reports digest_sent false, because the first marked its sources as rolled up.
GET /v1/notifications/preferences
Channel preferences
Returns every notice type and channel with its current setting: the default unless you changed it.
Parameters
None.
Request body
None.
Example request
curl
curl -sS -X GET "https://api.afaprotocol.com/v1/notifications/preferences" \
-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/notifications/preferences", headers=headers, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"preferences": [
{
"channel": "in_app",
"digest": false,
"enabled": true,
"mandatory": false,
"severity": "warning",
"type": "grant.revoked"
},
{
"channel": "email",
"digest": false,
"enabled": true,
"mandatory": false,
"severity": "warning",
"type": "grant.revoked"
},
{
"channel": "webhook",
"digest": false,
"enabled": true,
"mandatory": false,
"severity": "warning",
"type": "grant.revoked"
},
{
"channel": "email",
"digest": false,
"enabled": true,
"mandatory": true,
"severity": "warning",
"type": "breaking_change.announce"
}
]
}
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 notifications; the body names it. |
What would show this is false
Every row's channel is one the type declares. A type that only rides in-app has no email row to disable.
PUT /v1/notifications/preferences
Change preferences
Stores preference rows one by one and reports each rejected row with its reason.
Unknown types, channels the type does not declare, and disabling a mandatory type land in rejected with a reason. Accepted rows are stored regardless of the others.
Parameters
None.
Request body
preferencesarray of PreferenceRow- No description in the schema.
PreferenceRow fields
channelstring required- No description in the schema.
digestboolean- Default
false. enabledboolean- Default
true. typestring required- No description in the schema.
Example request
curl
curl -sS -X PUT "https://api.afaprotocol.com/v1/notifications/preferences" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
-H "Content-Type: application/json" \
-d '{
"preferences": [
{
"channel": "in_app",
"digest": true,
"enabled": true,
"type": "rate.limit_hit"
}
]
}'
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {
"preferences": [
{
"channel": "in_app",
"digest": True,
"enabled": True,
"type": "rate.limit_hit"
}
]
}
r = requests.put(f"{API}/v1/notifications/preferences", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"accepted": 1,
"rejected": []
}
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 notifications; 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
Disable breaking_change.announce: rejected with mandatory_type_cannot_be_disabled, and the stored preference is unchanged.
POST /v1/notifications/test
Send a test notice
Dispatches one notice of a declared type through its channels, so you can see the rows and the delivery.
Parameters
None.
Request body
bodystring- Default
"test notification". severitystring or null- No description in the schema.
typestring required- No description in the schema.
Example request
curl
curl -sS -X POST "https://api.afaprotocol.com/v1/notifications/test" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
-H "Content-Type: application/json" \
-d '{
"body": "test notification",
"severity": "warning",
"type": "key.expiring_7d"
}'
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {
"body": "test notification",
"severity": "warning",
"type": "key.expiring_7d"
}
r = requests.post(f"{API}/v1/notifications/test", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"delivery_status": {
"email": "sent",
"in_app": "sent"
},
"dispatched": [
"in_app",
"email"
]
}
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 notifications; the body names it. |
| 422 | validation error | A required field is missing or a value has the wrong type. |
| 403 | test_endpoint_requires_admin_in_production | Outside development only an administrator may send one. |
| 400 | unknown_notification_type | type is not in the declared list. |
What would show this is false
In production without the administrator role: 403 and no row appears in GET /v1/notifications.