Skip to content

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.

RouteCredential
POST /v1/events, POST /v1/events/batch, PATCH /v1/events/sources/selfPublish 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}/dismissRead key
GET /v1/events/infoPublish key or read key
GET /v1/events/docsNone

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:

Error
{
  "ok": false,
  "error": "invalid-field",
  "meta": {"field": "text"},
  "code": "invalid_field",
  "message": "text is 312 characters; at most 300"
}

Event object

NameTypeDescription
idstringEvent id, evt_ and 26 characters
cursorstringPosition in the store, a decimal string, strictly increasing
sourcestringSource slug
source_namestringSource name
source_colorstring or nullSource colour, #rgb, #rrggbb or #rrggbbaa
source_iconstring or nullSimple Icons slug or SF Symbol name
typestringEvent type
severitystringdebug, info, notice, warn, error or critical
textstringOne line
detailstring or nullPlain text, newlines allowed
subjectstring or nullOne line
urlstring or nullhttps:// URL
occurred_atstringRFC 3339 UTC
received_atstringRFC 3339 UTC
displayobject or nullPresentation hints as published
dataobject or nullJSON object as published
dismissed_atstring or nullWhen 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

NameDescription
AuthorizationBearer <publish key>
Content-Typeapplication/json
Retry-AfterResponse header on 429: seconds to wait

Parameters

NameTypeDescription
type requiredstring[a-z0-9._-]{1,48}, a dotted noun: deploy.succeeded, pr.merged, site.down
text requiredstringOne line, at most 300 characters; inline **bold**, *italic*, `code`
severitystringdebug, info, notice, warn, error or critical; default info
detailstringPlain text, at most 1,000 characters, newlines and tabs allowed
subjectstringOne line, at most 80 characters
urlstringAbsolute https:// URL, at most 2,048 characters
occurred_atstringRFC 3339; default the time of receipt; clamped to 7 days either side of receipt
displayobjectPresentation hints; keys below
display.lifetimenumber or stringWhole seconds 1..86400, or "sticky" (shown until dismissed)
display.layoutstringsplit or inline
display.effectstringnone, blink, flash, highlight, wave or gradient
display.targetstringemphasis (the bold spans) or message (the whole line)
display.periodnumberEffect cycle in seconds, 0.5..10
display.colorstringAccent for emphasised spans: #rgb, #rrggbb or #rrggbbaa
dataobjectJSON object, at most 8 KB
idempotency_keystring1 to 128 characters; a key used again on the source within 7 days returns the original event
sourcestringMust 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.

