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.

DELETE https://erdiknows.com/api/v1/subscribers/<app_user_id>
Authorization: Bearer <token>

Erase cohort rows for one provider id (RevenueCat app_user_id or Stripe customer id) — for end-user deletion requests you receive as controller. Same Bearer token as metrics.

Create a token in your project settings.

Personal data you send

Events, metrics, and support tickets can include personal data about other people. You need a legal basis for anything you post, and you remain responsible for it. Do not send special categories under Art. 9 GDPR — health data, beliefs, and the rest — or anything covered by professional secrecy. ErdiKnows is not built for that. Forwarded mail follows the same rules; see the Data Processing Agreement.

Subscriber cohort privacy

RevenueCat and Stripe write pseudonymous subscriber ids plus subscription state into subscribers so cohort history can be built later. Stored: provider id, trial / converted / cancelled / expired times, and product, country, price, currency when the provider sends them. Not stored: email, name, usage events, device data. Sandbox RevenueCat events are discarded for this table. See the privacy policy for retention and erasure.

From your coding agent

Cursor, Claude Code, Codex already know what changed. They can write a deploy title that a commit hash cannot.

The agent posts when it runs the deploy. If you push yourself, run the script by hand — or push a tag, which the GitHub hook picks up.

One script in the repo, one block in AGENTS.md or CLAUDE.md. No integration to install.

Post when:

  • A feature or fix ships that someone using the product would notice
  • You deploy or release to an environment users hit

Do not post for:

  • Refactoring with no user-visible change
  • Typo or copy edits alone
  • Build or CI fixes
  • Work that is not deployed yet

Copy the script, token line and agent block from Settings → API in your project.

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 milestone subscription
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, group, role and direction 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 0,8,16 * * *'
  workflow_dispatch:
jobs:
  metrics:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - name: Install dependencies
        run: |
          # Install whatever scripts/erdi-metrics.sh imports (pip, apt, …). Skip if none.
          true
      - name: Post metrics
        env:
          ERDI_TOKEN_METRICS: ${{ secrets.ERDI_TOKEN_METRICS }}
          ERDI_TZ: Europe/Berlin
          # DATABASE_URL: ${{ secrets.DATABASE_URL }}
        run: |
          FROM="$(TZ="$ERDI_TZ" date -d '3 days ago' +%Y-%m-%d)"
          TO="$(TZ="$ERDI_TZ" date +%Y-%m-%d)"
          chmod +x scripts/erdi-metrics.sh
          ./scripts/erdi-metrics.sh --from "$FROM" --to "$TO"

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.

Forwarded mail contains other people's personal data. Only forward what you are allowed to process — the same Art. 9 limits as the ingest endpoints above.

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.

Get started with ErdiKnows