Skip to content

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_… — scope leads: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.

List the site’s leads, newest first, paginated by cursor.

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.

Janela do terminal
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"
{
"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.

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.

Janela do terminal
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.

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.

Janela do terminal
curl "https://api.canverly.com/v1/leads/01K8Z9K3F7T8M2QYV5N6B4WJ8R" \
-H "Authorization: Bearer ck_live_xxx"
{
"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"
}
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.

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.

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.
  • Grant leads:read to 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 for Retry-After seconds. At 10 req/min, a fixed pace of one request every 6 seconds never trips the limit.