API Reference

Public API reference

This page documents every endpoint your integration code should call. Internal, admin, and webhook endpoints are not listed here.

Base URL
https://my.contactfire.com

Authentication

The submission endpoint accepts two authentication methods:

Embed token (frontend)

Pass as a form field named _token:

_token=cf_embed_...
API key (backend)

Pass as an HTTP header:

x-api-key: cf_api_...

Token management and form management endpoints (listed below with "JWT session" auth) require you to be logged in to my.contactfire.com. These endpoints are for programmatic account management — use them from your own backend scripts or automation, not from client-side code.

POST /api/form/submit

Submit a form. The primary integration endpoint.

Auth: Embed token OR API key
Request application/x-www-form-urlencoded, multipart/form-data, or application/json
FieldTypeReqDescription
_tokenstringyesEmbed token (cf_embed_...). Alternative to x-api-key header.
x-api-key headerstringnoAPI key (cf_api_...) as a request header. Alternative to _token field.
_redirectstringnoURL to redirect to after successful submission (HTML form use only).
_honeypotstringnoSpam trap — must be empty. Submissions with a non-empty honeypot are discarded.
[any other field]anynoAll other fields are stored as submission data.
Responses
HTTP 200
{"status":"success","message":"Submission received","submissionId":"abc123"}
HTTP 400
{"status":"error","message":"Missing required token","code":"MISSING_TOKEN"}
HTTP 403
{"status":"error","message":"Invalid token","code":"INVALID_TOKEN"}
HTTP 403
{"status":"error","message":"Domain not allowed","code":"DOMAIN_NOT_ALLOWED"}
HTTP 429
{"status":"error","message":"Rate limit exceeded","code":"RATE_LIMIT"}
GET /api/public/forms/{formId}

Fetch the form definition for rendering. Used by the embed script.

Auth: None (public)
Request
FieldTypeReqDescription
formIdstring (path)yes12-character public form ID.
Responses
HTTP 200
{
  "id": "abc123xyz789",
  "name": "Contact form",
  "status": "published",
  "displayMode": "standard",
  "sections": [ ... ],
  "fields": [ ... ],
  "settings": { ... }
}
HTTP 404
{"error":"Form not found"}
HTTP 403
{"error":"Form is not published"}
POST /api/public/forms/{formId}/submit

Submit data to a public Form Studio form. Supports live draft upgrades.

Auth: Embed token (x-embed-token header)
Request application/json
FieldTypeReqDescription
x-embed-token headerstringyesEmbed token (cf_embed_...).
formIdstring (path)yesPublic form ID.
dataobjectyesKey/value map of field names to submitted values.
sessionIdstringnoSession ID from a prior partial save. Upgrades the draft to a full submission.
Responses
HTTP 200
{"status":"success","submissionPublicId":"sub_abc123"}
HTTP 400
{"error":"Validation failed","details":[...]}
HTTP 403
{"error":"Domain not allowed"}
GET /api/public/forms/{formId}/partial

Load a saved partial (in-progress) submission by session ID.

Auth: Embed token (x-embed-token header)
Request
FieldTypeReqDescription
x-embed-token headerstringyesEmbed token.
formIdstring (path)yesPublic form ID.
sessionIdstring (query)yesSession ID from a prior POST /partial call.
Responses
HTTP 200
{"sessionId":"...","data":{"name":"Alice","email":"[email protected]"}}
HTTP 404
{"error":"Partial not found"}
POST /api/public/forms/{formId}/partial

Create or update an in-progress submission. Returns a session ID for resume.

Auth: Embed token (x-embed-token header)
Request application/json
FieldTypeReqDescription
x-embed-token headerstringyesEmbed token.
formIdstring (path)yesPublic form ID.
dataobjectyesPartial form data captured so far.
sessionIdstringnoPass on subsequent calls to update the same draft rather than create a new one.
Responses
HTTP 200
{"sessionId":"a1b2c3...","submissionPublicId":"sub_xyz789"}
HTTP 403
{"error":"Domain not allowed"}
GET /api/plans

List all active plans with pricing. Used to display pricing on your site.

Auth: None (public)
Responses
HTTP 200
[
  {
    "id": "plan_starter",
    "name": "Starter",
    "slug": "starter",
    "monthlyLimit": 1000,
    "pricing": {
      "monthly": { "amount": 9, "currency": "USD" },
      "yearly":  { "amount": 86, "currency": "USD" }
    },
    "features": ["Spam filtering", "Email notifications", "API access"]
  },
  ...
]
GET /api/plans/{planId}

Fetch a single plan by ID or slug.

Auth: None (public)
Request
FieldTypeReqDescription
planIdstring (path)yesPlan ID or slug (e.g., "starter", "pro").
Responses
HTTP 200
{ ... same shape as /api/plans item ... }
HTTP 404
{"error":"Plan not found"}
GET /api/apikeys

List all API keys for your account. Keys are masked — only the prefix is returned, never the full secret.

Auth: JWT session (dashboard login)
Responses
HTTP 200
[{"id":"...","name":"My server key","prefix":"cf_api_xx","createdAt":"2025-01-01T00:00:00Z"}]
HTTP 401
{"error":"Unauthorized"}
POST /api/apikeys

Create an API key. The full key is returned once — store it securely, it cannot be retrieved again.

