Skip to content
Lumina
Developer documentationManage API keys

Build with Lumina

Your forms.
Your workflow.

A REST API for forms and responses. Authenticated API-key requests are scoped to the workspace that owns the key. Base URL: https://luminaforms.app/api/v1.

Authentication

Create a key in Settings API keys. Copy the secret once and store it on your server. Send it as a Bearer token; never put it in browser JavaScript, URLs, source control, shell history, or logs.

Scopes are independent: forms:write does not include forms:read, and submissions:write does not include submissions:read. Select each scope your integration needs. Revoking a key rejects subsequent authentication attempts; it does not undo completed operations.

Settings and webhook management require an authorized user session, not an API key. Form deletion is also unavailable to API keys. Permanent response deletion requires an owner or admin session. Platform operator routes are internal, separately authorized operations; maintenance changes require a configured root operator. An API key never grants operator access.

API-key endpoints

Paths below are relative to the base URL. Replace {formId} and {submissionId} with internal UUIDs returned by authenticated API responses. The form’s separate publicId is used for respondent links, not creator endpoints.

Method and pathRequired scopeBehavior
GET /formsforms:readList forms with limit and offset.
POST /formsforms:writeCreate a form; the default status is draft.
GET /forms/{formId}forms:readRead one form.
PATCH /forms/{formId}forms:writeUpdate a form.
POST /forms/{formId}/duplicateforms:writeCreate a new draft copy without responses.
POST /forms/{formId}/publishforms:writeSet the form status to active.
POST /forms/{formId}/archiveforms:writeSet the form status to archived.
GET /submissionssubmissions:readList filtered workspace responses.
GET /submissions/statssubmissions:readRead total/unread counts and the last 14 UTC calendar days.
GET /submissions/{submissionId}submissions:readRead one workspace response.
PATCH /submissions/{submissionId}submissions:writeUpdate one response status.
POST /submissions/bulksubmissions:writeArchive 1–100 response IDs. Permanent deletion is session-only.
GET /forms/{formId}/submissionssubmissions:readList filtered responses for one form.
GET /forms/{formId}/submissions/{submissionId}submissions:readRead one response within a form.
PATCH /forms/{formId}/submissions/{submissionId}submissions:writeUpdate one response status within a form.
GET /forms/{formId}/submissions/exportsubmissions:readExport every matching response as CSV, oldest first.
GET /forms/{formId}/analyticssubmissions:readRead a form analytics report; offset pages its response sample.

This table covers the forms and responses API-key surface. Respondent submission, draft, upload, payment, and AI routes have separate access and configuration requirements. Their presence in OpenAPI does not grant API-key access or guarantee that an optional integration is configured. Account, billing, and platform administration are not part of this API-key guide.

Create a form

POST /forms accepts JSON and returns 201 with the form object directly, not a data wrapper. Omitting status creates a draft. Use unique field keys and explicit positions to preserve question ordering.

{
  "name": "Contact us",
  "fields": [
    {
      "key": "email",
      "label": "Your email",
      "type": "email",
      "required": true,
      "position": 0
    },
    {
      "key": "message",
      "label": "How can we help?",
      "type": "long_text",
      "position": 1
    }
  ]
}

Save this JSON as form.json. With the key securely injected as above, create the draft:

curl --fail-with-body --config - --data-binary @form.json <<EOF
url = "https://luminaforms.app/api/v1/forms"
header = "Authorization: Bearer ${LUMINA_API_KEY:?Set LUMINA_API_KEY securely first}"
header = "Content-Type: application/json"
EOF

Read id and publicId from the result. Publish with POST /forms/FORM_ID/publish and share https://luminaforms.app/f/PUBLIC_ID. Publish, archive, and update return the form object with 200. Creation and duplication remain subject to workspace quotas.

Read responses

GET /forms/FORM_ID/submissions returns one form’s responses. GET /submissions queries the workspace; it does not accept a form filter. Use the form-specific route instead.

# Inject LUMINA_API_KEY through your server's secret manager first.
# Do not enable shell tracing or curl verbose/trace output.
# The header goes over stdin, not in curl's command-line arguments.
curl --fail-with-body --config - <<EOF
url = "https://luminaforms.app/api/v1/submissions?status=unread&limit=25&offset=0"
header = "Authorization: Bearer ${LUMINA_API_KEY:?Set LUMINA_API_KEY securely first}"
EOF

