Tidings docs
Quickstart
Send your first notification in three steps — no SDK required. Only title is required.
1. Create a source & copy a key
In the console, create a source (e.g. api-prod) and generate a key. The secret tdg_sk_live_… is shown once — copy it now.
2. Send an event
curl -X POST https://tidings.tomca.be/api/ingest \
-H "Authorization: Bearer $TIDINGS_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Payment received","severity":"success"}'3. Watch it land
It arrives on your iPhone and Mac, and appears in the console's event log with a full delivery timeline. That's it.
Authentication
Every request is authenticated with a source key sent as a Bearer token. Keys belong to a single source, so you can rotate or revoke one without touching the rest. In v1 the body does not carry a source field — the key determines the source.
The Authorization header
Authorization: Bearer tdg_sk_live_a8f3c2e9b1d4Key scopes
| Scope | Meaning |
|---|---|
| full | Send events and read the log & analytics. For your own backend. |
| send_only | Can only POST /api/ingest. Safe for CI runners & edge functions. |
Rotating & revoking
Create a new key, deploy it, then revoke the old one — no downtime, and multiple keys can be live on the same source. Revoked keys return 401 immediately.
Send an event
One endpoint accepts every event — from a one-line ping to a full incident.
Request
curl -X POST https://tidings.tomca.be/api/ingest \
-H "Authorization: Bearer $TIDINGS_KEY" \
-d '{"title":"DB incident","severity":"warning","status":"open"}'Response
{ "id": "evt_8f3a2c91", "status": "queued" }The status: "queued" here is the delivery state (queued → delivered → read) — not the incident status (open → acknowledged → resolved).
Idempotency
Pass an Idempotency-Key header (or an idempotencyKey body field) and retries of the same request collapse to one notification — safe for at-least-once webhook senders.
Rate limits
Bursts up to 60 requests/second per source. Over the limit returns 429 with a Retry-After header.
Response codes
| Code | Name | Notes |
|---|---|---|
| 202 | accepted | Accepted & written. Body: { id, status: "queued" }. |
| 400 | bad_request | Malformed / unreadable JSON. |
| 401 | unknown_key | Missing, malformed or revoked key. |
| 403 | insufficient_scope | A send_only key tried to read. |
| 422 | validation_failed | Schema failure (invalid or unknown field). Body: { error, issues }. |
| 429 | rate_limited | Burst exceeded. Sends a Retry-After header. |
Schema reference
The full set of fields accepted by /api/ingest. Only title is required; unknown fields are rejected with 422.
| Field | Type | Required | Notes |
|---|---|---|---|
| title | string (1–200) | ✓ | Headline of the notification. |
| body | string (≤2000) | — | Supporting line. |
| severity | enum | — | info · success · warning · error · critical. Default: info. |
| status | enum | — | open · acknowledged · resolved. Incident status. Absent = simple notification. |
| url | url | — | Tap-through link. |
| groupKey | string (≤120) | — | Merges later updates into one incident (APNs thread-id). |
| idempotencyKey | string (≤200) | — | Dedup of retries. Alias of the Idempotency-Key header. |
| sections | { label, value }[] (≤20) | — | Key/value detail rows. |
| timeline | { at, text }[] (≤50) | — | Chronology. at = ISO 8601 (UTC). |
| actions | { label, url }[] (≤4) | — | Buttons (recall an emitter webhook). |
Severity & status
Severity is the backbone of Tidings — it drives color, sound and the iOS interruption level on every surface.
Status lifecycle
Incidents move through a lifecycle. Re-send with the same groupKey to advance the status — the card updates in place instead of stacking.
Sections & timeline
When an event deserves more than a line, attach sections ({ label, value } rows) and a timeline ({ at, text }, where at is ISO 8601 UTC). The same endpoint renders them as a full incident card.
{
"title": "DB incident",
"severity": "warning",
"status": "open",
"groupKey": "db-incident-prod",
"sections": [
{ "label": "Service", "value": "api-prod" },
{ "label": "Region", "value": "eu-central-1" }
],
"timeline": [
{ "at": "2026-06-20T10:02:00Z", "text": "Detected" },
{ "at": "2026-06-20T10:05:00Z", "text": "Investigating" }
],
"actions": [
{ "label": "Open dashboard", "url": "https://dash.example/incident/42" }
]
}timeline entry you add later (same groupKey) appends to the incident and re-pushes the Live Activity, until the status is resolved.Recipes
GitHub Actions
Ping yourself when a workflow fails. Add your key as a repository secret named TIDINGS_KEY, then drop this step at the end of the job.
- name: Notify Tidings on failure
if: failure()
run: |
curl -X POST https://tidings.tomca.be/api/ingest \
-H "Authorization: Bearer ${{ secrets.TIDINGS_KEY }}" \
-d '{"title":"Build failed","severity":"error"}'Stripe webhooks
Feel every sale. Forward Stripe's payment_intent.succeeded from your webhook handler.
if (event.type === 'payment_intent.succeeded') {
await fetch('https://tidings.tomca.be/api/ingest', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.TIDINGS_KEY}` },
body: JSON.stringify({
title: 'Payment received',
body: `€${amount} — ${plan}`,
severity: 'success'
})
})
}Supabase & cron
Fire on a new signup, or confirm a nightly job ran.
Deno.serve(async (req) => {
const { record } = await req.json()
await fetch('https://tidings.tomca.be/api/ingest', {
method: 'POST',
headers: { Authorization: `Bearer ${Deno.env.get('TIDINGS_KEY')}` },
body: JSON.stringify({ title: 'New signup', body: record.email })
})
})0 3 * * * curl -X POST https://tidings.tomca.be/api/ingest \
-H "Authorization: Bearer $TIDINGS_KEY" \
-d '{"title":"Backup complete","severity":"success"}'