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/notifications

The dispatch log

Lists the rows written when notices went out, one per channel, newest first.

Auth
session cookie or API key
Capability
notifications
Success
HTTP 200

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

cursor query, string or null
No description in the schema.
limit query, integer
Default 50.
severity query, 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

StatusCodeMeaning
401missing_token / invalid_or_expired_api_keyNo credential, an expired session, or a revoked or expired key.
403scope_missingA 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.

Auth
session cookie or API key (administrator)
Capability
notifications
Success
HTTP 200

Parameters

None.

Request body

user_id string 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

StatusCodeMeaning
401missing_token / invalid_or_expired_api_keyNo credential, an expired session, or a revoked or expired key.
403scope_missingA key limited by scopes lacks notifications; the body names it.
422validation errorA required field is missing or a value has the wrong type.
403admin_onlyThe 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.

Auth
session cookie or API key
Capability
notifications
Success
HTTP 200

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

StatusCodeMeaning
401missing_token / invalid_or_expired_api_keyNo credential, an expired session, or a revoked or expired key.
403scope_missingA 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.

Auth
session cookie or API key
Capability
notifications
Success
HTTP 200

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

preferences array of PreferenceRow
No description in the schema.
PreferenceRow fields
channel string required
No description in the schema.
digest boolean
Default false.
enabled boolean
Default true.
type string 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

StatusCodeMeaning
401missing_token / invalid_or_expired_api_keyNo credential, an expired session, or a revoked or expired key.
403scope_missingA key limited by scopes lacks notifications; the body names it.
422validation errorA 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.

Auth
session cookie or API key (administrator in production)
Capability
notifications
Success
HTTP 200

Parameters

None.

Request body

body string
Default "test notification".
severity string or null
No description in the schema.
type string 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

StatusCodeMeaning
401missing_token / invalid_or_expired_api_keyNo credential, an expired session, or a revoked or expired key.
403scope_missingA key limited by scopes lacks notifications; the body names it.
422validation errorA required field is missing or a value has the wrong type.
403test_endpoint_requires_admin_in_productionOutside development only an administrator may send one.
400unknown_notification_typetype 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.