Form lists and response lists return { items, total, limit, offset }. The default limit is 50, the maximum is 100, and offset starts at 0. Increase offset by the requested limit until the page is empty or you have reached total. Offset pagination is not a snapshot: concurrent inserts or deletions can shift results.

Response filterValue
searchNon-empty search text, up to 200 characters.
statusunread, read, archived, or spam.
minLeadScoreAn integer from 0 to 100.
isSpamThe literal query value true or false.
fromInclusive UTC ISO timestamp, for example 2026-01-01T00:00:00.000Z.
toExclusive UTC ISO timestamp later than from. Use the same UTC precision for both.
sortasc or desc by creation time; defaults to desc on response lists.

URL-encode filter values. These response filters do not apply to GET /forms, which accepts only limit and offset. A single-response endpoint returns the response object directly.

Update responses

PATCH /submissions/RESPONSE_ID accepts the following JSON and returns the updated response with 200:

{
  "status": "archived"
}

Valid statuses: unread, read, archived, spam. POST /submissions/bulk accepts { "ids": ["RESPONSE_ID"], "action": "archive" }, with 1–100 UUIDs, and returns { "changed": 1 }. Bulk operations are not a promised all-or-nothing transaction; reconcile actual response state after an interrupted request.

CSV export

GET /forms/FORM_ID/submissions/export downloads UTF-8 CSV with columns id, number, status, createdAt, payload. It accepts search, status, minLeadScore, isSpam, from, and to. It exports all matching rows, oldest first; limit, offset, and sort do not select the exported page or order.

The file includes a BOM for spreadsheets, escaped cells, and formula-injection protection. Responses may contain personal data: store exports with restricted access and an appropriate retention policy.

Signed webhooks

Use an authorized session in Settings Webhooks to register an HTTPS endpoint for a form. API-key scopes do not authorize webhook management. Save the signing secret shown at creation and keep it on your server.

Read the Lumina-Signature header: t=UNIX_SECONDS,v1=HEX_SIGNATURE. Compute HMAC-SHA256 with the signing secret over timestamp + "." + rawBody, using the exact UTF-8 request body before parsing JSON. Compare the hexadecimal signature using a constant-time comparison. Reject malformed signatures and timestamps more than 300 seconds from your server’s current time; keep your server clock synchronized.

After verification, deduplicate using the JSON envelope’s id, not Webhook-Attempt-Id (which identifies an individual attempt). Submission events use type: submission.created with response content under data; connectivity tests use type: webhook.test. Do not assume a test event contains a submission.

A stored response does not guarantee webhook delivery. The API’s submission handler dispatches in the background and does not itself promise durable retries. Return a successful HTTP response only after your receiver has safely accepted the event. Use API reconciliation when delivery is missing; test connectivity before relying on the integration.

Errors and limits

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "details": {
      "formErrors": [],
      "fieldErrors": {
        "name": [
          "Required"
        ]
      }
    }
  }
}

400 invalid input · 401 missing, expired, or revoked credentials · 402 plan or payment requirement · 403 insufficient scope or session permission · 404 missing or inaccessible resource · 408 request-body timeout or interruption · 409 conflict · 413 oversized payload · 429 rate limited · 500 internal error · 501 unavailable implementation · 503 service unavailable.

Error bodies contain error.code and error.message; details is optional and varies by endpoint. Retain X-Request-Id for support without logging credentials or respondent payloads. Edge failures may return non-JSON bodies, so check content type before parsing.

Limits vary by endpoint and deployment; this guide does not promise a universal API-key request quota. Honor Retry-After when supplied on 429 or maintenance 503 responses. Retry safe reads with bounded exponential backoff and jitter. Do not automatically replay writes after a timeout: a failed acknowledgement can follow a successful commit.

Form creation, duplication, and response mutations do not support a general Idempotency-Key contract. Reconcile existing records before retrying a write; sending that header does not prevent duplicates. Maintenance can temporarily block otherwise-authorized API keys. AI features are disabled for this release. Optional payment, upload and email integrations require deployment configuration and acceptance checks; an error is not successful delivery or settlement.