Leads
A lead is a form submission — a name, an e-mail, a phone number typed by a real person into a form on the site. It is personal data, and this surface treats it that way.
| Method | Path | Scope |
|---|---|---|
GET |
/v1/leads |
leads:read |
GET |
/v1/leads/{id} |
leads:read |
- Base URL:
https://api.canverly.com - Auth:
Authorization: Bearer ck_…— scopeleads:read - Rate limit: 10 req/min per key (leads bucket) and 60 req/min (general bucket)
The site is derived from the key. There is no site parameter, and a key only ever reads its own site’s leads.
GET /v1/leads
Section titled “GET /v1/leads”List the site’s leads, newest first, paginated by cursor.
Parameters
Section titled “Parameters”| Parameter | Type | Default | Notes |
|---|---|---|---|
form_id |
string (ULID/UUID) | — | Restrict to one form. Omit to list every form on the site. |
status |
string | clean |
clean, spam, or all. Anything else is a 422. |
de |
string | — | Period start — YYYY-MM-DD or an RFC-3339 instant. |
ate |
string | — | Period end — inclusive when given as a date (ate=2026-08-10 covers all of 10 August); an explicit RFC-3339 instant is used as an exclusive upper bound. |
cursor |
string | — | Keyset cursor from the previous page’s next_cursor. |
limit |
integer | 50 |
1–200. Out-of-range values are clamped into that range; a non-integer is rejected. |
ordem |
string | desc |
Order — desc (newest first) or asc. Anything else is a 422. |
de must be strictly before ate — an inverted or empty period is a 422, not an empty list. An unparseable date or cursor is also a 422: nothing is silently ignored, because a filter that quietly falls back would hand you more personal data than you asked for.
Example
Section titled “Example”curl "https://api.canverly.com/v1/leads?status=clean&de=2026-08-01&ate=2026-08-31&limit=50" \ -H "Authorization: Bearer ck_live_xxx"200 OK
Section titled “200 OK”{ "items": [ { "id": "01K8Z9K3F7T8M2QYV5N6B4WJ8R", "form_id": "01K8Z8YVQ4N7B1C2D3E4F5G6H7", "page_path": "/contato", "data": { "nome": "Ana Teste", "email": "ana@example.test", "mensagem": "Quero um orçamento." }, "status": "clean", "spam_score": 0, "created_at": "2026-08-24T14:03:11Z" } ], "next_cursor": ""}| Field | Type | Notes |
|---|---|---|
items[] |
Lead[] | The page of leads — see the Lead object. |
next_cursor |
string | null | Pass it back as cursor for the next page. End of the collection is signalled by an empty string "" — treat "", null and an absent field the same way: stop. A cursor is only issued when the page came back full, so the last page always ends the walk. |
Pagination
Section titled “Pagination”Keyset, not offset: read next_cursor, send it as cursor, repeat until it comes back empty. Cursors are opaque — do not parse or construct them. A cursor from a different filter is not meaningful; keep the other parameters identical across the pages of one walk.
curl "https://api.canverly.com/v1/leads?limit=50&cursor=1756044191000000000%7C01K8Z9K3F7T8M2QYV5N6B4WJ8R" \ -H "Authorization: Bearer ck_live_xxx"Remember the 10 req/min ceiling: at limit=200 that is up to 2 000 leads per minute, which is plenty for a sync but not for a scrape. For a full dump, use the export instead — see Bulk export.
GET /v1/leads/{id}
Section titled “GET /v1/leads/{id}”Read a single lead. id is the lead’s ULID (a UUID is accepted too). The response is one lead in exactly the same shape as a list item — not wrapped in items.
curl "https://api.canverly.com/v1/leads/01K8Z9K3F7T8M2QYV5N6B4WJ8R" \ -H "Authorization: Bearer ck_live_xxx"200 OK
Section titled “200 OK”{ "id": "01K8Z9K3F7T8M2QYV5N6B4WJ8R", "form_id": "01K8Z8YVQ4N7B1C2D3E4F5G6H7", "page_path": "/contato", "data": { "nome": "Ana Teste", "email": "ana@example.test" }, "status": "clean", "spam_score": 0, "created_at": "2026-08-24T14:03:11Z"}The Lead object
Section titled “The Lead object”| Field | Type | Notes |
|---|---|---|
id |
string (ULID) | Lead identifier. |
form_id |
string (ULID) | The form that produced it. |
page_path |
string | Path of the page the form was submitted from, e.g. /contato. |
data |
object | The submitted answers: field id → string value. Keys are the form’s own field ids, so they differ per form. |
status |
string | clean or spam. |
spam_score |
integer | Spam signal strength (0 = no signal). |
created_at |
string (RFC-3339) | When it was submitted, UTC. |
Everything in data is typed by a visitor. Treat it as untrusted input: escape it before rendering, and never interpolate it into HTML, SQL or a shell command.
Bulk export
Section titled “Bulk export”To pull the whole collection at once — NDJSON or CSV, streamed — use GET /v1/export?resource=leads instead of paginating this route. It requires both the export scope and leads:read, and it is audited and rate-limited exactly like the routes on this page. See Bulk export & import.
Errors
Section titled “Errors”Envelope: { "error": { "code", "message" } }. Full table in Errors & rate limits.
| Status | error.code |
When |
|---|---|---|
400 |
upstream |
Malformed limit on the list (not an integer). Send a whole number. |
401 |
unauthorized |
Missing, invalid or revoked key. |
403 |
forbidden |
Key lacks leads:read — it is opt-in, so a key that was never granted it lands here. Re-create the key with the scope. |
404 |
not_found |
The site is not a member, or the Forms app is not installed on it. On /v1/leads/{id}, also: the lead does not exist, or belongs to another site. All of these are the same opaque 404. |
422 |
— | Invalid filter on the list: unknown status, unknown ordem, unparseable de/ate, an inverted period, or a malformed cursor. The message says which. |
429 |
rate_limited |
The strict leads bucket (10/min) or the general bucket (60/min). Honour Retry-After. |
5xx |
upstream |
Transient upstream error; retry with backoff. |
Handling the data responsibly
Section titled “Handling the data responsibly”- Grant
leads:readto one key, for one job. A publishing key does not need it. One key per consumer keeps the audit trail readable and the blast radius small. - Assume the read is on the record. Every call you make is attributed to the key in the audit trail, including calls that end in
404. - Do not mirror leads into a system with weaker access control than the Canverly admin, and do not log full responses — that turns a debug log into a copy of the contact base.
- Retry politely. On
429, sleep forRetry-Afterseconds. At 10 req/min, a fixed pace of one request every 6 seconds never trips the limit.