Everything Erdi charts comes through two posts. Events are markers on the timeline. Metrics are the lines underneath them.
POST https://erdiknows.com/api/v1/events Authorization: Bearer <token>
Things that happened — a deploy, a price change, a campaign.
POST https://erdiknows.com/api/v1/metrics Authorization: Bearer <token>
Daily numbers — users, revenue, scans, whatever you already count.
Create a token in your project settings.
A deploy is enough. Title and kind, nothing else.
curl -X POST https://erdiknows.com/api/v1/events \
-H "Authorization: Bearer $ERDI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"kind":"deploy","title":"v1.4.0"}'| Field | Required | What it is |
|---|---|---|
kind | Yes | deploy release task ticket marketing incident external |
title | Yes | The label on the marker. Keep it short. |
occurred_at | Optional | When it happened, not when you sent it. Defaults to now. Backfilling old events works and puts them on the right day. |
external_id | Optional | Your own id for this thing. Send the same one twice and Erdi updates instead of duplicating. Webhooks retry. |
source | Optional | Defaults to api. Set this if more than one system posts. |
body | Optional | Longer notes under the title. |
url | Optional | A link from the marker. |
meta | Optional | Any extra JSON you want stored. |
One object is fine. A list is better — post every metric for the day in one request.
curl -X POST https://erdiknows.com/api/v1/metrics \
-H "Authorization: Bearer $ERDI_TOKEN" \
-H "Content-Type: application/json" \
-d '[
{
"metric": "dau",
"day": "2026-08-21",
"value": 1280,
"label": "Daily active users",
"unit": "count",
"primary": true
},
{
"metric": "scans_photo",
"day": "2026-08-21",
"value": 412,
"label": "Photo scans",
"unit": "count",
"group": "Scans"
},
{
"metric": "scans_barcode",
"day": "2026-08-21",
"value": 198,
"label": "Barcode scans",
"unit": "count",
"group": "Scans"
}
]'Units:
countcurrencypercentaveragedurationErdi does not query anything. Run whatever aggregation your database is good at, then post the result. One row per metric per day.
Node
import pg from 'pg'
const db = new pg.Pool({ connectionString: process.env.DATABASE_URL })
const { rows } = await db.query(`
select date(created_at) as day, count(distinct user_id)::int as value
from sessions
where created_at >= now() - interval '30 days'
group by 1
order by 1
`)
await fetch('https://erdiknows.com/api/v1/metrics', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ERDI_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(
rows.map((row) => ({
metric: 'dau',
day: row.day.toISOString().slice(0, 10),
value: row.value,
label: 'Daily active users',
unit: 'count',
primary: true,
}))
),
})Python
import os
import requests
import psycopg
token = os.environ["ERDI_TOKEN"]
with psycopg.connect(os.environ["DATABASE_URL"]) as conn:
rows = conn.execute("""
select date(created_at) as day, count(distinct user_id) as value
from sessions
where created_at >= now() - interval '30 days'
group by 1
order by 1
""").fetchall()
requests.post(
"https://erdiknows.com/api/v1/metrics",
headers={"Authorization": f"Bearer {token}"},
json=[
{
"metric": "dau",
"day": str(day),
"value": value,
"label": "Daily active users",
"unit": "count",
"primary": True,
}
for day, value in rows
],
)GitHub Actions
name: Tell Erdi
on:
release:
types: [published]
jobs:
event:
runs-on: ubuntu-latest
steps:
- name: Post release to Erdi
env:
ERDI_TOKEN: ${{ secrets.ERDI_TOKEN }}
run: |
curl -X POST https://erdiknows.com/api/v1/events \
-H "Authorization: Bearer $ERDI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"kind":"release","title":"${{ github.event.release.tag_name }}","url":"${{ github.event.release.html_url }}","external_id":"${{ github.event.release.id }}"}'Post the last 30 days on every run. Erdi upserts, so re-sending the same day is safe and fixes late-arriving data.
Erdi never asks for data — you decide when to send. Tell it how often to expect something and it will let you know when the sending stops.
1000 requests per hour per token. Send metrics in batches rather than one request per row — a single call can carry up to 1000 entries.
pg_cron
select cron.schedule(
'erdi-metrics',
'0 2 * * *',
$$
select net.http_post(
url := 'https://erdiknows.com/api/v1/metrics',
headers := jsonb_build_object(
'Authorization', 'Bearer ' || current_setting('app.erdi_token'),
'Content-Type', 'application/json'
),
body := (
select jsonb_agg(jsonb_build_object(
'metric', 'dau',
'day', day,
'value', value
))
from (
select date(created_at) as day,
count(distinct user_id) as value
from sessions
where created_at >= now() - interval '30 days'
group by 1
) days
)
);
$$
);GitHub Actions
name: Erdi metrics
on:
schedule:
- cron: '0 2 * * *'
workflow_dispatch:
jobs:
metrics:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: node scripts/post-erdi-metrics.mjs
env:
ERDI_TOKEN: ${{ secrets.ERDI_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}If your apps already collect feedback, post it here. It lands in the Inbox tab. You can also point your support address at the inbox in project settings. Every mail provider and support tool can forward — Gmail, Outlook, Fastmail, Proton, Crisp, Chatwoot, Front, or a rule at your domain host.
curl -X POST https://erdiknows.com/api/v1/tickets \
-H "Authorization: Bearer $ERDI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"subject": "Cannot sign in on iOS 18",
"body": "Three reports this morning. Same screen, same build.",
"from_name": "Maya",
"from_email": "maya@example.com",
"severity": "critical",
"topic": "auth",
"external_id": "zendesk:1842"
}'Critical tickets become markers on the timeline. Normal ones only count towards the daily total — otherwise the timeline becomes unreadable.