Tidings docs

Get started

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

bash
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.

Get started

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

http
Authorization: Bearer tdg_sk_live_a8f3c2e9b1d4

Key scopes

ScopeMeaning
fullSend events and read the log & analytics. For your own backend.
send_onlyCan 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.

Get started

Send an event

One endpoint accepts every event — from a one-line ping to a full incident.

POSThttps://tidings.tomca.be/api/ingest

Request

bash
curl -X POST https://tidings.tomca.be/api/ingest \
  -H "Authorization: Bearer $TIDINGS_KEY" \
  -d '{"title":"DB incident","severity":"warning","status":"open"}'

Response

202 Accepted
{ "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

CodeNameNotes
202acceptedAccepted & written. Body: { id, status: "queued" }.
400bad_requestMalformed / unreadable JSON.
401unknown_keyMissing, malformed or revoked key.
403insufficient_scopeA send_only key tried to read.
422validation_failedSchema failure (invalid or unknown field). Body: { error, issues }.
429rate_limitedBurst exceeded. Sends a Retry-After header.
Payload

Schema reference

The full set of fields accepted by /api/ingest. Only title is required; unknown fields are rejected with 422.

FieldTypeRequiredNotes
titlestring (1–200)Headline of the notification.
bodystring (≤2000)Supporting line.
severityenuminfo · success · warning · error · critical. Default: info.
statusenumopen · acknowledged · resolved. Incident status. Absent = simple notification.
urlurlTap-through link.
groupKeystring (≤120)Merges later updates into one incident (APNs thread-id).
idempotencyKeystring (≤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).
Payload

Severity & status

Severity is the backbone of Tidings — it drives color, sound and the iOS interruption level on every surface.

infopassiveNo sound, batched quietly into the feed.
successactiveLight sound, a normal banner.
warningactiveLight sound, stands out in the feed.
errortime-sensitiveBreaks through Focus.
criticalcriticalCritical Alert (Pro/Team). Pierces silent mode & Do Not Disturb.

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.

openacknowledgedresolved
Payload

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.

json
{
  "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" }
  ]
}
Each timeline entry you add later (same groupKey) appends to the incident and re-pushes the Live Activity, until the status is resolved.
Recipes

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.

.github/workflows/ci.yml
- 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.

app/api/stripe/route.ts
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.

supabase/functions/notify/index.ts
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 })
  })
})
crontab
0 3 * * *  curl -X POST https://tidings.tomca.be/api/ingest \
  -H "Authorization: Bearer $TIDINGS_KEY" \
  -d '{"title":"Backup complete","severity":"success"}'