chatformdocs

Pagination

Cursors, and why they are not page numbers.

List endpoints return a page and a cursor:

{
  "data": [ /* … */ ],
  "has_more": true,
  "next_cursor": "eyJvcmRlciI6ImNyZWF0ZWQi…"
}

Ask for the next page by handing it back:

curl "https://api.chatform.in/v1/forms/$FORM_ID/responses?limit=50&cursor=$NEXT" \
  -H "x-api-key: $CHATFORM_SECRET_KEY"

Stop when has_more is false. limit defaults to 25 and caps at 100.

Every list answers in that shape, including the bounded ones. A form has a handful of versions and an organization a handful of webhooks, so /v1/templates, /v1/webhooks, /v1/forms/{id}/versions, /v1/forms/{id}/integrations and /v1/exports return everything they have and has_more is always false. Read them the same way as the rest and nothing will need changing on the day one of them grows a cursor.

Two lists are not in that shape, because they answer with more than the list:

  • GET /v1/forms/{id}/knowledge returns { sources, usage, enabled }: the sources, how much of the plan's allowance they use, and whether the plan has a knowledge base at all.
  • GET /v1/payment-accounts returns { accounts, enabled, providers }: the connected accounts, whether verified payments are switched on, and which gateways can be connected.

Neither is paginated.

Why not offsets

Responses arrive while you are reading them. With ?page=2, a new response pushes everything down by one — so you see a row twice and miss another, and nothing tells you it happened.

A cursor is anchored to a row rather than a position, so a page boundary means the same thing however much has changed. It is signed, so a modified one is rejected rather than quietly returning the wrong window.

Ordering

order=created (default) or order=updated. A cursor belongs to the ordering it was issued for; using one with the other is a 400.

order=updated with updated_since is how you poll for changes without re-reading everything:

curl "https://api.chatform.in/v1/forms/$FORM_ID/responses?order=updated&updated_since=$LAST_SEEN" \
  -H "x-api-key: $CHATFORM_SECRET_KEY"

Though if you are polling for new responses, a webhook is better than both.

Iterating

async function* allResponses(formId) {
  let cursor;
  do {
    const url = new URL(`https://api.chatform.in/v1/forms/${formId}/responses`);
    url.searchParams.set("limit", "100");
    if (cursor) url.searchParams.set("cursor", cursor);

    const page = await fetch(url, {
      headers: { "x-api-key": process.env.CHATFORM_SECRET_KEY },
    }).then((r) => r.json());

    yield* page.data;
    cursor = page.next_cursor;
  } while (cursor);
}

On this page