Forms and leads
From a contact form on your site to a lead your sales team can follow up.
5 min read
How it works
A form is a small definition an admin saves for the brand: a key, a list of fields and a success message. Your site posts visitor input to the public endpoint. ATLAS validates it against the definition, stores it as a lead, queues a forward to your CRM, notifies your team and, when the visitor consented, tells the ad platforms about the conversion.
Forms depend on the forms module. When a brand switches that module off, the public endpoint answers 404 and the manage calls for forms and leads answer 403.
Define a form
Forms are managed by an admin. All calls are under https://api.atlascms.id/v1/manage and need a signed in session plus the X-Brand-Id header.
/v1/manage/formsLists the forms of the brand, ordered by key. POST /v1/manage/forms creates one (201), PUT /v1/manage/forms/{form} updates it and DELETE /v1/manage/forms/{form} removes it (204). Every change is written to the audit log.
| Field | Rule |
|---|---|
key | Required on create, cannot change later. Lowercase letters, digits, _ and -, starting with a letter, up to 48 characters, unique in the brand. |
name | Required, up to 120 characters. |
fields | Required, 1 to 30 fields. |
success_message | Optional, up to 255 characters. Sent back to the visitor. |
active | Optional boolean. An inactive form answers 404 to visitors. |
Each field in fields has these properties:
| Property | Rule |
|---|---|
name | Required. Lowercase letters, digits and _, starting with a letter, up to 32 characters, unique in the form. |
type | Required: text, email, phone, textarea or select. |
required | Optional boolean. |
max | Optional, 1 to 5000. The length limit. Text fields are capped at 1000 and textareas at 5000 whatever you set. |
options | For select: up to 50 allowed values, each up to 120 characters. |
{
"key": "contact",
"name": "Contact form",
"fields": [
{ "name": "name", "type": "text", "required": true },
{ "name": "email", "type": "email", "required": true },
{ "name": "phone", "type": "phone" },
{ "name": "topic", "type": "select", "options": ["Sales", "Support", "Partnership"] },
{ "name": "message", "type": "textarea", "max": 2000 }
],
"success_message": "Thanks, we will reply within one working day.",
"active": true
}Name fields `email` and `phone`
The tracking events below read the fields named email and phone. Use those names if you want conversions matched on the ad platforms.
Submit from your site
/v1/public/forms/{key}Send the visitor's input as JSON. Only fields the form declares are accepted and stored. The brand comes from your read token (Authorization: Bearer atl_read_...) or from the Host header.
| Field | Description |
|---|---|
fields | Required object keyed by field name. |
consent | Optional boolean. Stored with the lead and decides whether a tracking event is sent. |
utm | Optional object with utm_source, utm_medium, utm_campaign, utm_term, utm_content and utm_id, each up to 255 characters. |
fbclid, gclid | Optional click ids, up to 255 characters. |
source_page, referrer | Optional, up to 2048 characters. |
locale | Optional, up to 8 characters. |
website | Honeypot. Keep it in the page as a hidden input that people never fill. |
curl -s -X POST "https://api.atlascms.id/v1/public/forms/contact" \
-H "Authorization: Bearer $ATLAS_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Atlas-Client-Ip: 203.0.113.7" \
-d '{
"fields": { "name": "Sari", "email": "[email protected]", "message": "Hello" },
"consent": true,
"utm": { "utm_source": "newsletter" },
"source_page": "/contact",
"website": ""
}'{ "data": { "id": "01m518vvbf8txh40wcvwhvtzyd", "message": "Thanks, we will reply within one working day." } }| Status | Meaning |
|---|---|
| 201 | Stored. The body holds the lead id and your success_message. |
| 202 | The honeypot website field was filled. The visitor sees the same message, but nothing is stored. |
| 404 | Unknown or inactive form, or the forms module is off. |
| 422 | A field failed validation. Body: { "message", "errors": { "field": ["..."] } }. |
| 429 | More than 10 submissions a minute for the same token and visitor address. |
Validation follows the field type: email must be a valid address (up to 254 characters), phone accepts an optional +, digits, spaces, brackets and hyphens (6 to 20 characters), and select must be one of the declared options.
Pass the visitor, not your server
When your site's server posts the form, send the read token plus two headers so the visitor is judged and stored, not your server.
| Header | Value |
|---|---|
X-Atlas-Client-Ip | The visitor's address. It is used for the rate limit and stored only as a salted hash. |
X-Atlas-Client-User-Agent | The visitor's user agent, kept up to 255 characters. |
ATLAS believes these headers only on a call that carries a read token. Without one, the connection address is used. The raw IP address is never stored.
Work the lead inbox
Leads hold personal data. Every call in this section needs a publisher or admin. An editor gets 403, even to read.
A lead has two statuses. status is where it stands with your team: new (default), contacted, qualified, spam or archived. delivery_status is the CRM forward: pending, forwarded, failed or skipped.
/v1/manage/form-submissionsFilters: form (the form key), status, delivery, assigned (a member id, me or none), from and to (Y-m-d days in the brand timezone), q (searches the submitted fields, the note and the source page), sort (created_at or status, prefix - to reverse) and per_page (1 to 100, default 50). meta.status_counts gives the count per status for the current filters.
| Call | What it does |
|---|---|
GET /v1/manage/form-submissions/{id} | The lead, its assigned member and up to 50 activities (received, status_changed, note_updated, assigned, delivery). Activities never contain submitted values. |
PATCH /v1/manage/form-submissions/{id} | Change status, note (up to 2000 characters, null clears it) or assigned_to (a member of the brand, or null). Nothing else can change. |
POST /v1/manage/form-submissions/bulk | { "action": "status", "status": "contacted", "ids": [...] } or { "action": "delete", "ids": [...] }, 1 to 200 ids. Answers { action, affected, skipped }. |
DELETE /v1/manage/form-submissions/{id} | Permanently deletes the personal data (204). |
POST /v1/manage/form-submissions/{id}/retry | Queues the CRM forward again. |
GET /v1/manage/leads/summary | Totals for a window (default last 30 days), split by form, status and day. |
GET /v1/manage/leads/assignees | Members a lead can be assigned to: id, name and avatar_url only. |
curl -s -X PATCH "https://api.atlascms.id/v1/manage/form-submissions/01m518vvbf8txh40wcvwhvtzyd" \
-H "X-Brand-Id: $BRAND_ID" -H "Content-Type: application/json" \
--cookie "$SESSION" \
-d '{ "status": "contacted", "note": "Called on Monday", "assigned_to": null }'Spam is counted separately: the summary and the dashboard count every submission that is not spam.
Export to CSV
/v1/manage/form-submissions/exportTakes the same filters as the list and streams a text/csv download (UTF-8 with BOM, 10 requests a minute). Columns are id, created_at, form, status, delivery_status, locale, consent, source_page, referrer, the utm_* values, fbclid, gclid, note, and one field_<name> column per form field. The export is written to the audit log with its filters.
A cell that starts with =, +, -, @, a tab or a carriage return gets a leading ', so a spreadsheet never runs a submitted value as a formula.
Forward leads to a CRM
Right after a lead is stored, ATLAS queues a forward to the brand's crm integration. Without a configured endpoint the lead stays in ATLAS with delivery_status: skipped. A failed delivery is tried 5 times in total, waiting 10 seconds, 1 minute, 5 minutes and 15 minutes between tries, then marked failed with a short reason. Use retry to send it again.
Configure it with PUT /v1/manage/integrations/crm (admin):
| Config key | Description |
|---|---|
endpoint | Public HTTPS address on port 443. Private addresses are refused. |
auth_header | Header that carries the token: Authorization (default), X-Website-Api-Key or X-Api-Key. |
auth_scheme | Bearer (default) sends Bearer <token>. none sends the raw token. |
payload | atlas (default) sends the full lead. flat puts the form fields at the top level, flattens utm_source, utm_medium and utm_campaign, and adds atlas_lead_id and source_page. |
{
"enabled": true,
"config": {
"endpoint": "https://crm.example.com/api/leads",
"auth_header": "X-Api-Key",
"auth_scheme": "none",
"payload": "atlas"
}
}The token is a write only secret. Save it with PUT /v1/manage/secrets/crm/token and body { "value": "..." }. It is encrypted, never returned and never written to logs or audit rows. Saving a different endpoint deletes the stored token, so enter it again.
The default atlas payload looks like this. Each request also carries X-Atlas-Delivery with the lead id, so your CRM can ignore a repeat.
{
"id": "01m518vvbf8txh40wcvwhvtzyd",
"brand": "atlascms",
"form": "contact",
"locale": "en",
"fields": { "name": "Sari", "email": "[email protected]", "message": "Hello" },
"utm": { "utm_source": "newsletter" },
"fbclid": null,
"gclid": null,
"source_page": "/contact",
"referrer": null,
"consent": true,
"submitted_at": "2026-10-21T14:42:20+00:00"
}Conversion tracking
When a visitor ticks consent ("consent": true), ATLAS also sends a Lead conversion event from the server to every enabled ad platform integration (meta_pixel with its Conversions API token, tiktok_pixel with its access token). The lead id is the event id, so a matching browser pixel event is deduplicated. Email and phone are hashed before they leave ATLAS. A lead without consent sends no event.
Send a test event with POST /v1/manage/integrations/{provider}/test (admin, 10 a minute). An optional test_event_code keeps it out of real reports.
Notifications
When a lead arrives, every publisher and admin of the brand gets an in app notification. The first 5 leads in a 10 minute window notify one by one. After that, one roll up notification with a count is sent per window, so a burst never floods the team.
Privacy rules
- Only publishers and admins can read, change, export or delete leads. A lead of another brand is a 404.
- Visitor IP addresses are stored only as a salted hash.
- Secrets, tokens and submitted values never appear in audit rows, logs or URLs. The audit log records that a lead changed, never the note text.
- Brand backups leave lead fields out. See Backups.
- Deleting a lead is permanent. Delete on request, and keep your own privacy notice in step with what you collect.
Was this page helpful?