Docs

Three ways to get a form in front of someone, and one API to work the responses afterwards.

Embedding

Drop this where the form should appear:

<div data-crawlink-form="FORM_ID"></div>
<script src="https://form.crawlink.dev/embed.js" async></script>

The script replaces the div with an iframe and keeps it exactly as tall as the form, using a postMessage from inside. Several forms on one page is fine.

Optional attributes:

  • data-height — a fixed height in pixels; auto-sizing is switched off.
  • data-hide-header — drop the form's title and description.
  • data-prefill — a JSON object of field keys to values, for hidden fields and known-customer defaults.

Prefer a plain link? https://form.crawlink.dev/f/FORM_ID is the hosted page.

Domain allow-list

By default a form embeds anywhere, which is what makes it useful out of the box. Add one domain under Settings → Domains and the list becomes exclusive: only the domains on it can embed the form or post to its public endpoint. *.example.com matches every subdomain.

Submitting from your own code

No key needed — this is the endpoint the embed itself uses.

POST https://form.crawlink.dev/api/public/forms/FORM_ID/submissions
Content-Type: application/json

{ "full_name": "Asha Rao", "email": "asha@example.com" }

The keys are the field keys shown next to each field in the builder. A validation failure comes back as 422 with a per-field list:

{
  "message": "Some answers need fixing",
  "errors": [{ "key": "email", "message": "Email is not a valid email address." }]
}

A workspace that has used its monthly response limit answers 402. A form that is closed answers 409. Rate limiting answers 429.

API keys

Mint one under Settings → API keys. You see the key once; we store only its hash. Each key carries scopes and, optionally, a list of forms it may touch.

ScopeAllows
forms.readList forms and read their definitions
data.readList and read responses
data.writeCreate and update responses
data.deleteDelete responses
files.readDownload uploaded files

Send it as a bearer token:

curl https://form.crawlink.dev/api/v1/forms \
  -H "Authorization: Bearer fk_ORGID_..."

REST API

MethodPathScope
GET/api/v1/formsforms.read
GET/api/v1/forms/{id}forms.read
GET/api/v1/forms/{id}/submissionsdata.read
POST/api/v1/forms/{id}/submissionsdata.write
GET/api/v1/forms/{id}/submissions/{sid}data.read
PATCH/api/v1/forms/{id}/submissions/{sid}data.write
DELETE/api/v1/forms/{id}/submissions/{sid}data.delete
GET/api/v1/usageany

Listing responses

?page, ?pageSize (max 200), ?q for free text, ?from / ?to as epoch milliseconds, ?sortBy as a field key and ?sortDir. Filter on a field with ?where=field:operator:value, repeatable, joined with ?match=all|any.

GET /api/v1/forms/ID/submissions?where=rating:gte:4&where=city:eq:Pune&match=all

What counts against your plan

  • One response per row written, whether it came from the hosted page, the embed, the API or an import.
  • One row view per row handed back to a reader. A page of 50 in the dashboard costs 50; so does a page of 50 from the API; so does exporting 50 rows.
  • Bytes downloaded against the monthly download meter, when a file is served.

Counting happens inside your workspace's own database, in the same transaction as the write, so the number on your usage page is the number you were charged for. GET /api/v1/usage returns it live.

Webhooks

Point one at your endpoint under a form's Share tab. We POST the response as JSON. With a secret set, the body is signed:

X-Form-Signature: sha256=<hex hmac of the raw body>
X-Form-Event: submission.created
X-Form-Delivery: <unique id>

Verify against the raw bytes, not a re-serialised object. Any 2xx is success; anything else is retried at 1 minute, 5, 25, 2 hours and 10 hours before we give up.