Events
Publish events, receive webhooks, and list, stream and dismiss events.
Access: Keys with the events:publish or events:read scope and webhook URLs, issued by Albastar Corp.
The Events routes publish events into a source with a publish key, turn webhook deliveries into events, and list, stream and dismiss events with a read key.
| Route | Credential |
|---|---|
POST /v1/events, POST /v1/events/batch, PATCH /v1/events/sources/self | Publish key |
POST /v1/events/hooks/in/{token} | Token in the URL, optional signature |
GET /v1/events, GET /v1/events/sources, GET /v1/events/stream, POST/DELETE /v1/events/{id}/dismiss | Read key |
GET /v1/events/info | Publish key or read key |
GET /v1/events/docs | None |
Albastar Corp. issues publish keys (abk_ keys with the events:publish scope, bound to one source), read keys
(abk_ keys with the events:read scope) and webhook URLs. An error is JSON with code, message and, for a field
error, meta.field:
{
"ok": false,
"error": "invalid-field",
"meta": {"field": "text"},
"code": "invalid_field",
"message": "text is 312 characters; at most 300"
}Event object
| Name | Type | Description |
|---|---|---|
id | string | Event id, evt_ and 26 characters |
cursor | string | Position in the store, a decimal string, strictly increasing |
source | string | Source slug |
source_name | string | Source name |
source_color | string or null | Source colour, #rgb, #rrggbb or #rrggbbaa |
source_icon | string or null | Simple Icons slug or SF Symbol name |
type | string | Event type |
severity | string | debug, info, notice, warn, error or critical |
text | string | One line |
detail | string or null | Plain text, newlines allowed |
subject | string or null | One line |
url | string or null | https:// URL |
occurred_at | string | RFC 3339 UTC |
received_at | string | RFC 3339 UTC |
display | object or null | Presentation hints as published |
data | object or null | JSON object as published |
dismissed_at | string or null | When the calling read key dismissed the event; null otherwise and on publish responses |
Publish an event
POST /v1/events stores one event in the publish key's source and returns the stored event.
Headers
| Name | Description |
|---|---|
Authorization | Bearer <publish key> |
Content-Type | application/json |
Retry-After | Response header on 429: seconds to wait |
Parameters
| Name | Type | Description |
|---|---|---|
type required | string | [a-z0-9._-]{1,48}, a dotted noun: deploy.succeeded, pr.merged, site.down |
text required | string | One line, at most 300 characters; inline **bold**, *italic*, `code` |
severity | string | debug, info, notice, warn, error or critical; default info |
detail | string | Plain text, at most 1,000 characters, newlines and tabs allowed |
subject | string | One line, at most 80 characters |
url | string | Absolute https:// URL, at most 2,048 characters |
occurred_at | string | RFC 3339; default the time of receipt; clamped to 7 days either side of receipt |
display | object | Presentation hints; keys below |
display.lifetime | number or string | Whole seconds 1..86400, or "sticky" (shown until dismissed) |
display.layout | string | split or inline |
display.effect | string | none, blink, flash, highlight, wave or gradient |
display.target | string | emphasis (the bold spans) or message (the whole line) |
display.period | number | Effect cycle in seconds, 0.5..10 |
display.color | string | Accent for emphasised spans: #rgb, #rrggbb or #rrggbbaa |
data | object | JSON object, at most 8 KB |
idempotency_key | string | 1 to 128 characters; a key used again on the source within 7 days returns the original event |
source | string | Must equal the key's source slug; leave out |
An unknown field or display key fails with 400 unknown_field.
Request
curl
curl https://api.albastov.com/v1/events \
-H "Authorization: Bearer $ALBASTAR_EVENTS_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "deploy.failed",
"severity": "error",
"text": "Deploy **failed** `9f3c2e1`",
"detail": "npm run build exited 1",
"subject": "web",
"display": {"lifetime": "sticky"},
"idempotency_key": "deploy-9f3c2e1"
}'Python
import json, os, urllib.request
req = urllib.request.Request(
"https://api.albastov.com/v1/events",
data=json.dumps({
"type": "deploy.failed",
"severity": "error",
"text": "Deploy **failed** `9f3c2e1`",
"detail": "npm run build exited 1",
"subject": "web",
"display": {"lifetime": "sticky"},
"idempotency_key": "deploy-9f3c2e1",
}).encode(),
headers={
"User-Agent": "my-app/1.0",
"Authorization": f"Bearer {os.environ['ALBASTAR_EVENTS_KEY']}",
"Content-Type": "application/json",
},
method="POST",
)
print(json.load(urllib.request.urlopen(req, timeout=5)))JavaScript
const res = await fetch("https://api.albastov.com/v1/events", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ALBASTAR_EVENTS_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
type: "deploy.failed",
severity: "error",
text: "Deploy **failed** `9f3c2e1`",
detail: "npm run build exited 1",
subject: "web",
display: { lifetime: "sticky" },
idempotency_key: "deploy-9f3c2e1",
}),
signal: AbortSignal.timeout(5000),
});
console.log(res.status, await res.json());Response
201 with the stored event; 200 with the original event when the source used idempotency_key
in the last 7 days.
{
"id": "evt_01M6GDQMZRD7F5H9K3M6P8R1T4",
"cursor": "1752",
"source": "vercel",
"source_name": "Vercel",
"source_color": "#000000",
"source_icon": "vercel",
"type": "deploy.failed",
"severity": "error",
"text": "Deploy **failed** `9f3c2e1`",
"detail": "npm run build exited 1",
"subject": "web",
"url": null,
"occurred_at": "2026-11-03T10:02:03Z",
"received_at": "2026-11-03T10:02:03Z",
"display": {"lifetime": "sticky"},
"data": null,
"dismissed_at": null
}Errors
| Status | Code | Meaning |
|---|---|---|
400 | bad_json | The body is not one JSON object |
400 | unknown_field | A field or display key outside the table; meta.field names it |
400 | invalid_field | A field fails validation; meta.field names it |
401 | publish_key_required | No Authorization header |
401 | invalid_key | Unknown or revoked key |
403 | scope_missing | The key carries no events:publish scope: a read key or an account key |
403 | account_cannot | The key's account cannot use this route |
403 | key_unbound | The key is not a publish key bound to one source |
403 | source_mismatch | source names another source |
403 | source_archived | The key's source is archived |
413 | body_too_large | Body over 16 KB |
429 | rate_limited | Over the key's rate; wait Retry-After seconds |
Publish a batch
POST /v1/events/batch stores 1 to 100 events in the publish key's source in one request.
Headers
| Name | Description |
|---|---|
Authorization | Bearer <publish key> |
Content-Type | application/json |
Retry-After | Response header on 429: seconds to wait |
Parameters
| Name | Type | Description |
|---|---|---|
events required | array | 1 to 100 event objects, each with the fields of Publish an event, each at most 16 KB |
One invalid event refuses the whole batch; the error's meta.index names the event. Each event of the batch counts
against the key's event rate.
Request
curl
curl https://api.albastov.com/v1/events/batch \
-H "Authorization: Bearer $ALBASTAR_EVENTS_KEY" \
-H "Content-Type: application/json" \
-d '{
"events": [
{"type": "deploy.succeeded", "text": "Deploy **done**", "subject": "web"},
{"type": "deploy.succeeded", "text": "Deploy **done**", "subject": "docs"}
]
}'Python
import json, os, urllib.request
events = [
{"type": "deploy.succeeded", "text": "Deploy **done**", "subject": "web"},
{"type": "deploy.succeeded", "text": "Deploy **done**", "subject": "docs"},
]
req = urllib.request.Request(
"https://api.albastov.com/v1/events/batch",
data=json.dumps({"events": events}).encode(),
headers={
"User-Agent": "my-app/1.0",
"Authorization": f"Bearer {os.environ['ALBASTAR_EVENTS_KEY']}",
"Content-Type": "application/json",
},
method="POST",
)
print(json.load(urllib.request.urlopen(req, timeout=5)))JavaScript
const res = await fetch("https://api.albastov.com/v1/events/batch", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ALBASTAR_EVENTS_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
events: [
{ type: "deploy.succeeded", text: "Deploy **done**", subject: "web" },
{ type: "deploy.succeeded", text: "Deploy **done**", subject: "docs" },
],
}),
signal: AbortSignal.timeout(5000),
});
console.log(res.status, await res.json());Response
{"events": [...]} with the stored events in request order; status 201 when at least one event is
new, else 200.
{
"events": [
{
"id": "evt_01M6GEE49Q8P1R4T7W9Y3B6D8F",
"cursor": "1753",
"source": "vercel",
"source_name": "Vercel",
"source_color": "#000000",
"source_icon": "vercel",
"type": "deploy.succeeded",
"severity": "info",
"text": "Deploy **done**",
"detail": null,
"subject": "web",
"url": null,
"occurred_at": "2026-11-03T10:14:20Z",
"received_at": "2026-11-03T10:14:20Z",
"display": null,
"data": null,
"dismissed_at": null
},
{
"id": "evt_01M6GEE49Q8P1R4T7W9Y3B6D8G",
"cursor": "1754",
"source": "vercel",
"source_name": "Vercel",
"source_color": "#000000",
"source_icon": "vercel",
"type": "deploy.succeeded",
"severity": "info",
"text": "Deploy **done**",
"detail": null,
"subject": "docs",
"url": null,
"occurred_at": "2026-11-03T10:14:20Z",
"received_at": "2026-11-03T10:14:20Z",
"display": null,
"data": null,
"dismissed_at": null
}
]
}Errors
| Status | Code | Meaning |
|---|---|---|
400 | bad_json | The body is not {"events": [...]} |
400 | unknown_field | A top-level key other than events, or an unknown field in an event; meta.field names the field and, for an event, meta.index the event |
400 | invalid_field | events is not an array of 1 to 100 events, or an event fails validation; meta.field names the field and, for an event, meta.index the event |
401 | publish_key_required | No Authorization header |
401 | invalid_key | Unknown or revoked key |
403 | scope_missing | The key carries no events:publish scope |
403 | account_cannot | The key's account cannot use this route |
403 | key_unbound | The key is not a publish key bound to one source |
403 | source_mismatch | An event's source names another source; meta.index names the event |
403 | source_archived | The key's source is archived |
413 | body_too_large | Body over 256 KB, or one event over 16 KB (meta.index) |
429 | rate_limited | Over the key's request or event rate; wait Retry-After seconds |
Set the source display
PATCH /v1/events/sources/self sets the name, icon and colour of the publish key's own source, for the source's whole
history.
Headers
| Name | Description |
|---|---|
Authorization | Bearer <publish key> |
Content-Type | application/json |
Parameters
| Name | Type | Description |
|---|---|---|
name | string | At most 80 characters; "" sets the name to the slug |
icon | string | A Simple Icons slug or an SF Symbol name, [a-z0-9.-]{1,48}; "" clears the icon |
color | string | #rgb, #rrggbb or #rrggbbaa; "" clears the colour |
Omitted fields keep their values; {} returns the current values.
Request
curl
curl -X PATCH https://api.albastov.com/v1/events/sources/self \
-H "Authorization: Bearer $ALBASTAR_EVENTS_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Vercel", "icon": "vercel", "color": "#000000"}'Python
import json, os, urllib.request
req = urllib.request.Request(
"https://api.albastov.com/v1/events/sources/self",
data=json.dumps({
"name": "Vercel",
"icon": "vercel",
"color": "#000000",
}).encode(),
headers={
"User-Agent": "my-app/1.0",
"Authorization": f"Bearer {os.environ['ALBASTAR_EVENTS_KEY']}",
"Content-Type": "application/json",
},
method="PATCH",
)
print(json.load(urllib.request.urlopen(req)))JavaScript
const res = await fetch("https://api.albastov.com/v1/events/sources/self", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.ALBASTAR_EVENTS_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "Vercel", icon: "vercel", color: "#000000" }),
});
console.log(await res.json());Response
{
"slug": "vercel",
"name": "Vercel",
"icon": "vercel",
"color": "#000000"
}Errors
| Status | Code | Meaning |
|---|---|---|
400 | bad_json | The body is not a JSON object of name, icon and color |
400 | invalid_field | name, icon or color fails validation; meta.field names it |
401 | publish_key_required | No Authorization header |
401 | invalid_key | Unknown or revoked key |
403 | scope_missing | The key carries no events:publish scope |
403 | account_cannot | The key's account cannot use this route |
403 | key_unbound | The key is not a publish key bound to one source |
403 | source_archived | The key's source is archived |
413 | body_too_large | Body over 16 KB |
429 | rate_limited | Over the key's rate; wait Retry-After seconds |
Receive a webhook delivery
POST /v1/events/hooks/in/{token} turns a request from another app into events of the webhook's source, with rules
that Albastar Corp. sets for the webhook. Sending, signatures and duplicates:
Webhooks.
Headers
| Name | Description |
|---|---|
| Signature header | The header the webhook's signature names (X-Signature by default, Stripe-Signature for scheme stripe); required when the signature is required |
X-GitHub-Delivery, Idempotency-Key, Webhook-Id, X-Webhook-Id, X-Delivery-Id | Optional delivery id, the same on every retry; the first one present dedupes the delivery's events |
Retry-After | Response header on 429: seconds to wait |
Parameters
| Name | Type | Description |
|---|---|---|
token required | string | Path: the webhook's token, 43 characters, issued by Albastar Corp. |
| Body | any | JSON, a form, or any text, at most 2 MB; an empty body reads the query string |
Request
curl
curl https://api.albastov.com/v1/events/hooks/in/$ALBASTAR_WEBHOOK_TOKEN \
-H "Content-Type: application/json" \
-d '{
"project": "web",
"deployment": {"id": "dpl_4812", "state": "ERROR", "commit": "9f3c2e1"}
}'Python
import json, os, urllib.request
token = os.environ["ALBASTAR_WEBHOOK_TOKEN"]
body = {
"project": "web",
"deployment": {"id": "dpl_4812", "state": "ERROR", "commit": "9f3c2e1"},
}
req = urllib.request.Request(
f"https://api.albastov.com/v1/events/hooks/in/{token}",
data=json.dumps(body).encode(),
headers={"User-Agent": "my-app/1.0", "Content-Type": "application/json"},
method="POST",
)
print(json.load(urllib.request.urlopen(req)))JavaScript
const token = process.env.ALBASTAR_WEBHOOK_TOKEN;
const url = `https://api.albastov.com/v1/events/hooks/in/${token}`;
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
project: "web",
deployment: { id: "dpl_4812", state: "ERROR", commit: "9f3c2e1" },
}),
});
console.log(await res.json());Response
200 with the delivery's outcome and the ids of the stored events.
{
"ok": true,
"outcome": "evt_01M6GEVFQHS6V8X2Z4B7D9G1J5",
"events": ["evt_01M6GEVFQHS6V8X2Z4B7D9G1J5"]
}A delivery that makes several items, here an event, a held event and an item a rule dropped, answers one entry per item:
{
"ok": true,
"outcome": "2 events: evt_01M6GFGF0Y6K9N2Q5S8V1X4Z7C,held,dropped: rule 1",
"events": ["evt_01M6GFGF0Y6K9N2Q5S8V1X4Z7C"]
}outcome | Meaning |
|---|---|
evt_… | One event, stored; for a repeated idempotency key, the event stored the first time |
held | One event, held: at most 5 events of one webhook source a minute reach readers one by one, and the rest arrive after the minute as one summary event |
held: pending review | No event: the webhook is pending; the delivery is kept |
dropped: <reason> | No event: one of the webhook's rules dropped the item (rule 1, case 3, or the rule's own reason) |
dropped: duplicate of delivery <id> | No event: the same body arrived within the last 10 minutes |
dropped: split: 0 items | No event: the request held an empty list of items |
dropped: invalid: <message> | No event: the event the request made fails field validation |
dropped: rate limited | No event: the source is over its rate |
error: <message> | No event: storing the event failed |
2 events: evt_a,held,dropped: rule 1 | A request that holds several items: one entry per item, in order; the count includes stored and held items only |
…; url dropped: host <host> not allowed | The event's url pointed outside the hosts allowed for the webhook and was removed |
…; 12 items over 1000 dropped | The request held more than 1,000 items; items past 1,000 make no event |
…; mapping error: <message> | The webhook's rules failed on this request; the delivery made one plain event of type webhook instead |
Errors
| Status | Code | Meaning |
|---|---|---|
400 | bad_delivery | The webhook refused the body as malformed |
401 | bad_signature | Missing or invalid signature where one is required or sent |
404 | hook_not_found | Unknown token, a webhook turned off, or an archived source |
413 | body_too_large | Body over 2 MB |
429 | rate_limited | Source over 120 deliveries a minute (bursts of 30), or the client address over its allowance of refused requests; wait Retry-After seconds |
503 | seal_key_missing | Webhook signatures cannot be checked right now; retry later |
List events
GET /v1/events lists the read key's events, newest first; with after, oldest first.
Headers
| Name | Description |
|---|---|
Authorization | Bearer <read key> |
Retry-After | Response header on 429: seconds to wait |
Parameters
| Name | Type | Description |
|---|---|---|
source | string | Query: comma-separated source slugs; default every granted source |
type | string | Query: exact event type, [a-z0-9._-]{1,48} |
severity_min | string | Query: lowest severity, debug, info, notice, warn, error or critical |
q | string | Query: case-insensitive substring of text, detail, subject or type; at most 200 characters |
after | string | Query: cursor; events after the cursor, oldest first |
before | string | Query: cursor; events before the cursor, newest first |
limit | integer | Query: 1..500; default 100 |
sticky | string | Query: open lists sticky events this key has not dismissed |
Request
curl
curl "https://api.albastov.com/v1/events?source=vercel&limit=1" \
-H "Authorization: Bearer $ALBASTAR_EVENTS_READ_KEY"Python
import json, os, urllib.request
req = urllib.request.Request(
"https://api.albastov.com/v1/events?source=vercel&limit=1",
headers={
"User-Agent": "my-app/1.0",
"Authorization": f"Bearer {os.environ['ALBASTAR_EVENTS_READ_KEY']}",
},
)
print(json.load(urllib.request.urlopen(req)))JavaScript
const res = await fetch(
"https://api.albastov.com/v1/events?source=vercel&limit=1",
{
headers: {
Authorization: `Bearer ${process.env.ALBASTAR_EVENTS_READ_KEY}`,
},
},
);
console.log(await res.json());Response
| Name | Type | Description |
|---|---|---|
events | array | Events, with dismissed_at for this key |
next_before | string or null | Cursor of the next older page; null when the page holds fewer than limit events, and with after. With after, pass the last event's cursor as the next after |
latest_cursor | string | Newest cursor among the requested sources; pass as after or Last-Event-ID to resume |
{
"events": [
{
"id": "evt_01M6GEE49Q8P1R4T7W9Y3B6D8G",
"cursor": "1754",
"source": "vercel",
"source_name": "Vercel",
"source_color": "#000000",
"source_icon": "vercel",
"type": "deploy.succeeded",
"severity": "info",
"text": "Deploy **done**",
"detail": null,
"subject": "docs",
"url": null,
"occurred_at": "2026-11-03T10:14:20Z",
"received_at": "2026-11-03T10:14:20Z",
"display": null,
"data": null,
"dismissed_at": null
}
],
"next_before": "1754",
"latest_cursor": "1754"
}Errors
| Status | Code | Meaning |
|---|---|---|
400 | invalid_field | A bad parameter; meta.field names it |
400 | unknown_source | source names no source (keys granted every source) |
401 | unauthorized | No Authorization header |
401 | invalid_key | Unknown or revoked key |
403 | scope_missing | The key carries no events:read scope: a publish key or an account key |
403 | account_cannot | The key's account cannot use this route |
403 | key_unbound | The key is not a read key |
403 | source_not_granted | source names a source outside the key's grant |
429 | rate_limited | Over the key's rate; wait Retry-After seconds |
Get key info
GET /v1/events/info returns the calling key's kind, name and sources.
Headers
| Name | Description |
|---|---|
Authorization | Bearer <read key> or Bearer <publish key> |
Request
curl
curl https://api.albastov.com/v1/events/info \
-H "Authorization: Bearer $ALBASTAR_EVENTS_READ_KEY"Python
import json, os, urllib.request
req = urllib.request.Request(
"https://api.albastov.com/v1/events/info",
headers={
"User-Agent": "my-app/1.0",
"Authorization": f"Bearer {os.environ['ALBASTAR_EVENTS_READ_KEY']}",
},
)
print(json.load(urllib.request.urlopen(req)))JavaScript
const res = await fetch("https://api.albastov.com/v1/events/info", {
headers: { Authorization: `Bearer ${process.env.ALBASTAR_EVENTS_READ_KEY}` },
});
console.log(await res.json());Response
| Name | Type | Description |
|---|---|---|
protocol | string | Protocol version of the Events routes: ambient/1 |
name | string | Albastar API |
key | string | reader or publish |
reader | string or null | The read key's name; null for a publish key |
sources | array | Source slugs the key reaches |
features | array | batch, search |
docs | string | URL of the Events guide as markdown (GET /v1/events/docs) |
{
"protocol": "ambient/1",
"name": "Albastar API",
"key": "reader",
"reader": "dashboard",
"sources": ["stripe", "vercel"],
"features": ["batch", "search"],
"docs": "https://api.albastov.com/v1/events/docs"
}Errors
| Status | Code | Meaning |
|---|---|---|
401 | events_key_required | No Authorization header |
401 | invalid_key | Unknown or revoked key |
403 | scope_missing | The key carries neither events:read nor events:publish |
403 | account_cannot | The key's account cannot use this route |
403 | key_unbound | The key is not bound as a read key or a publish key |
429 | rate_limited | Over the key's rate; wait Retry-After seconds |
List sources
GET /v1/events/sources returns the read key's granted sources as an array.
Headers
| Name | Description |
|---|---|
Authorization | Bearer <read key> |
Request
curl
curl https://api.albastov.com/v1/events/sources \
-H "Authorization: Bearer $ALBASTAR_EVENTS_READ_KEY"Python
import json, os, urllib.request
req = urllib.request.Request(
"https://api.albastov.com/v1/events/sources",
headers={
"User-Agent": "my-app/1.0",
"Authorization": f"Bearer {os.environ['ALBASTAR_EVENTS_READ_KEY']}",
},
)
print(json.load(urllib.request.urlopen(req)))JavaScript
const res = await fetch("https://api.albastov.com/v1/events/sources", {
headers: { Authorization: `Bearer ${process.env.ALBASTAR_EVENTS_READ_KEY}` },
});
console.log(await res.json());Response
| Name | Type | Description |
|---|---|---|
slug | string | Source slug |
name | string | Source name |
color | string or null | Source colour |
icon | string or null | Simple Icons slug or SF Symbol name |
last_event_at | string or null | Time of the newest event, RFC 3339 UTC |
count_24h | number | Events in the last 24 hours |
archived | boolean | true when the source takes no new events |
[
{
"slug": "stripe",
"name": "Stripe",
"color": "#635bff",
"icon": "stripe",
"last_event_at": "2026-11-03T09:41:12Z",
"count_24h": 6,
"archived": false
},
{
"slug": "vercel",
"name": "Vercel",
"color": "#000000",
"icon": "vercel",
"last_event_at": "2026-11-03T10:14:20Z",
"count_24h": 14,
"archived": false
}
]Errors
| Status | Code | Meaning |
|---|---|---|
401 | unauthorized | No Authorization header |
401 | invalid_key | Unknown or revoked key |
403 | scope_missing | The key carries no events:read scope |
403 | account_cannot | The key's account cannot use this route |
403 | key_unbound | The key is not a read key |
429 | rate_limited | Over the key's rate; wait Retry-After seconds |
Stream events
GET /v1/events/stream returns a text/event-stream of new events of the read key's sources.
Headers
| Name | Description |
|---|---|
Authorization | Bearer <read key> |
Last-Event-ID | Optional cursor; sends the events after the cursor first; takes precedence over after |
Content-Type | Response header: text/event-stream; charset=utf-8 |
Parameters
| Name | Type | Description |
|---|---|---|
source | string | Query: comma-separated source slugs; default every granted source |
after | string | Query: cursor; as Last-Event-ID |
Request
curl
curl -N https://api.albastov.com/v1/events/stream \
-H "Authorization: Bearer $ALBASTAR_EVENTS_READ_KEY"Python
import os, urllib.request
req = urllib.request.Request(
"https://api.albastov.com/v1/events/stream",
headers={
"User-Agent": "my-app/1.0",
"Authorization": f"Bearer {os.environ['ALBASTAR_EVENTS_READ_KEY']}",
},
)
with urllib.request.urlopen(req) as res:
for line in res:
print(line.decode().rstrip("\n"))JavaScript
const res = await fetch("https://api.albastov.com/v1/events/stream", {
headers: { Authorization: `Bearer ${process.env.ALBASTAR_EVENTS_READ_KEY}` },
});
const decoder = new TextDecoder();
for await (const bytes of res.body) {
process.stdout.write(decoder.decode(bytes, { stream: true }));
}Response
retry: 3000
: connected
id: 1752
event: event
data: {"id":"evt_01M6GDQMZRD7F5H9K3M6P8R1T4","cursor":"1752",…}
event: state
data: {"id":"evt_01M6GDQM…","dismissed_at":"2026-11-03T10:05:00Z"}
: keepaliveA stream resumed with Last-Event-ID: 700 while 1,052 events are pending after cursor 700 sends the newest 1,000
(cursors 753 to 1752) after a gap frame:
retry: 3000
: connected
event: gap
data: {"after":"700","skipped":52,"resume":"752"}
id: 753
event: event
data: {"id":"evt_01M6ERZP6V…","cursor":"753",…}| Frame | Meaning |
|---|---|
retry: 3000, : connected | Sent once on connect |
event | One event; id is the event's cursor |
state | The key dismissed (dismissed_at set) or restored (dismissed_at: null) an event |
gap | More than 1,000 events were pending after the cursor: the skipped events after cursor after, up to and including cursor resume, were not sent |
: keepalive | Comment every 20 s |
With a cursor, the stream sends the pending events (the newest 1,000 after a gap), then this key's dismissals since
the cursor as state frames, then new events. To fetch the events a gap skipped, call
GET /v1/events?after=<after>&limit=500, then repeat with the last cursor of each page as after until the page
reaches cursor resume.
Errors
| Status | Code | Meaning |
|---|---|---|
400 | invalid_field | after or Last-Event-ID is not a cursor; meta.field names it |
400 | unknown_source | source names no source (keys granted every source) |
401 | unauthorized | No Authorization header |
401 | invalid_key | Unknown or revoked key |
403 | scope_missing | The key carries no events:read scope |
403 | account_cannot | The key's account cannot use this route |
403 | key_unbound | The key is not a read key |
403 | source_not_granted | source names a source outside the key's grant |
429 | too_many_streams | The key already holds 8 open streams |
429 | rate_limited | Over the key's rate; wait Retry-After seconds |
Dismiss an event
POST /v1/events/{id}/dismiss sets the read key's dismissal of an event and returns the event.
Headers
| Name | Description |
|---|---|
Authorization | Bearer <read key> |
Parameters
| Name | Type | Description |
|---|---|---|
id required | string | Path: event id, evt_… |
A second POST keeps the first dismissal time. Every open stream of the same key receives a state frame.
Request
curl
curl -X POST \
https://api.albastov.com/v1/events/evt_01M6GDQMZRD7F5H9K3M6P8R1T4/dismiss \
-H "Authorization: Bearer $ALBASTAR_EVENTS_READ_KEY"Python
import json, os, urllib.request
event_id = "evt_01M6GDQMZRD7F5H9K3M6P8R1T4"
req = urllib.request.Request(
f"https://api.albastov.com/v1/events/{event_id}/dismiss",
headers={
"User-Agent": "my-app/1.0",
"Authorization": f"Bearer {os.environ['ALBASTAR_EVENTS_READ_KEY']}",
},
method="POST",
)
print(json.load(urllib.request.urlopen(req)))JavaScript
const eventId = "evt_01M6GDQMZRD7F5H9K3M6P8R1T4";
const res = await fetch(
`https://api.albastov.com/v1/events/${eventId}/dismiss`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ALBASTAR_EVENTS_READ_KEY}`,
},
},
);
console.log(await res.json());Response
The event with dismissed_at set.
{
"id": "evt_01M6GDQMZRD7F5H9K3M6P8R1T4",
"cursor": "1752",
"source": "vercel",
"source_name": "Vercel",
"source_color": "#000000",
"source_icon": "vercel",
"type": "deploy.failed",
"severity": "error",
"text": "Deploy **failed** `9f3c2e1`",
"detail": "npm run build exited 1",
"subject": "web",
"url": null,
"occurred_at": "2026-11-03T10:02:03Z",
"received_at": "2026-11-03T10:02:03Z",
"display": {"lifetime": "sticky"},
"data": null,
"dismissed_at": "2026-11-03T10:05:00Z"
}Errors
| Status | Code | Meaning |
|---|---|---|
401 | unauthorized | No Authorization header |
401 | invalid_key | Unknown or revoked key |
403 | scope_missing | The key carries no events:read scope |
403 | account_cannot | The key's account cannot use this route |
403 | key_unbound | The key is not a read key |
404 | not_found | No event with that id in the key's grant |
429 | rate_limited | Over the key's rate; wait Retry-After seconds |
Restore an event
DELETE /v1/events/{id}/dismiss clears the read key's dismissal of an event and returns the event with
dismissed_at: null. Headers, parameters and errors match Dismiss an event; a second DELETE
changes nothing. Every open stream of the same key receives a state frame with dismissed_at: null.
Request
curl
curl -X DELETE \
https://api.albastov.com/v1/events/evt_01M6GDQMZRD7F5H9K3M6P8R1T4/dismiss \
-H "Authorization: Bearer $ALBASTAR_EVENTS_READ_KEY"Python
import json, os, urllib.request
event_id = "evt_01M6GDQMZRD7F5H9K3M6P8R1T4"
req = urllib.request.Request(
f"https://api.albastov.com/v1/events/{event_id}/dismiss",
headers={
"User-Agent": "my-app/1.0",
"Authorization": f"Bearer {os.environ['ALBASTAR_EVENTS_READ_KEY']}",
},
method="DELETE",
)
print(json.load(urllib.request.urlopen(req)))JavaScript
const eventId = "evt_01M6GDQMZRD7F5H9K3M6P8R1T4";
const res = await fetch(
`https://api.albastov.com/v1/events/${eventId}/dismiss`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.ALBASTAR_EVENTS_READ_KEY}`,
},
},
);
console.log(await res.json());Response
The event with dismissed_at: null, otherwise as in Dismiss an event.
Get the Events guide
GET /v1/events/docs returns the Events guides as one markdown document, without a key.
Headers
| Name | Description |
|---|---|
Content-Type | Response header: text/markdown; charset=utf-8 |
Cache-Control | Response header: public, max-age=300 |
Request
curl
curl https://api.albastov.com/v1/events/docsPython
import urllib.request
req = urllib.request.Request(
"https://api.albastov.com/v1/events/docs",
headers={"User-Agent": "my-app/1.0"},
)
print(urllib.request.urlopen(req).read().decode()[:400])JavaScript
const res = await fetch("https://api.albastov.com/v1/events/docs");
console.log((await res.text()).slice(0, 400));Response
The Overview, Publishing, Webhooks and Receiving pages in sidebar order, each under its title and page URL, without front matter.
Errors
The route takes no key and no parameters, and has no route-specific errors.