Usage API
Last updated
One endpoint for login and usage events. A few minutes of work, and the strongest answer to “I never used it”.
1.Quickstart
When a customer disputes a charge, a bank wants to know they used what they paid for. Your product is the only place that knows. Send us a line each time a customer logs in or does something that matters, and we put the right ones in front of the bank: dates, IP addresses and devices.
- In Zevron, open Settings, Connections, Your product (usage API) and choose Create key. The key starts
zv_live_and is shown once. - Check the key. This sends nothing:
curl https://zevron.ai/api/v1/events \
-H "Authorization: Bearer $ZEVRON_KEY"{
"ok": true,
"merchant": "Your store",
"maxEventsPerRequest": 500,
"eventsLast30d": 0,
"lastEventAt": null
}- Send your first event. The Connections page shows it straight away.
curl -X POST https://zevron.ai/api/v1/events \
-H "Authorization: Bearer $ZEVRON_KEY" \
-H "Content-Type: application/json" \
-d '{"id":"evt_1","email":"[email protected]","event":"login","at":"2026-10-01T14:03:00Z","ip":"203.0.113.7"}'We only read an event when a dispute opens for that customer, and only the ones from the 60 days before it. Nothing is sold or shared, and a customer’s events are removed when they ask us to erase their data.
2.Keys and authentication
Send the key as Authorization: Bearer zv_live_…. Segment’s webhook destination sends it as the HTTP Basic username with an empty password, and that works too. A missing, wrong or stopped key gets 401.
We keep only a fingerprint of each key, so we cannot show it again. You can hold up to five keys, for example one per environment. Add one from Connections without touching the others.
To rotate a key, choose Rotate on its row. The new key works at once and the old one for 24 hours, so you can deploy without a gap, then it stops. Stop ends a key immediately, and anything still using it gets a 401.
3.Send events
POST /api/v1/events with a JSON body. Each event needs an email or a user_id, and an event name.
| Field | Type | Notes |
|---|---|---|
event | string, required | Your name for it: login, file_exported, lesson_completed. Names that look like a login are counted as logins. Up to 100 characters. |
email | string | How we find the customer: use the address on their card receipt. Required unless you send user_id. |
user_id | string | Your own ID. Works alone for your records, but cannot be matched to a dispute without an email. |
at | ISO 8601 time | When it happened. Leave it out and it is the time we received it. Not in the future, not more than 800 days old. |
ip | string | A valid IPv4 or IPv6 address. |
device_id | string | Any stable device or browser ID, up to 200 characters. |
id | string | Your ID for this event. Sending it again is ignored. See retries below. |
properties | object | Anything else, up to 8 KB. Set properties.platform to the tool the event came from. |
curl -X POST https://zevron.ai/api/v1/events \
-H "Authorization: Bearer $ZEVRON_KEY" \
-H "Content-Type: application/json" \
-d '{
"id": "evt_8812",
"email": "[email protected]",
"event": "login",
"at": "2026-10-01T14:03:00Z",
"ip": "203.0.113.7",
"device_id": "d_91",
"properties": { "plan": "pro", "country": "US" }
}'await fetch("https://zevron.ai/api/v1/events", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.ZEVRON_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ id: "evt_8812", email: "[email protected]", event: "login", at: new Date().toISOString(), ip: "203.0.113.7" }),
});import os, requests
requests.post(
"https://zevron.ai/api/v1/events",
headers={"Authorization": f"Bearer {os.environ['ZEVRON_KEY']}"},
json={"id": "evt_8812", "email": "[email protected]", "event": "login", "at": "2026-10-01T14:03:00Z", "ip": "203.0.113.7"},
timeout=10,
)4.Batches
Send an array, or { "events": [...] }, with up to 500 events and under 1 MB. Each event is checked on its own: the good ones are stored, and the response says what happened to the rest.
curl -X POST https://zevron.ai/api/v1/events \
-H "Authorization: Bearer $ZEVRON_KEY" \
-H "Content-Type: application/json" \
-d '[{"id":"a1","email":"[email protected]","event":"login"},{"id":"b1","email":"not-an-email","event":"login"}]'HTTP/1.1 202 Accepted
{
"accepted": 1,
"duplicates": 0,
"rejected": [{ "index": 1, "error": "email: Invalid email address" }]
}accepted were stored, duplicates were already there, and rejected lists the position (from 0) and the reason for each event we refused. A batch with a refused event still stores the rest; fix and resend only the ones listed.
5.Retries and idempotency
A network error does not tell you whether we got the request. Retrying is safe, two ways.
- An id on each event. We store an event with a given
idonce per merchant, however often it arrives, now or next year. The second time it counts as a duplicate, not an error. - An Idempotency-Key header on the request. For events without an id: the same key and the same batch stores nothing twice. We match on the key and each event’s position in the batch, so send a new key for each different batch, for example
nightly-2026-10-01.
curl -X POST https://zevron.ai/api/v1/events \
-H "Authorization: Bearer $ZEVRON_KEY" \
-H "Idempotency-Key: nightly-2026-10-01" \
-H "Content-Type: application/json" \
-d '[{"email":"[email protected]","event":"login"},{"email":"[email protected]","event":"login"}]'A replay returns 202 with "accepted": 0 and a duplicates count, plus an Idempotent-Replay: true header. Retry 429 and 5xx with the same ids or key, and back off. Do not retry a 400 unchanged: it will be refused again.
6.Responses and errors
| Status | Meaning | What to do |
|---|---|---|
202 | At least one event is stored, or was already stored. Check rejected for any we refused. | Nothing. Resend only the rejected ones, fixed. |
400 | Nothing was stored and something was refused: the body is not JSON, or every event failed a check. The reason is in rejected or error. | Fix the request. Retrying it unchanged fails the same way. |
401 | The key is missing, wrong or stopped. Body: { "error": "Invalid or missing API key" }. | Check the key in Connections. A rotated-out key stops 24 hours after rotation. |
413 | The body is over 1 MB, or there are more than 500 events. | Split it into smaller requests. |
429 | More than 600 requests a minute on one key. The Retry-After header says how many seconds to wait. | Wait, then retry the same request. Batching events keeps you far under the limit. |
5xx | Something failed on our side. | Retry with the same ids or Idempotency-Key, with a growing delay. |
Every error body is JSON with an error message you can log.
7.Limits
| Limit | Value |
|---|---|
| Events per request | 500 |
| Request size | 1 MB |
| Requests per key | 600 a minute |
| properties per event | 8 KB |
| Age of an event | Not in the future, not more than 800 days old |
| Keys per account | 5 working at once |
| CSV upload | 5 MB and 20,000 rows, 30 uploads an hour |
8.Proof of agreement
Send a terms_accepted event when a customer ticks the box that agrees to recurring billing, with the wording they saw. It becomes the first exhibit in a renewal response.
curl -X POST https://zevron.ai/api/v1/events \
-H "Authorization: Bearer $ZEVRON_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"event": "terms_accepted",
"at": "2026-03-01T10:02:00Z",
"ip": "203.0.113.7",
"properties": {
"disclosure_text": "Renews every month at $34 until you cancel.",
"checkbox": true,
"terms_version": "2026-02",
"price": "$34.00",
"interval": "every month"
}
}'9.Segment, Zapier and files
Already on Segment? Add a webhook destination that points at https://zevron.ai/api/v1/events, with your key as the HTTP Basic username and an empty password. Track, identify, page and screen calls are accepted as they are, and Segment’s messageId prevents duplicates.
Zapier can post to the same address with a Webhooks step. Set properties.platform to the tool the event came from.
No developer? Upload a CSV under Settings, Connections, or post it: columns email (or user_id), event, at, and optionally ip, device_id and id. Any other column is kept as a property. The same file twice adds nothing.
curl -X POST https://zevron.ai/api/v1/events/import \
-H "Authorization: Bearer $ZEVRON_KEY" \
-H "Content-Type: text/csv" \
--data-binary @events.csvWe do not connect to your database or warehouse. That would mean holding a credential into your production data, so a file you export and hand over is the safer way in.
10.What happens next
When a customer disputes, their events are quoted in the response as a login timeline. Your Connections page shows when we last received an event and how many. Questions? Write to the team from your dashboard, or see how we protect data.