Response
{
  "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

StatusCodeMeaning
400bad_jsonThe body is not one JSON object
400unknown_fieldA field or display key outside the table; meta.field names it
400invalid_fieldA field fails validation; meta.field names it
401publish_key_requiredNo Authorization header
401invalid_keyUnknown or revoked key
403scope_missingThe key carries no events:publish scope: a read key or an account key
403account_cannotThe key's account cannot use this route
403key_unboundThe key is not a publish key bound to one source
403source_mismatchsource names another source
403source_archivedThe key's source is archived
413body_too_largeBody over 16 KB
429rate_limitedOver 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

NameDescription
AuthorizationBearer <publish key>
Content-Typeapplication/json
Retry-AfterResponse header on 429: seconds to wait

Parameters

NameTypeDescription
events requiredarray1 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.

Response
{
  "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

StatusCodeMeaning
400bad_jsonThe body is not {"events": [...]}
400unknown_fieldA 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
400invalid_fieldevents 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
401publish_key_requiredNo Authorization header
401invalid_keyUnknown or revoked key
403scope_missingThe key carries no events:publish scope
403account_cannotThe key's account cannot use this route
403key_unboundThe key is not a publish key bound to one source
403source_mismatchAn event's source names another source; meta.index names the event
403source_archivedThe key's source is archived
413body_too_largeBody over 256 KB, or one event over 16 KB (meta.index)
429rate_limitedOver 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

NameDescription
AuthorizationBearer <publish key>
Content-Typeapplication/json

Parameters

NameTypeDescription
namestringAt most 80 characters; "" sets the name to the slug
iconstringA Simple Icons slug or an SF Symbol name, [a-z0-9.-]{1,48}; "" clears the icon
colorstring#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

Response
{
  "slug": "vercel",
  "name": "Vercel",
  "icon": "vercel",
  "color": "#000000"
}

Errors

StatusCodeMeaning
400bad_jsonThe body is not a JSON object of name, icon and color
400invalid_fieldname, icon or color fails validation; meta.field names it
401publish_key_requiredNo Authorization header
401invalid_keyUnknown or revoked key
403scope_missingThe key carries no events:publish scope
403account_cannotThe key's account cannot use this route
403key_unboundThe key is not a publish key bound to one source
403source_archivedThe key's source is archived
413body_too_largeBody over 16 KB
429rate_limitedOver 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

NameDescription
Signature headerThe 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-IdOptional delivery id, the same on every retry; the first one present dedupes the delivery's events
Retry-AfterResponse header on 429: seconds to wait

Parameters

NameTypeDescription
token requiredstringPath: the webhook's token, 43 characters, issued by Albastar Corp.
BodyanyJSON, 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.

Response
{
  "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:

Response
{
  "ok": true,
  "outcome": "2 events: evt_01M6GFGF0Y6K9N2Q5S8V1X4Z7C,held,dropped: rule 1",
  "events": ["evt_01M6GFGF0Y6K9N2Q5S8V1X4Z7C"]
}
outcomeMeaning
evt_…One event, stored; for a repeated idempotency key, the event stored the first time
heldOne 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 reviewNo 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 itemsNo event: the request held an empty list of items
dropped: invalid: <message>No event: the event the request made fails field validation
dropped: rate limitedNo event: the source is over its rate
error: <message>No event: storing the event failed
2 events: evt_a,held,dropped: rule 1A request that holds several items: one entry per item, in order; the count includes stored and held items only
…; url dropped: host <host> not allowedThe event's url pointed outside the hosts allowed for the webhook and was removed
…; 12 items over 1000 droppedThe 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

StatusCodeMeaning
400bad_deliveryThe webhook refused the body as malformed
401bad_signatureMissing or invalid signature where one is required or sent
404hook_not_foundUnknown token, a webhook turned off, or an archived source
413body_too_largeBody over 2 MB
429rate_limitedSource over 120 deliveries a minute (bursts of 30), or the client address over its allowance of refused requests; wait Retry-After seconds
503seal_key_missingWebhook 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

NameDescription
AuthorizationBearer <read key>
Retry-AfterResponse header on 429: seconds to wait

Parameters

NameTypeDescription
sourcestringQuery: comma-separated source slugs; default every granted source
typestringQuery: exact event type, [a-z0-9._-]{1,48}
severity_minstringQuery: lowest severity, debug, info, notice, warn, error or critical
qstringQuery: case-insensitive substring of text, detail, subject or type; at most 200 characters
afterstringQuery: cursor; events after the cursor, oldest first
beforestringQuery: cursor; events before the cursor, newest first
limitintegerQuery: 1..500; default 100
stickystringQuery: 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

NameTypeDescription
eventsarrayEvents, with dismissed_at for this key
next_beforestring or nullCursor 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_cursorstringNewest cursor among the requested sources; pass as after or Last-Event-ID to resume
Response
{
  "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

StatusCodeMeaning
400invalid_fieldA bad parameter; meta.field names it
400unknown_sourcesource names no source (keys granted every source)
401unauthorizedNo Authorization header
401invalid_keyUnknown or revoked key
403scope_missingThe key carries no events:read scope: a publish key or an account key
403account_cannotThe key's account cannot use this route
403key_unboundThe key is not a read key
403source_not_grantedsource names a source outside the key's grant
429rate_limitedOver 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

NameDescription
AuthorizationBearer <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

NameTypeDescription
protocolstringProtocol version of the Events routes: ambient/1
namestringAlbastar API
keystringreader or publish
readerstring or nullThe read key's name; null for a publish key
sourcesarraySource slugs the key reaches
featuresarraybatch, search
docsstringURL of the Events guide as markdown (GET /v1/events/docs)
Response
{
  "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

StatusCodeMeaning
401events_key_requiredNo Authorization header
401invalid_keyUnknown or revoked key
403scope_missingThe key carries neither events:read nor events:publish
403account_cannotThe key's account cannot use this route
403key_unboundThe key is not bound as a read key or a publish key
429rate_limitedOver 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

NameDescription
AuthorizationBearer <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

NameTypeDescription
slugstringSource slug
namestringSource name
colorstring or nullSource colour
iconstring or nullSimple Icons slug or SF Symbol name
last_event_atstring or nullTime of the newest event, RFC 3339 UTC
count_24hnumberEvents in the last 24 hours
archivedbooleantrue when the source takes no new events
Response
[
  {
    "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

StatusCodeMeaning
401unauthorizedNo Authorization header
401invalid_keyUnknown or revoked key
403scope_missingThe key carries no events:read scope
403account_cannotThe key's account cannot use this route
403key_unboundThe key is not a read key
429rate_limitedOver 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

NameDescription
AuthorizationBearer <read key>
Last-Event-IDOptional cursor; sends the events after the cursor first; takes precedence over after
Content-TypeResponse header: text/event-stream; charset=utf-8

Parameters

NameTypeDescription
sourcestringQuery: comma-separated source slugs; default every granted source
afterstringQuery: 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

Frames, data shortened
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"}

: keepalive

A 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:

Frames after a gap, data shortened
retry: 3000
: connected

event: gap
data: {"after":"700","skipped":52,"resume":"752"}

id: 753
event: event
data: {"id":"evt_01M6ERZP6V…","cursor":"753",…}
FrameMeaning
retry: 3000, : connectedSent once on connect
eventOne event; id is the event's cursor
stateThe key dismissed (dismissed_at set) or restored (dismissed_at: null) an event
gapMore than 1,000 events were pending after the cursor: the skipped events after cursor after, up to and including cursor resume, were not sent
: keepaliveComment 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

StatusCodeMeaning
400invalid_fieldafter or Last-Event-ID is not a cursor; meta.field names it
400unknown_sourcesource names no source (keys granted every source)
401unauthorizedNo Authorization header
401invalid_keyUnknown or revoked key
403scope_missingThe key carries no events:read scope
403account_cannotThe key's account cannot use this route
403key_unboundThe key is not a read key
403source_not_grantedsource names a source outside the key's grant
429too_many_streamsThe key already holds 8 open streams
429rate_limitedOver 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

NameDescription
AuthorizationBearer <read key>

Parameters

NameTypeDescription
id requiredstringPath: 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.

Response
{
  "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

StatusCodeMeaning
401unauthorizedNo Authorization header
401invalid_keyUnknown or revoked key
403scope_missingThe key carries no events:read scope
403account_cannotThe key's account cannot use this route
403key_unboundThe key is not a read key
404not_foundNo event with that id in the key's grant
429rate_limitedOver 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

NameDescription
Content-TypeResponse header: text/markdown; charset=utf-8
Cache-ControlResponse header: public, max-age=300

Request

curl

curl https://api.albastov.com/v1/events/docs

Python

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.