Full reference
Everything there is. Three routes, no SDK — what follows is the exact contract the code already keeps, not a summary of it.
Authentication
The key goes in the header, either of two ways:
Authorization: Bearer hmu_sk_live_9f3ac1d2_...X-HMU-Key: hmu_sk_live_9f3ac1d2_...The second exists for environments where a proxy already uses "Authorization" for something else.
| hmu_pk_live_… | hmu_sk_live_… | |
|---|---|---|
| Where it lives | In the browser. It's public by design. | Server only. |
| Can send | page.viewed, session.started, your custom events | Everything the public key can, plus user.created, user.deleted, user.activated, subscription.* |
| Can import | No | Yes |
The distinction isn't a convention — it's in the code. A user.created sent with the public key is rejected with secret_key_required and logged as a signal for review. It's what keeps a number published on a ranking from being "whatever any visitor wanted it to be".
The secret key is shown once, when it's created. Our database only holds its hash.
POST /v1/events
{ "event": "user.created", "user_id": "usr_123", "created_at": "2026-08-08T20:00:00Z" }{ "events": [ { "event": "user.created", "user_id": "usr_123" }, … ] }Fields
| Field | Type | Notes |
|---|---|---|
| event | required | Lowercase, up to three levels: checkout.completed. Can't start with user., subscription. or hmu. unless it's one of ours. |
| user_id | required for user events | A stable, non-personal id. We hash it before storing it, but sending emails anyway is still wrong — we don't need them. |
| created_at | optional | ISO 8601. Defaults to now if missing. 2026-08-08 is accepted too (anchored to noon UTC). |
| idempotency_key | optional | If you retry, the event isn't counted twice. |
| props | optional | A flat object, up to 20 keys, simple values. Don't send personal data here. |
Response — 202
{ "accepted": 97, "duplicated": 2, "rejected": [ { "index": 4, "code": "missing_user_id", "message": "…" } ] }A bad event never takes down the batch. The good ones are accepted and you get back the detail of each rejection with its index. If the whole batch failed instead, your retry would fail on the same event forever.
Per-event rejection codes
| code | What happened |
|---|---|
| secret_key_required | An authoritative event sent with the public key. |
| missing_user_id | Missing user_id on an event that needs it. |
| future_timestamp | created_at is in the future. Check your server clock. |
| timestamp_too_old | More than 10 years in the past. Use the import endpoint for history. |
| reserved_event_name | The name collides with a reserved one. |
| invalid_event | Doesn't match the schema (the message says which field). |
Whole-request errors
401 invalid_key · 403 project_inactive · 413 payload_too_large · 429 rate_limited (with Retry-After) · 400 invalid_request
Event types we understand
user.created · user.deleted · user.activated · subscription.started · subscription.cancelled · session.started · page.viewed · your own.
session.started is what feeds daily/weekly/monthly actives; page.viewed deliberately doesn't move that metric (a single session is dozens of views).
POST /v1/users/import
Users you already had before installing us. Secret key only, up to 1,000 per request, 5 requests per hour.
{ "users": [ { "user_id": "usr_1", "created_at": "2025-11-03" } ] }Response 202: { "imported": 843, "duplicated": 12, "failed": 0 }
They're marked as imported, forever: they count toward your total and it's disclosed on your profile, but they don't count toward growth rankings or the minimum observed users that eligibility requires. It's what keeps importing a thousand made-up users from being the shortcut to first place.
GET /v1/status
To confirm the key works and that we're receiving.
{
"project": { "id": "…", "slug": "plata", "name": "Plata" },
"key": { "kind": "secret", "prefix": "hmu_sk_live_9f3ac1d2" },
"live": true,
"last_event_at": "2026-08-08T20:14:02Z",
"total_users": 1284,
"today": { "accepted": 312, "rejected": 0, "duplicated": 4, "last_error": null }
}Embeddables
No key needed — they only draw what the owner published.
GET /api/badge/<slug>.svg?theme=dark|light|neon|minimal&lang=en|es
GET /api/counter/<slug>.svg?theme=…&lang=…The HTML you paste comes from the panel, and carries the attribution link back (/r/b/<slug>). That link is what lets us know a visit came from THAT badge — instead of "from nowhere", which is what badge traffic looks like without it.
Limits
| Limit | |
|---|---|
| Events with the secret key | 600 requests/min per project |
| Events with the public key | 120 requests/min per project |
| Import | 5 requests/hour |
| Body of /v1/events | 256 KB |
| Events per request | 100 |
Over the limit: 429 with Retry-After in seconds. Batch your events before raising the frequency.
Privacy
We don't want — and don't store — your users' names, emails, phone numbers or addresses. The user_id you send becomes an irreversible hash, different for every project, before it ever touches the database. We measure products, not people.