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. Accepts JSON only.

Auth: Embed token OR API key
Request (application/json)
FieldTypeReqDescription
x-embed-token headerstringnoEmbed token (cf_embed_...) passed as a request header. Alternative to embedToken body field.
x-api-key headerstringnoAPI key (cf_api_...) as a request header. For server-side use only.
embedTokenstringnoEmbed token in the JSON body. Alternative to X-Embed-Token header.
formDataobjectnoKey/value map of your form fields (e.g., { name, email, message }). All keys are stored as submission data.
honeypotstringnoAnti-spam field. The embed script manages this automatically. You do not need to include it in manual integrations.
Responses
HTTP 200
{"message":"Form submission received successfully","submissionId":"abc123xyz","status":"accepted","usage":{"current":42,"limit":1000,"overage":0}}
HTTP 200
{"message":"Form submission received successfully","submissionId":"abc123xyz","status":"flagged_as_spam","usage":{...}}
HTTP 400
{"error":"Invalid form data","details":[{"path":["formData"],"message":"Expected object"}]}
HTTP 401
{"error":"No authentication token provided. Include X-API-Key header or X-Embed-Token header."}
HTTP 401
{"error":"Domain not allowed for this embed token"}
HTTP 429
{"error":"Rate limit exceeded. Please try again later."}
HTTP 429
{"error":"Monthly submission limit reached","usage":{"current":1000,"limit":1000,"overage":0}}
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. No auth token required.

Auth: None (public): access controlled by form's allowed domains setting
Request (application/json)
FieldTypeReqDescription
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 201
{"success":true,"message":"Form submitted successfully","submissionId":"abc123xyz"}
HTTP 201
{"success":true,"message":"Form submitted successfully","submissionId":"abc123xyz","redirectUrl":"https://yoursite.com/thank-you"}
HTTP 400
{"error":"Validation failed","details":[...]}
HTTP 403
{"error":"Domain not allowed"}
HTTP 404
{"error":"Form not found"}
GET /api/public/forms/{formId}/partial

Load a saved partial (in-progress) submission by session ID. No auth token required.

Auth: None (public): access controlled by form's allowed domains setting
Request
FieldTypeReqDescription
formIdstring (path)yesPublic form ID.
sessionIdstring (query)yesSession ID from a prior POST /partial call.
Responses
HTTP 200
{"sessionId":"a1b2c3...","submissionPublicId":"sub_xyz789","data":{"name":"Alice","email":"alice@example.com"}}
HTTP 404
{"error":"Draft not found"}
POST /api/public/forms/{formId}/partial

Create or update an in-progress submission. Returns a session ID for resume. No auth token required.

Auth: None (public): access controlled by form's allowed domains setting
Request (application/json)
FieldTypeReqDescription
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
{
  "success": true,
  "data": [
    {
      "id": 1,
      "name": "Starter",
      "slug": "starter",
      "description": "Perfect for personal projects and small sites",
      "submissionLimit": 1000,
      "pricing": [
        { "billingType": "monthly", "priceCents": 900, "currency": "USD" },
        { "billingType": "yearly",  "priceCents": 8640, "currency": "USD" }
      ],
      "features": [
        { "featureKey": "spam_filtering", "displayName": "Spam filtering" },
        { "featureKey": "email_notifications", "displayName": "Email notifications" }
      ]
    },
    ...
  ]
}
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, as 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, with 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