API

Everything Erdi charts comes through two posts. Events are markers on the timeline. Metrics are the lines underneath them.

Two endpoints

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.

Your first event

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"}'
FieldRequiredWhat it is
kindYesdeploy release task ticket marketing incident external
titleYesThe label on the marker. Keep it short.
occurred_atOptionalWhen it happened, not when you sent it. Defaults to now. Backfilling old events works and puts them on the right day.
external_idOptionalYour own id for this thing. Send the same one twice and Erdi updates instead of duplicating. Webhooks retry.
sourceOptionalDefaults to api. Set this if more than one system posts.
bodyOptionalLonger notes under the title.
urlOptionalA link from the marker.
metaOptionalAny extra JSON you want stored.

Your first metrics

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"
    }
  ]'
label, unit and group are only read the first time a metric appears. After that, rename it in project settings — your cron won't overwrite it.

Units:

  • count
  • currency
  • percent
  • average
  • duration

Where the numbers come from

Erdi 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 }}"}'

Sending on a schedule

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 }}

Support messages

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.