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://datamold.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://datamold.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://datamold.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.
| Scope | Allows |
|---|---|
forms.read | List forms and read their definitions |
data.read | List and read responses |
data.write | Create and update responses |
data.delete | Delete responses |
files.read | Download uploaded files |
Send it as a bearer token:
curl https://datamold.crawlink.dev/api/v1/forms \
-H "Authorization: Bearer fk_ORGID_..." REST API
| Method | Path | Scope |
|---|---|---|
| GET | /api/v1/forms | forms.read |
| GET | /api/v1/forms/{id} | forms.read |
| GET | /api/v1/forms/{id}/submissions | data.read |
| POST | /api/v1/forms/{id}/submissions | data.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/usage | any |
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.