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.
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.
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.
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.
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:
Do not post for:
Copy the script, token line and agent block from Settings → API in your project.
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 milestone subscription |
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 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"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.