Auth: JWT session (dashboard login)
Request application/json
FieldTypeReqDescription
namestringyesHuman-readable label for this key.
replyToFieldstringnoForm field name to use as Reply-To email address. Null means auto-detect.
notificationRecipientsstring[]noOverride email recipients for all submissions via this key. Leave empty to use account default.
Responses
HTTP 201
{"id":"...","key":"cf_api_xxxxxxxxxxxxxxxx","name":"My server key"}
HTTP 401
{"error":"Unauthorized"}
PATCH /api/apikeys/:id

Update an API key — rename it, change the reply-to field, or update notification recipients.

Auth: JWT session (dashboard login)
Request application/json
FieldTypeReqDescription
idstring (path)yesAPI key ID.
namestringnoNew label.
replyToFieldstring | nullnoField name for Reply-To. Set to null to clear.
notificationRecipientsstring[]noOverride recipients. Pass an empty array to clear the override.
Responses
HTTP 200
{"id":"...","name":"Updated key","replyToField":"email"}
HTTP 404
{"error":"Key not found"}
DELETE /api/apikeys/:id

Revoke an API key. All future requests using this key are rejected immediately — no grace period.

Auth: JWT session (dashboard login)
Request
FieldTypeReqDescription
idstring (path)yesAPI key ID to revoke.
Responses
HTTP 200
{"success":true}
HTTP 404
{"error":"Key not found"}
GET /api/embed-tokens

List all embed tokens for your account.

Auth: JWT session (dashboard login)
Responses
HTTP 200
[{"id":"...","name":"My site","token":"cf_embed_xx...","createdAt":"2025-01-01T00:00:00Z"}]
HTTP 401
{"error":"Unauthorized"}
POST /api/embed-tokens

Create an embed token. Safe to include in client-side HTML and JavaScript.

Auth: JWT session (dashboard login)
Request application/json
FieldTypeReqDescription
namestringyesLabel for this token.
allowedDomainsstring[]noRestrict the token to specific domains. Leave empty to allow all.
replyToFieldstringnoForm field name for Reply-To header.
notificationRecipientsstring[]noOverride notification recipients for submissions via this token.
Responses
HTTP 201
{"id":"...","token":"cf_embed_xxxxxxxxxxxxxxxx","name":"My site"}
HTTP 401
{"error":"Unauthorized"}
PATCH /api/embed-tokens

Update an embed token. Pass the token ID as the ?id= query parameter.

Auth: JWT session (dashboard login)
Request application/json
FieldTypeReqDescription
idstring (query)yesToken ID — passed as ?id=...
namestringnoNew label.
allowedDomainsstring[]noUpdated domain allowlist.
replyToFieldstring | nullnoField for Reply-To. Null clears it.
notificationRecipientsstring[]noOverride recipients. Empty array clears the override.
Responses
HTTP 200
{"id":"...","name":"Updated","replyToField":"email"}
HTTP 404
{"error":"Token not found"}
DELETE /api/embed-tokens

Revoke an embed token. Pass the token ID as the ?id= query parameter. Requests using this token are rejected immediately.

Auth: JWT session (dashboard login)
Request
FieldTypeReqDescription
idstring (query)yesToken ID — passed as ?id=...
Responses
HTTP 200
{"success":true}
HTTP 404
{"error":"Token not found"}
GET /api/forms/:id

Fetch a form by its ID. Returns the full form definition including fields, sections, and settings.

Auth: JWT session (dashboard login)
Request
FieldTypeReqDescription
idstring (path)yesForm ID.
Responses
HTTP 200
{"id":"...","name":"Contact form","replyToField":"email","status":"published","sections":[...],"fields":[...]}
HTTP 404
{"error":"Form not found"}
PUT /api/forms/:id

Update a form. Accepts partial updates — only provided fields are changed.

Auth: JWT session (dashboard login)
Request application/json
FieldTypeReqDescription
idstring (path)yesForm ID.
namestringnoForm display name.
replyToFieldstring | nullnoField name for Reply-To header. Null clears it.
notificationRecipientsstring[]noOverride notification recipients for this form.
settingsobjectnoForm settings (ldsEnabled, spamThreshold, etc.).
Responses
HTTP 200
{"id":"...","name":"Updated form","replyToField":"email"}
HTTP 404
{"error":"Form not found"}

Error codes

CodeStatusMeaning
MISSING_TOKEN400No embed token or API key was provided
INVALID_TOKEN403Token does not exist or has been revoked
DOMAIN_NOT_ALLOWED403Request origin is not on the token's allowed domain list
RATE_LIMIT42910 submissions per IP per minute exceeded (enforced at edge)
SPAM_DETECTED200*Submission accepted but flagged as spam — stored with spam=true
FORM_NOT_FOUND404The formId does not match any published form
FORM_NOT_PUBLISHED403The form exists but is in draft or archived state

* SPAM_DETECTED returns 200 so the form submitter does not know their message was flagged.

Rate limits

Submission endpoint10 requests / IP / minute (enforced at Cloudflare edge)
Plan monthly quotaVaries by plan — see Pricing

CORS

The public form endpoints return CORS headers based on your form's allowed domains list. If your domain is not on the list, the browser rejects the preflight request.

  • Add your site's domain to the form or embed token allowed domains list
  • Both yoursite.com and https://yoursite.com are matched
  • Server-side calls (API key via x-api-key) are not subject to CORS
Previous: Embed tokens Next: Spam filtering