API Reference

Read your forms, responses, and contacts — and write responses and contacts — from your own code.

Base URL

https://api.signalform.io/api/v1

Every endpoint below is relative to that base URL. The v1 prefix is a stability promise: we will not remove or repurpose a field in v1, only add to it.

Authentication

Create a key under Settings → API Keys, then send it as a bearer token on every request:

curl https://api.signalform.io/api/v1/forms \
  -H "Authorization: Bearer sf_a1b2c3d4e5f6_YOUR_KEY_HERE"

Things worth knowing before you build on it:

  • The key is shown once. SignalForm stores only a hash of it, so there is no way to retrieve it later. If you lose it, revoke the key and create a new one.
  • A key belongs to one account. It can read and write only that account’s data. If you belong to several accounts, create a key in each.
  • Keys are issued by owners and admins. Team members with the member role cannot create or revoke them.
  • Revoking is immediate. So is removing the person who issued the key from the account — their keys stop working at the same moment.
  • Treat it like a password. Keep it server-side. Never ship a key in a browser bundle, a mobile app, or a public repository.

Scopes

Each key carries an explicit list of scopes, chosen when you create it. A request to an endpoint whose scope the key does not hold is rejected with 403. Grant the least a key needs — a reporting script wants reads only.

ScopeGrants
forms:readList forms and read a single form with its questions.
responses:readRead the responses submitted to a form.
responses:writeRecord a new response against a form.
contacts:readList CRM contacts and read a single contact.
contacts:writeCreate or update a CRM contact.

Scopes are fixed for the life of a key. To change them, revoke the key and issue a new one.

Endpoints

GET/formsforms:read

Lists the account's forms, newest first.

Accepts page (default 1) and per_page (default 20, max 100). Returns forms and a pagination object with page, per_page, and total.

GET/forms/{id}forms:read

Returns one form with its questions.

The questions array gives each question’s id, type, title, and required flag — you need the IDs to submit answers.

GET/forms/{id}/responsesresponses:read

Lists a form's responses, newest first.

Paginated like /forms. Each response carries answers keyed by question ID, along with submitted_at, email_verified, and respondent_email when available.

POST/forms/{id}/responsesresponses:write

Records a response against a form.

curl -X POST https://api.signalform.io/api/v1/forms/$FORM_ID/responses \
  -H "Authorization: Bearer $SIGNALFORM_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "answers": {
      "3f1c...": "Ada Lovelace",
      "9b22...": "[email protected]"
    }
  }'

answers is an object keyed by question ID. Answers are validated exactly as a browser submission is: required questions must be present and non-empty, values must match the question type, and answers to questions hidden by conditional logic are dropped. Unknown question IDs are ignored.

Responses created this way count against your plan’s monthly response allowance, the same as responses submitted through a form.

GET/contactscontacts:read

Lists the account's CRM contacts.

Paginated, and filterable with search (matches email, name, or company) and status (one of new, contacted, in_progress, won, lost).

GET/contacts/{id}contacts:read

Returns one contact.

POST/contactscontacts:write

Creates a contact, or updates the existing one with that email.

curl -X POST https://api.signalform.io/api/v1/contacts \
  -H "Authorization: Bearer $SIGNALFORM_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "name": "Ada Lovelace",
    "company": "Analytical Engines"
  }'

Contacts are deduplicated by email. Posting an email that already exists updates that contact’s name, phone, and company rather than creating a second record. Only email is required.

Rate limits

Each key gets 60 requests per minute, with a burst of 60. Exceeding it returns 429 Too Many Requests. Limits are per-key, so one busy integration will not starve another — give each integration its own key.

Errors

Errors return a JSON body with an error code and a human-readable message.

StatusMeaning
400The request body or a parameter was invalid. The message says which.
401No key, or a key that is unknown, revoked, or expired.
403The key is valid but lacks the scope this endpoint requires.
404No such resource in this key's account. Resources in other accounts report 404 too, not 403.
402A plan limit was reached — for example the monthly response allowance.
429Rate limit exceeded for this key.

Webhooks instead of polling

If you want to react to submissions as they happen, configure a webhook on the form rather than polling /forms/{id}/responses. Webhooks are set up per form in the dashboard and deliver each submission to a URL you control.

Need help?