Docs

Server-to-server submission of form data via REST. Authenticate with a tenant API token, post a JSON payload, get a submission ID back.

HTTP ingestion API

The HTTP ingestion API lets your backend submit form data programmatically without a browser or form widget. One POST per submission — you get a submission ID back, and the result is indistinguishable from a user filling the form manually.


Endpoint

POST /api/v1/tenant/forms/:form_id/submissions

:form_id is the UUID of the form definition. The form must be in published status; submitting to a draft or archived form returns 422 form_not_accepting_submissions.


Authentication

Pass a tenant API token as a Bearer token in the Authorization header.

Authorization: Bearer fy_pat_<your-token>

The token must carry the forms:ingest scope. Tokens with admin:* are also accepted. To mint a token with the correct scope, go to Settings → API tokens in the admin portal.

If the token is missing or does not have the required scope, the endpoint returns 401 Unauthorized or 403 Forbidden respectively.


Request

Headers

HeaderRequiredDescription
AuthorizationYesBearer fy_pat_<token>
Content-TypeYesapplication/json
Idempotency-KeyNoDeduplication key — see Idempotency
X-Client-NowNoRFC 3339 timestamp from the submitting device; stored in submission metadata. Ignored if malformed.

Body

{
  "payload": {
    "field_key": "value"
  }
}
FieldTypeRequiredDescription
payloadobjectYesKey-value map of field answers. Keys must match the field keys defined in the form schema.
client_ipstringNoOverride the IP address recorded in submission metadata. Only honoured when the token also holds the forms:ingest:trust_client_ip scope (for proxied / gateway senders). Ignored otherwise.

The field values in payload are validated against the form's published schema. Required fields must be present; type constraints (text, number, email, etc.) are enforced. Validation failures return 422 validation_failed with a per-field error list.


Response

201 Created — new submission

{
  "submission_id": "01jx3p...",
  "was_idempotent_replay": false
}

200 OK — idempotent replay

When an Idempotency-Key header was supplied and a matching prior submission exists, the endpoint returns 200 instead of 201 with the original submission_id and "was_idempotent_replay": true. No duplicate row is written.

{
  "submission_id": "01jx3p...",
  "was_idempotent_replay": true
}

Idempotency

Include an Idempotency-Key header to make requests safe to retry.

Idempotency-Key: order-12345-attempt-1

Key format: 1–128 characters, allowed characters A-Z a-z 0-9 . _ -. Any other characters return 400 bad_idempotency_key_format.

How it works:

  1. On the first request with a given key, the submission is written and the key is associated with the resulting submission_id.
  2. Retrying with the same key and the same payload returns 200 with the original submission_id. No second submission is created.
  3. Retrying with the same key but a different payload returns 409 idempotency_key_payload_mismatch. The key cannot be reused with different data.

TTL: Idempotency records expire after 24 hours. After expiry, the same key may be reused and will be treated as a fresh submission.

Payload hashing: Sameness is determined by a canonical SHA-256 hash of the payload object. Object key ordering is normalized before hashing, so {"b": 2, "a": 1} and {"a": 1, "b": 2} are treated as identical.

Concurrency: If two requests arrive simultaneously with the same key, one wins and proceeds; the other spins briefly and then returns 200 with the winner's submission_id. If the key is still in flight after the retry budget is exhausted, a 422 in_flight response is returned — wait a moment and retry.


Rate limiting

The endpoint is rate-limited to a burst of 60 requests per (token, form) pair. After exhausting the burst, requests return 429 Too Many Requests with a Retry-After header and a retry_after_secs field in the response body.

HTTP/1.1 429 Too Many Requests
Retry-After: 3

{
  "error": {
    "code": "rate_limited",
    "retry_after_secs": 3,
    "message": "rate limited"
  }
}

Error reference

All errors share the envelope shape { "error": { "code": "...", "message": "..." } }. Some codes include additional fields noted below.

HTTP statusError codeDescription
400bad_idempotency_key_formatIdempotency-Key contains disallowed characters or exceeds 128 chars.
400malformed_jsonRequest body is not valid JSON.
401unauthorizedAuthorization header is absent or the token is revoked.
403scope_requiredToken exists but does not have forms:ingest scope. Response includes "required": "forms:ingest".
404form_not_foundNo published form with that ID belongs to your tenant. Also returned for cross-tenant ID guessing.
409idempotency_key_payload_mismatchThe idempotency key was previously used with a different payload.
422tier_requiredYour tenant is on Solo tier. Response includes "required": "pro" and "current": "hobby".
422form_not_accepting_submissionsThe form is not in published status.
422validation_failedOne or more fields failed schema validation. Response includes "field_errors": [{ "field": "...", "message": "..." }].
422in_flightAn idempotency key is being processed by a concurrent request. Retry after a short delay.
429rate_limitedBurst quota exceeded. Respect the Retry-After header. Response includes "retry_after_secs".
500internalUnexpected server error. The body message is always "internal server error" — no internal details are leaked.

Example

curl -X POST https://app.fastyoke.io/api/v1/tenant/forms/FORM_ID/submissions \
  -H "Authorization: Bearer fy_pat_YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: my-batch-job-row-42" \
  -d '{
    "payload": {
      "name": "Alice",
      "email": "alice@example.com"
    }
  }'

Successful response:

{
  "submission_id": "01jx3pabcdefghijklmn",
  "was_idempotent_replay": false
}