# API methods

Complete reference for every REST API method with parameters, examples, and responses.

## API methods

Complete reference for every method exposed by the formbase REST API. Each method shows its parameters, example requests, and response shapes.

<p>
  The same reference is available as an <a href="/openapi.json">OpenAPI 3.1 description</a> for code generators, API clients, and agents.
</p>

> ℹ️ **Single endpoint, many methods**
> <p>
>     Every method is <code>POST https://api.formbase.so/api/v1</code> with a JSON body <code>{`{"method": "...", "params": {...}}`}</code>{' '}
>     and an <code>Authorization: Bearer fb_...</code> header. See <a href="/developers/overview">API overview</a> for auth and error
>     handling, and <a href="/developers/api-tokens">API tokens</a> for the token itself.
>   </p>

<h2 id="conventions">Conventions</h2>

<ul>
  <li>
    <code>params</code> may be omitted; it defaults to <code>{`{}`}</code>. An unknown method is <code>404 METHOD_NOT_FOUND</code>.
  </li>
  <li>
    A token is bound to <strong>one workspace</strong>. Naming another workspace, or a form in one, is <code>403 FORBIDDEN</code> even when
    you belong to both.
  </li>
  <li>
    <strong>Pagination.</strong> List methods return <code>{`{ items, nextCursor, hasMore }`}</code>; most also return{' '}
    <code>canPaginate</code>, which is <code>false</code> when <code>hasMore</code> is true but no cursor can resume (fuzzy search). Pass{' '}
    <code>nextCursor</code> back as <code>cursor</code>. <code>limit</code> is 1–100, default 20 — except <code>requests.list</code>, whose
    default is 25.
  </li>
  <li>
    <strong>Rate limits.</strong> 120 calls per minute per token, shared with the <a href="/developers/mcp-server">MCP server</a>;{' '}
    <code>requests.create</code> has its own 60 per minute. Failed authentication is limited separately, 30 per 15 minutes per IP, after
    which bad tokens see <code>RATE_LIMITED</code> instead of <code>UNAUTHORIZED</code>.
  </li>
  <li>
    <strong>Body size.</strong> 1 MiB. Larger bodies are rejected with <code>VALIDATION_ERROR</code>.
  </li>
  <li>
    <strong>Versioning.</strong> The path carries the version. Breaking changes ship as <code>/api/v2</code>; new methods and new response
    fields do not.
  </li>
</ul>

{/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */}

<h2 id="forms">Forms</h2>

{/* ── forms.list ──────────────────────────────────────────────────────────── */}

List forms in a workspace. Supports cursor pagination and optional fuzzy name search.

  
    Workspace ID.
  
  
    Filter by folder. Pass <code>null</code> for root-level forms only. Omit to list all.
  
  
    Fuzzy name search. Results capped at <code>limit</code>; not cursor-paginated.
  
  
    Page size (1–100).
  
  
    Pagination cursor from a previous response.
  

  
```
curl -X POST https://api.formbase.so/api/v1 \
  -H "Authorization: Bearer fb_..." \
  -H "Content-Type: application/json" \
  -d '{
    "method": "forms.list",
    "params": { "workspaceId": "ws_abc123" }
  }'
```

  
    
```
{
  "ok": true,
  "data": {
    "items": [
      {
        "id": "frm_abc123",
        "name": "Contact Form",
        "folderId": null,
        "workspaceId": "ws_abc123",
        "isPublished": true,
        "publishedAt": 1714041851000,
        "unpublishedAt": null,
        "createdAt": 1714041800000,
        "lastEditedAt": 1714042000000,
        "emoji": null
      }
    ],
    "nextCursor": null,
    "hasMore": false,
    "canPaginate": false
  }
}
```

  

{/* ── forms.get ───────────────────────────────────────────────────────────── */}

Get full details for a single form, including questions, cover, logo, and a preview URL.

  
    Form ID.
  

  
```
curl -X POST https://api.formbase.so/api/v1 \
  -H "Authorization: Bearer fb_..." \
  -H "Content-Type: application/json" \
  -d '{
    "method": "forms.get",
    "params": { "formId": "frm_abc123" }
  }'
```

  
    
```
{
  "ok": true,
  "data": {
    "id": "frm_abc123",
    "name": "Event Feedback",
    "folderId": null,
    "workspaceId": "ws_abc123",
    "isPublished": true,
    "publishedAt": 1714041851000,
    "unpublishedAt": null,
    "createdAt": 1714041800000,
    "lastEditedAt": 1714042000000,
    "emoji": null,
    "questions": [
      {
        "id": "q_1",
        "type": "email-input",
        "inputType": "email",
        "title": "Your email",
        "metadata": null
      },
      {
        "id": "q_2",
        "type": "rating-input",
        "inputType": "rating",
        "title": "Overall experience",
        "metadata": { "kind": "rating", "maxStars": 5 }
      }
    ],
    "hasContent": true,
    "cover": null,
    "logo": null,
    "isDeleted": false,
    "trashedAt": null,
    "previewUrl": "https://formbase.so/preview/abc..."
  }
}
```

  

{/* ── forms.create ────────────────────────────────────────────────────────── */}

Create a new empty form. Returns the form and a preview URL.

  
    Form name (1–255 characters).
  
  
    Workspace ID.
  
  
    Place the form in a folder. Omit to create at workspace root.
  

  
    
      
```
curl -X POST https://api.formbase.so/api/v1 \
  -H "Authorization: Bearer fb_..." \
  -H "Content-Type: application/json" \
  -d '{
    "method": "forms.create",
    "params": {
      "name": "Contact",
      "workspaceId": "ws_abc123"
    }
  }'
```

    
    
      
```
const res = await fetch('https://api.formbase.so/api/v1', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.FORMBASE_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    method: 'forms.create',
    params: { name: 'Contact', workspaceId: 'ws_abc123' },
  }),
})
const { ok, data } = await res.json()
```

    
    
      
```
import os, requests
res = requests.post(
  "https://api.formbase.so/api/v1",
  headers={"Authorization": f"Bearer {os.environ['FORMBASE_TOKEN']}"},
  json={
    "method": "forms.create",
    "params": {
      "name": "Contact",
      "workspaceId": "ws_abc123",
    },
  },
)
data = res.json()
```

  

  
    
```
{
  "ok": true,
  "data": {
    "id": "frm_new123",
    "name": "Contact",
    "workspaceId": "ws_abc123",
    "folderId": null,
    "isPublished": false,
    "createdAt": 1714041851000,
    "previewUrl": "https://formbase.so/preview/abc..."
  }
}
```

  

{/* ── forms.update ────────────────────────────────────────────────────────── */}

Update form metadata: name, folder, emoji, cover, or logo. Does not update form content (use the editor tools for that).

  
    Form ID.
  
  
    New form name (1–255 characters).
  
  
    Move form to a folder. Pass <code>null</code> to move to workspace root.
  
  
    Form emoji (max 10 characters). Pass <code>null</code> to clear.
  
  
    Cover. <code>{`{"type": "color", "color": "#ffffff"}`}</code>, <code>{`{"type": "image", "url": "https://...", "offsetY": 50}`}</code> (
    <code>offsetY</code> 0–100, default 50), or <code>{`{"type": "none"}`}</code> to remove. Image URLs must be <code>http(s)</code> or a{' '}
    <code>data:image</code> URI.
  
  
    Logo. <code>{`{"type": "icon", "name": "HeartIcon"}`}</code>, <code>{`{"type": "image", "url": "https://..."}`}</code>, or{' '}
    <code>{`{"type": "none"}`}</code> to remove. Icon names are fixed: <code>QuestionMarkIcon</code>, <code>ListBulletsIcon</code>,{' '}
    <code>ChartBarIcon</code>, <code>ClockCountdownIcon</code>, <code>HeartIcon</code>, <code>LightbulbIcon</code>,{' '}
    <code>CheckCircleIcon</code>, <code>MagnifyingGlassIcon</code>, <code>TrendUpIcon</code>, <code>EnvelopeIcon</code>,{' '}
    <code>PhoneIcon</code>, <code>CalendarIcon</code>, <code>LinkIcon</code>, <code>UsersIcon</code>.
  

<p>Pass at least one of the five updatable fields. It does not change form content — use the MCP editor tools for that.</p>

  
```
curl -X POST https://api.formbase.so/api/v1 \
  -H "Authorization: Bearer fb_..." \
  -H "Content-Type: application/json" \
  -d '{
    "method": "forms.update",
    "params": {
      "formId": "frm_abc123",
      "name": "Updated Name",
      "emoji": "📋"
    }
  }'
```

  
    
```
{
  "ok": true,
  "data": {
    "id": "frm_abc123",
    "name": "Updated Name",
    "folderId": null,
    "emoji": "📋"
  }
}
```

  

<p>
  <code>cover</code> and <code>logo</code> come back only when you sent them. A payload that matched the current state on every scalar field
  adds <code>noChange: true</code>.
</p>

{/* ── forms.publish ───────────────────────────────────────────────────────── */}

Publish a form so it can accept responses, and freeze its [field keys](/requests/field-keys) into a new snapshot. Idempotent: an
already-published form returns success with `alreadyPublished: true`, and an unpublished form is republished from its last snapshot.

  
    Form ID.
  

<p>
  A form with content blocks but no questions publishes with a warning. A form with no content at all cannot be published. Publishing does
  not create a public URL — call <a href="#share-links-create">shareLinks.create</a> for that.
</p>

{/* ── forms.unpublish ─────────────────────────────────────────────────────── */}

Take a form offline. Respondents can no longer open it. Idempotent — a form that is not published returns `alreadyUnpublished: true`.
Reversible with `forms.publish`.

  
    Form ID.
  

{/* ── forms.delete ────────────────────────────────────────────────────────── */}

Move a form to trash. Its active share links are revoked, so their public URLs stop serving.

  
    Form ID.
  

> ⚠️ **Restoring does not bring the links back**
> <p>
>     <code>forms.restore</code> returns the form, but the share links it revoked stay revoked. Mint new ones with{' '}
>     <code>shareLinks.create</code>. A form already in trash returns <code>alreadyTrashed: true</code> and keeps its original trash date.
>   </p>

{/* ── forms.restore ───────────────────────────────────────────────────────── */}

Restore a form from trash.

  
    Form ID.
  
  
    Where to restore it. Omit for its original folder, <code>null</code> for workspace root, or a folder ID.
  

<p>
  A form that is not in trash returns <code>alreadyRestored: true</code>.
</p>

{/* ── formSettings.get ────────────────────────────────────────────────────── */}

Read a form's behavior settings.

  
    Form ID.
  

<p>
  Returns <code>{`{ settings, isDefault, availableEmailDomains, defaultFromAddress, payment }`}</code>. <code>isDefault</code> is true when
  the form has no saved settings row yet and you are seeing the defaults. <code>availableEmailDomains</code> holds the verified domain ids
  you can pass as <code>emailDomainId</code>, and <code>payment</code> reports whether Stripe is connected (connecting it is a dashboard
  step).
</p>

{/* ── formSettings.update ─────────────────────────────────────────────────── */}

Update a form's behavior settings. A partial update: only the fields you send are written.

  
    Form ID.
  
  
    <code>language</code> (BCP-47, default <code>"en"</code>), <code>requireAuthentication</code>, <code>showBranding</code>,{' '}
    <code>captchaEnabled</code>, <code>passwordEnabled</code>, <code>password</code> (4 characters or more; a string implies{' '}
    <code>passwordEnabled: true</code>, <code>null</code> clears the gate).
  
  
    <code>notifyOnSubmission</code>, <code>notificationEmails</code> (array), <code>selfNotificationSubject</code>,{' '}
    <code>selfNotificationEmailBody</code>, <code>pdfGenerationEnabled</code>, <code>showViewSubmissionButton</code>. Owner emails are not
    translatable — write them in the language you want.
  
  
    <code>respondentNotificationEnabled</code>, <code>respondentNotificationTo</code> (the field id of an email question, or{' '}
    <code>null</code>), <code>respondentNotificationSubject</code>, <code>respondentNotificationBody</code>,{' '}
    <code>respondentNotificationPdfEnabled</code>.
  
  
    <code>respondentReminderEnabled</code>, <code>respondentReminderTo</code>, <code>respondentReminderSubject</code>,{' '}
    <code>respondentReminderBody</code>, <code>respondentReminderRequiredFieldIds</code>, and <code>reminderSteps</code> — idle offsets like{' '}
    <code>["1d","3d","1w"]</code>, at most 5, sorted and deduplicated on save, <code>[]</code> for none. The schedule applies to abandoned
    public-link responses and to requests alike. Pro.
  
  
    <code>redirectUrl</code> (<code>http(s)</code>; <code>null</code> or <code>""</code> clears), <code>redirectQueryParams</code> (
    <code>{`[{ paramName, fieldId }]`}</code>), <code>allowAnotherResponse</code> (mutually exclusive with a redirect),{' '}
    <code>maxSubmissionsPerRespondent</code> (0 = unlimited, max 1000), <code>editAfterSubmit</code>, <code>maxEdits</code> (max 3; 0 means
    unlimited on Pro and Business, 3 on Free).
  
  
    <code>draftRetentionDays</code> and <code>submissionRetentionDays</code> (0–36500, <code>null</code> reverts to the default). Submission
    retention is Business, and setting it clears any fixed deletion date configured in the builder.
  
  
    A verified email-domain id from <code>formSettings.get</code>, for a custom From address. <code>null</code> resets to the default
    sender.
  

<p>
  Subjects and bodies are plain text and accept <code>{`{{variable}}`}</code> placeholders; newlines become paragraphs. Customizing a
  respondent subject or body makes it translatable, so its keys appear in <code>translations.listEntries</code> right away.
</p>

{/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */}

<h2 id="submissions" class="border-t border-border pt-8">
  Submissions
</h2>

{/* ── submissions.list ────────────────────────────────────────────────────── */}

List a form's submissions, newest page first, with cursor pagination.

  
    Form ID.
  
  
    Include responses that were started but never submitted. Drafts are a Pro feature: on Free only completed submissions are listed.
  
  
    Attach stored AI translations of the answers under <code>items[].translation.display</code>, keyed like <code>display</code>.{' '}
    <code>items[].answers</code> and <code>items[].display</code> always stay the original.
  
  
    Page size (1–100).
  
  
    Pagination cursor from a previous response.
  

  
    
```
{
  "ok": true,
  "data": {
    "formId": "frm_abc123",
    "formName": "Event Feedback",
    "items": [
      {
        "id": "sub_xyz789",
        "submittedAt": "2026-05-18 18:02:28",
        "isCompleted": true,
        "createdAt": "2026-05-18 18:02:19",
        "answers": {
          "email": "user@example.com",
          "plan": "pro",
          "contacts": [{ "name": "Ada" }, { "name": "Grace" }]
        },
        "display": {
          "email": "user@example.com",
          "plan": "Pro",
          "contacts": "Ada, Grace"
        }
      }
    ],
    "nextCursor": null,
    "hasMore": false,
    "canPaginate": false
  }
}
```

  

> ℹ️ **Same answers as webhooks and callbacks**
> <p>
>     Each item carries <code>answers</code> keyed by <a href="/requests/field-keys">field key</a> and <code>display</code> with the same keys
>     as readable text — the shape a <a href="/developers/webhooks-reference">webhook payload</a>, a{' '}
>     <a href="/requests/callbacks">request callback</a> and <code>requests.get</code> carry. A choice answer is its option key, a repeating
>     group an array of instances. Call <code>fields.list</code> for each key's title and option labels. This method returns no totals.
>   </p>

{/* ── submissions.pdf ─────────────────────────────────────────────────────── */}

Get a link to one submission's PDF. Built for the Zapier connector: it returns a result only when the form has an active Zapier integration
configured to include the PDF, and the PDF was retained.

  
    Form ID.
  
  
    Submission ID. Must belong to that form and be completed.
  

  
    
```
{
  "ok": true,
  "data": {
    "url": "https://api.formbase.so/api/storage/...",
    "filename": "formbase-submission-sub_xyz789.pdf",
    "contentType": "application/pdf",
    "byteLength": 148213
  }
}
```

  

{/* ── submissions.sample ──────────────────────────────────────────────────── */}

Build a sample submission payload for a form, without any real data. It is the exact shape a share-link submission delivery carries, so
connectors use it for field discovery; a request-born submission reaches a subscription as <code>request.completed</code> instead, sampled by{' '}

<a href="#requests-sample">requests.sample</a>. <code>data.form.snapshotId</code> is the form's current published version, the same id live
events carry, or <code>null</code> while the form is unpublished.

  
    Form ID.
  

  
    
```
{
  "ok": true,
  "data": {
    "id": "evt_example000000000000",
    "type": "submission.completed",
    "createdAt": "2026-05-18T19:00:00.000Z",
    "apiVersion": "2026-09-24",
    "test": true,
    "data": {
      "form": { "id": "frm_abc123", "name": "Event Feedback", "snapshotId": "js7abc123" },
      "submission": {
        "id": "sub_example000000000000",
        "respondentEmail": "respondent@example.com",
        "submittedAt": "2026-05-18T19:00:00.000Z",
        "updatedAt": null,
        "editCount": 0,
        "pdfUrl": null,
        "language": "en"
      },
      "answers": { "your_email": "john@example.com" },
      "display": { "your_email": "john@example.com" }
    }
  }
}
```

  

<p>
  Field and payload semantics are documented once, in the <a href="/developers/webhooks-reference#payload">webhooks reference</a>.
</p>

{/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */}

<h2 id="share-links" class="border-t border-border pt-8">
  Share links
</h2>

{/* ── shareLinks.list ─────────────────────────────────────────────────────── */}

List share links for a form.

  
    Form ID.
  
  
    Include revoked links in the result.
  
  
    Page size (1–100).
  
  
    Pagination cursor.
  

  
    
```
{
  "ok": true,
  "data": {
    "items": [
      {
        "id": "sl_abc123",
        "code": "RPjNes52",
        "url": "https://formbase.so/RPjNes52",
        "customDomainUrl": null,
        "formId": "frm_abc123",
        "createdAt": 1714041851000,
        "expiresAt": null,
        "maxClaims": null,
        "claimedCount": 7,
        "isRevoked": false,
        "revokedAt": null,
        "customDomainId": null,
        "customSlug": null
      }
    ],
    "availableCustomDomains": [],
    "nextCursor": null,
    "hasMore": false,
    "canPaginate": false
  }
}
```

  

{/* ── shareLinks.create ───────────────────────────────────────────────────── */}

Create a share link for a form. The form must be published first.

  
    Form ID. A form that is unpublished, or was published and then unpublished, is rejected — call <code>forms.publish</code> first.
  
  
    Expiration as a future Unix timestamp in milliseconds. Unlike on update, <code>0</code> is not accepted here.
  
  
    Maximum number of times this link can be used. Must be positive; use <code>shareLinks.update</code> to clear it later.
  

<p>
  The response is the share link (same shape as a <code>shareLinks.list</code> item) plus <code>availableCustomDomains</code>, so you can
  follow up with <code>shareLinks.update</code> to attach one.
</p>

  
```
curl -X POST https://api.formbase.so/api/v1 \
  -H "Authorization: Bearer fb_..." \
  -H "Content-Type: application/json" \
  -d '{
    "method": "shareLinks.create",
    "params": {
      "formId": "frm_abc123",
      "maxClaims": 100
    }
  }'
```

{/* ── shareLinks.update ───────────────────────────────────────────────────── */}

Update a share link. Can change expiration, max claims, custom domain, slug, or revoke the link.

  
    Share link ID.
  
  
    New expiration timestamp in milliseconds. Pass <code>0</code> to remove expiration.
  
  
    New max claims. Pass <code>-1</code> to remove the limit.
  
  
    Attach a custom domain. Pass <code>null</code> to detach.
  
  
    Custom URL slug (3–64 characters, lowercase alphanumeric and hyphens). Required together with <code>customDomainId</code>; pass both{' '}
    <code>null</code> to detach. <code>login</code>, <code>auth-callback</code>, <code>preview</code>, <code>payment</code>,{' '}
    <code>api</code>, <code>admin</code> and <code>health</code> are reserved.
  
  
    Set to <code>true</code> to permanently revoke the link. Cannot be combined with other fields, and cannot be undone — this is the only
    delete path for a share link.
  

{/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */}

<h2 id="fields" class="border-t border-border pt-8">
  Fields
</h2>

{/* ── fields.list ─────────────────────────────────────────────────────────── */}

List every field of a form's current published version, with the key to address each one by. Call this before `requests.create` instead of
hard-coding keys. See [Field keys](/requests/field-keys).

  
    Form ID. A form that has never been published has no field keys yet and answers <code>published: false</code> with no items.
  

  
```
curl -X POST https://api.formbase.so/api/v1 \
  -H "Authorization: Bearer fb_..." \
  -H "Content-Type: application/json" \
  -d '{
    "method": "fields.list",
    "params": { "formId": "j57..." }
  }'
```

  
    
```
{
  "ok": true,
  "data": {
    "published": true,
    "items": [
      { "key": "company_name", "type": "text", "title": "Company", "required": true, "prefillable": true },
      {
        "key": "company_size", "type": "select", "title": "Company size", "required": false, "prefillable": true,
        "options": [
          { "key": "1_50", "label": "1–50" },
          { "key": "51_200", "label": "51–200" }
        ]
      },
      {
        "key": "satisfaction", "type": "matrix", "title": "How did we do?", "required": false, "prefillable": true,
        "rows": [{ "key": "delivery_speed", "label": "Delivery speed" }],
        "columns": [{ "key": "very_good", "label": "Very good" }, { "key": "poor", "label": "Poor" }]
      },
      { "key": "case_id", "type": "hidden", "title": "Case", "required": false, "prefillable": false, "context": true },
      { "key": "total", "type": "number", "title": "Total", "required": false, "prefillable": false, "calculated": true }
    ],
    "hasMore": false
  }
}
```

  

> ℹ️ **Reading the flags**
> <p>
>     <code>context: true</code> is a hidden field — its value belongs in <code>context</code>, never in <code>prefill</code>.{' '}
>     <code>prefillable: false</code> marks a field nobody can supply a value for (file, signature, payment, appointment, documents). For a
>     choice question, send the option <strong>key</strong>, not its label; a matrix lists its <code>rows</code> and <code>columns</code> the
>     same way and takes <code>{'{ "row_key": "column_key" }'}</code>. <code>calculated: true</code> is a calculated field: the form works out
>     its value, you read it back in <code>answers</code>, and nothing can send it.
>   </p>

<p>
  A repeating group is <code>type: "group"</code> with <code>repeating: true</code> and a <code>members</code> array. A Documents block is{' '}
  <code>type: "documents"</code> and carries <code>documents: [{`{ name }`}]</code>, the authored files every respondent already sees.
</p>

{/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */}

<h2 id="requests" class="border-t border-border pt-8">
  Requests
</h2>

A request assigns one published form to one person and calls you back when it ends. The conceptual guide lives in
[Creating a request](/requests/creating-requests); this is the parameter list.

{/* ── requests.create ─────────────────────────────────────────────────────── */}

Create a request. Spends one unit of the workspace's monthly allowance, whether or not the recipient answers.

  
    The published form to assign.
  
  
    <code>{`{ email?, name? }`}</code>. An email is required when <code>delivery</code> is <code>"email"</code>; otherwise it only
    identifies the person in the Requests page and on their answers.
  
  
    Initial answers by field key. The recipient sees them and can change them.
  
  
    Prefilled keys the recipient cannot change. Every key here must also appear in <code>prefill</code>, and a locked required field must be
    prefilled with a non-empty value.
  
  
    Values for the form's hidden fields, by field key. Trusted, unchangeable, and echoed back in the callback. An unknown key is rejected
    with <code>UNKNOWN_FIELD_KEY</code>.
  
  
    Your own bookkeeping. Never reaches the form; comes back in callbacks and reads.
  
  
    One of the form's published languages. Defaults to the form's own default.
  
  
    <code>"email"</code> to have formbase send the invitation (needs a recipient email, and Pro or Business or one of a Free account's 10
    free invitations), or <code>"none"</code> to deliver the link yourself.
  
  
    Override the form's reminder schedule for this request. An empty array switches reminders off.
  
  
    Epoch milliseconds. Defaults to 30 days out; 365 days is the maximum.
  
  
    Where formbase POSTs the callback when the request ends. HTTPS only, and the host must resolve to a public address.
  
  
    Your own id for this request. Filterable in <code>requests.list</code>.
  
  
    Repeating it with the same body returns the original request with <code>deduplicated: true</code>. A different body is rejected. Keys
    live 30 days.
  
  
    Mint the link on one of your custom domains. REST API only.
  
  
    <code>{`[{ documentId, field?, name? }]`}</code> — files handed to this one recipient, uploaded first with{' '}
    <a href="#documents-create">documents.create</a>.
  
  
    A dry run: nothing is emailed, the callback carries <code>"test": true</code>, and the submission counts nowhere. The link closes within
    24 hours, and on Free a workspace may create 10 test requests a day.
  

  
```
curl -X POST https://api.formbase.so/api/v1 \
  -H "Authorization: Bearer fb_..." \
  -H "Content-Type: application/json" \
  -d '{
    "method": "requests.create",
    "params": {
      "formId": "j57...",
      "recipient": { "email": "ada@acme.com", "name": "Ada" },
      "prefill": { "company_name": "Acme" },
      "readonly": ["company_name"],
      "context": { "case_id": "CASE-9" },
      "delivery": "email",
      "externalId": "run-42",
      "callbackUrl": "https://automation.example/webhook/resume-abc",
      "idempotencyKey": "run-42"
    }
  }'
```

  
    
```
{
  "ok": true,
  "data": {
    "id": "kd7...",
    "status": "pending",
    "url": "https://form.formbase.so/r/rq_...",
    "deliveryStatus": "queued",
    "expiresAt": 1794787200000,
    "createdAt": 1789379200000,
    "externalId": "run-42",
    "deduplicated": false
  }
}
```

  

> ⚠️ **Keep the url**
> <p>
>     <code>url</code> carries the one-time token. <code>requests.get</code> can usually rebuild it, but it comes back <code>null</code> for a
>     request created before the deployment had a request-token key. If you deliver the link yourself, store it when you create it.
>   </p>

<p>
  <code>deliveryStatus</code> is <code>not_requested</code> until an invitation is queued, then <code>queued</code> → <code>sent</code> or{' '}
  <code>failed</code>, and <code>bounced</code> once the mail provider reports a hard bounce or a complaint.
</p>

{/* ── requests.get ────────────────────────────────────────────────────────── */}

Get one request in full: status, outcome, what was prefilled, its timeline, and — once completed — <code>answers</code> and <code>display</code> keyed by field key, the same two maps the callback carries.

  
    Request ID.
  

  
    
```
{
  "ok": true,
  "data": {
    "id": "kd7...",
    "formId": "j57...",
    "status": "completed",
    "outcome": "approve",
    "isTest": false,
    "recipient": { "email": "ada@acme.com", "name": "Ada" },
    "language": "en",
    "externalId": "run-42",
    "metadata": null,
    "context": { "case_id": "CASE-9" },
    "prefill": { "company_name": "Acme" },
    "readonlyKeys": ["company_name"],
    "delivery": "email",
    "deliveryStatus": "sent",
    "hasCallback": true,
    "callbackFailedAt": null,
    "submissionId": "kp2...",
    "url": "https://form.formbase.so/r/rq_...",
    "answers": { "company_name": "Acme", "decision": "approve" },
    "display": { "company_name": "Acme", "decision": "Approve" },
    "timeline": [
      { "id": "kd7...:created", "type": "created", "at": 1789379200000 },
      { "id": "kd7...:completed", "type": "completed", "at": 1789465600000 }
    ],
    "expiresAt": 1794787200000,
    "completedAt": 1789465600000
  }
}
```

  

> ℹ️ **outcome vs status**
> <p>
>     <code>status</code> says whether the request finished; <code>outcome</code> says what the recipient decided — <code>approve</code>,{' '}
>     <code>decline</code>, <code>changes</code>, or <code>null</code> on anything but a completed request whose recipient picked one of the
>     three — including a form with no <a href="/requests/decisions-and-approvals">decision question</a>. The callback URL itself is never
>     returned; <code>hasCallback</code> only says whether one is set.
>   </p>

<p>
  The example above is trimmed. A full response also carries <code>workspaceId</code>, <code>formSnapshotId</code>, <code>createdVia</code>,{' '}
  <code>documents</code>, <code>reminderStep</code>, <code>remindersSent</code>, <code>reminderDueAt</code>, <code>dataPurgedAt</code>, and
  the rest of the timestamps (<code>updatedAt</code>, <code>openedAt</code>, <code>startedAt</code>, <code>lastActivityAt</code>,{' '}
  <code>expiredAt</code>, <code>canceledAt</code>, <code>canceledBy</code>, <code>cancelReason</code>).
</p>
<p>
  Two fields tell you when the copy in front of you is the only copy. <code>callbackFailedAt</code> is set while this request&apos;s
  callback has run out of attempts, and cleared once one gets through or you replay it. <code>dataPurgedAt</code> is set once retention
  stripped the request: <code>context</code>, <code>prefill</code> and <code>metadata</code> come back empty, <code>readonlyKeys</code> and{' '}
  <code>documents</code> are <code>[]</code>, and <code>submissionId</code>, <code>answers</code> and <code>display</code> are{' '}
  <code>null</code>.
</p>
<p>
  <code>timeline</code> is derived, oldest first. Each entry has an <code>id</code>, an <code>at</code>, and a <code>type</code> —{' '}
  <code>created</code>, <code>invitation</code>, <code>reminder</code>, <code>opened</code>, <code>started</code>, <code>completed</code>,{' '}
  <code>expired</code>, <code>canceled</code>, <code>callback</code>. Delivery entries add <code>deliveryStatus</code> and{' '}
  <code>attemptCount</code>, and callbacks add <code>eventType</code>. Delivery rows are kept 30 days, so older timelines thin back to the
  timestamps.
</p>

{/* ── requests.list ───────────────────────────────────────────────────────── */}

List requests in a workspace or on one form, newest first. Test requests are left out unless you ask for them.

  
    Scope to a workspace. Give this or <code>formId</code>.
  
  
    Scope to one form.
  
  
    <code>pending</code>, <code>completed</code>, <code>expired</code>, or <code>canceled</code>.
  
  
    <code>approve</code>, <code>decline</code>, or <code>changes</code>. Implies completed requests only.
  
  
    Your own id, to find the request a run created.
  
  
    Include requests created with <code>test: true</code>.
  
  
    Page size (1–100).
  
  
    Pagination cursor from a previous response.
  

  
    
```
{
  "ok": true,
  "data": {
    "items": [
      {
        "id": "kd7...",
        "formId": "j57...",
        "status": "pending",
        "outcome": null,
        "recipient": { "email": "ada@acme.com", "name": "Ada" },
        "externalId": "run-42",
        "deliveryStatus": "sent",
        "expiresAt": 1794787200000,
        "createdAt": 1789379200000
      }
    ],
    "nextCursor": null,
    "hasMore": false
  }
}
```

  

<p>
  List items carry the same fields as <code>requests.get</code> minus <code>url</code>, <code>answers</code>, <code>display</code>, and{' '}
  <code>timeline</code>, and each one carries <code>isTest</code>. Give <code>workspaceId</code> or <code>formId</code> — neither is{' '}
  <code>400 VALIDATION_ERROR</code> with reason <code>SCOPE_REQUIRED</code>. <code>outcome</code> overrides <code>status</code>, since only
  a completed request has a verdict.
</p>

{/* ── requests.cancel ─────────────────────────────────────────────────────── */}

Withdraw a pending request. The link stops working, the recipient sees a withdrawn notice, and a `request.canceled` callback fires.

  
    Request ID.
  
  
    Your note for why, kept on the request and sent in the callback.
  

{/* ── requests.remind ─────────────────────────────────────────────────────── */}

Email the recipient now, without touching the reminder schedule. Needs a recipient email and a Pro or Business plan.

  
    Request ID. Must still be pending, and not a test request.
  

<p>
  Two floors apply: at least 10 minutes between manual reminders, and at most 8 reminders per request in total, manual and scheduled
  together. The automatic schedule is untouched — <code>reminderStep</code> and <code>reminderDueAt</code> stay where they were.
</p>

{/* ── requests.replayCallback ─────────────────────────────────────────────── */}

Re-send the callback a request fired when it ended — same payload, same event id, so a receiver that already handled it can deduplicate.
Use it after fixing a broken endpoint.

  
    Request ID. Must be completed, expired, or canceled.
  

  
    
```
{
  "ok": true,
  "data": { "dispatchId": "kf9...", "eventId": "evt_kf9..." }
}
```

  

{/* ── requests.sample ─────────────────────────────────────────────────────── */}

Build a sample request event for a form, without any real request. It is the exact envelope a <code>request\_\*</code> subscription created
with <a href="#webhooks-create">webhooks.create</a> receives, so connectors use it for field discovery. A completed sample carries the
same example answers <a href="#submissions-sample">submissions.sample</a> shows; an expired or canceled sample carries the request block
alone.

  
    Form ID.
  
  
    Which ending to sample, in the <code>webhooks.create</code> spelling. The envelope's <code>type</code> is the dotted form.
  

  
    
```
{
  "ok": true,
  "data": {
    "id": "evt_example000000000000",
    "type": "request.completed",
    "createdAt": "2026-05-18T19:00:00.000Z",
    "apiVersion": "2026-09-24",
    "test": true,
    "data": {
      "request": {
        "id": "req_example000000000000",
        "externalId": "order-1234",
        "status": "completed",
        "language": "en",
        "recipient": { "email": "recipient@example.com", "name": "Sample Recipient" },
        "metadata": { "source": "sample" },
        "context": {},
        "createdAt": "2026-05-18T18:00:00.000Z",
        "completedAt": "2026-05-18T19:00:00.000Z"
      },
      "form": { "id": "frm_abc123", "name": "Event Feedback", "snapshotId": "js7abc123" },
      "submission": {
        "id": "sub_example000000000000",
        "respondentEmail": "respondent@example.com",
        "submittedAt": "2026-05-18T19:00:00.000Z",
        "updatedAt": null,
        "editCount": 0,
        "pdfUrl": null,
        "language": "en"
      },
      "answers": { "your_email": "john@example.com" },
      "display": { "your_email": "john@example.com" }
    }
  }
}
```

  

<p>
  The request block and the outcome are documented on the <a href="/requests/callbacks#payload">callbacks page</a>; the submission half on
  the <a href="/developers/webhooks-reference#payload">webhooks reference</a>. Sample ids are the fixed placeholders shown above and{' '}
  <code>test</code> is <code>true</code>, so a receiver can tell a sample from a live event.
</p>

{/* ── documents.create ────────────────────────────────────────────────────── */}

Reserve an upload for a file you will hand to one recipient through the form's [Documents block](/building-forms/documents-block). Bytes
never travel through this API: you get a presigned `PUT`, you upload, and `requests.create` verifies the object before the request exists.

  
    The form whose Documents block will show the file. Scopes the upload to that workspace.
  
  
    Display name the recipient sees (1–200 characters). Overridable per request.
  
  
    <code>application/pdf</code> or an image type: <code>image/png</code>, <code>image/jpeg</code>, <code>image/webp</code>,{' '}
    <code>image/gif</code>, <code>image/svg+xml</code>, <code>image/avif</code>, <code>image/bmp</code>, <code>image/tiff</code>. Office
    documents are not accepted.
  
  
    Exact byte length. Maximum 25 MB (26,214,400).
  
  
    Hex digest of the bytes. Verified after upload when given.
  

  
    
```
{
  "ok": true,
  "data": {
    "id": "kn7...",
    "name": "Lease contract draft",
    "contentType": "application/pdf",
    "size": 412000,
    "uploadUrl": "https://...",
    "expiresAt": 1792198800000
  }
}
```

  

<p>
  <code>PUT</code> the raw bytes to <code>uploadUrl</code> within the hour, with <code>Content-Type</code> set to the type you declared,
  then reference the id from <code>requests.create</code>:
</p>

```
{
  "method": "requests.create",
  "params": {
    "formId": "j57...",
    "documents": [
      { "documentId": "kn7...", "name": "Your lease contract" },
      { "documentId": "kn8...", "field": "attachments" }
    ]
  }
}
```

<ul>
  <li>
    <code>field</code> is the Documents block's field key. Optional when the form has exactly one block; required with two or more.
  </li>
  <li>The block's authored documents stay; yours appear below them, for this one recipient.</li>
  <li>Caps: 25 MB per document, 100 MB of documents per request, 20 documents shown per block including the authored ones.</li>
  <li>
    One upload can be referenced by any number of requests. An upload nobody references ages out. Bytes count against the workspace owner's
    storage until the last request referencing them is stripped by retention.
  </li>
</ul>

<p>
  Every failure here is <code>400 VALIDATION_ERROR</code> with a <code>details.reason</code>: <code>DOCUMENT_TYPE_NOT_ALLOWED</code>,{' '}
  <code>DOCUMENT_TOO_LARGE</code>, <code>INVALID_DOCUMENT_NAME</code> or <code>INVALID_DOCUMENT_SHA256</code> from this method, and{' '}
  <code>DOCUMENT_NOT_FOUND</code>, <code>DOCUMENT_NOT_UPLOADED</code> (you skipped the <code>PUT</code>), <code>DOCUMENT_INVALID</code>,{' '}
  <code>INVALID_DOCUMENT_TARGET</code>, <code>DOCUMENTS_TOO_LARGE</code> or <code>DOCUMENTS_TOO_MANY</code> from{' '}
  <code>requests.create</code>.
</p>

{/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */}

<h2 id="webhooks" class="border-t border-border pt-8">
  Webhooks
</h2>

{/* ── webhooks.list ───────────────────────────────────────────────────────── */}

List webhook subscriptions for a form.

  
    Form ID.
  

  
    
```
{
  "ok": true,
  "data": {
    "items": [
      {
        "subscriptionId": "int_abc123",
        "formId": "frm_abc123",
        "provider": "zapier",
        "targetUrl": "https://hooks.zapier.com/...",
        "eventType": "submission_created",
        "status": "active",
        "createdAt": 1714041851000
      }
    ],
    "hasMore": false
  }
}
```

  

{/* ── webhooks.create ─────────────────────────────────────────────────────── */}

Subscribe a URL to form events: new or abandoned submissions, or requests on the form ending. The URL must use HTTPS.

  
    Form ID.
  
  
    HTTPS URL to receive webhook payloads.
  
  
    Which tool the subscription belongs to. It is a label for your own bookkeeping — there is no marketplace app to install, and every
    provider behaves the same.
  
  
    Event type to subscribe to. The three <code>submission_*</code> types deliver the submission payload: <code>submission_created</code> a
    first submission, <code>submission_updated</code> a respondent's edit, and <code>submission_abandoned</code> an idle draft. The three{' '}
    <code>request_*</code> types deliver the matching <a href="/requests/callbacks#payload">request event</a> whenever a request on the form
    ends that way, signed with this subscription's secret; test requests reach no subscription.
  
  
    Required when <code>eventType</code> is <code>submission_abandoned</code>; rejected for every other type.
  
  
    Optional HMAC signing secret, 32–255 characters. When supplied, deliveries include <code>X-formbase-Signature</code>. Secret is stored
    but never returned by API.
  

  
```
curl -X POST https://api.formbase.so/api/v1 \
  -H "Authorization: Bearer fb_..." \
  -H "Content-Type: application/json" \
  -d '{
    "method": "webhooks.create",
    "params": {
      "formId": "frm_abc123",
      "targetUrl": "https://hooks.zapier.com/hooks/catch/...",
      "provider": "zapier",
      "eventType": "submission_created",
      "signingSecret": "whsec_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
    }
  }'
```

  
    
```
{
  "ok": true,
  "data": {
    "subscriptionId": "int_new123",
    "formId": "frm_abc123",
    "provider": "zapier",
    "targetUrl": "https://hooks.zapier.com/hooks/catch/...",
    "eventType": "submission_created"
  }
}
```

  

<p>
  Abandoned-submission subscriptions return the selected <code>idleWindow</code> from both <code>webhooks.create</code> and{' '}
  <code>webhooks.list</code>. Every other subscription omits it.
</p>

<p>
  A request subscription hears the same events a <a href="/requests/callbacks">callback</a> does, but as its own delivery: its own event id,
  its own signature, and its own five-attempt retry budget, after which the subscription pauses. A request created with a{' '}
  <code>callbackUrl</code> on a form with a <code>request_completed</code> subscription therefore fires twice, once to each receiver.{' '}
  <code>requests.replayCallback</code> re-sends the callback only. Use <a href="#requests-sample">requests.sample</a> to see the payload
  before any request has ended.
</p>

{/* ── webhooks.delete ─────────────────────────────────────────────────────── */}

Remove a webhook subscription.

  
    Subscription ID from <code>webhooks.list</code> or <code>webhooks.create</code>.
  

  
    
```
{
  "ok": true,
  "data": {
    "subscriptionId": "int_abc123",
    "deleted": true
  }
}
```

  

{/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */}

<h2 id="analytics" class="border-t border-border pt-8">
  Analytics
</h2>

{/* ── analytics.get ───────────────────────────────────────────────────────── */}

Get aggregated analytics metrics for a form. Supports date range, device, traffic source, and country filters.

Analytics are a Pro feature, and the rule follows the workspace **owner's** plan, the same way the dashboard's Analytics tab does. If the owner is not on Pro, this returns `UPGRADE_REQUIRED` — including for history recorded while they were. A free member of a Pro owner's workspace gets the data.

  
    Form ID.
  
  
    Start of date range as Unix timestamp in milliseconds. Must be less than or equal to <code>to</code> when both are set.
  
  
    End of date range as Unix timestamp in milliseconds. Omit both for all time — <code>period</code> then comes back as{' '}
    <code>{`{ "from": null, "to": null }`}</code>.
  
  
    Filter by device type.
  
  
    Filter by traffic source (e.g. <code>"Direct"</code>, <code>"Google"</code>).
  
  
    Filter by 2-letter country code (e.g. <code>"US"</code>, <code>"DE"</code>).
  
  
    Also return the sanitized analytics events behind the metrics, for your own analysis. No visitor ids.
  

<p>
  Rates are numbers from 0 to 100, counts are integers, and <code>totalEvents</code> is the raw event row count before deduplication into
  unique visitors.
</p>

  
    
```
{
  "ok": true,
  "data": {
    "formId": "frm_abc123",
    "period": { "from": null, "to": null },
    "totalEvents": 17,
    "metrics": {
      "views": 7,
      "uniqueVisitors": 7,
      "engaged": 6,
      "submissions": 4,
      "engagementRate": 86,
      "completionRate": 57,
      "bounceRate": 14
    },
    "breakdown": {
      "byBrowser": { "Chrome": 17 },
      "byCountry": { "US": 10, "DE": 7 },
      "byDevice": { "desktop": 14, "mobile": 3 },
      "bySource": { "Direct": 12, "Google": 5 }
    }
  }
}
```

  

{/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */}

<h2 id="workspaces" class="border-t border-border pt-8">
  Workspaces
</h2>

{/* ── workspaces.list ─────────────────────────────────────────────────────── */}

List the workspaces your token can reach. No parameters.

An API token is bound to one workspace, so this returns exactly that one — even when your account belongs to several.

  
    
```
{
  "ok": true,
  "data": {
    "items": [
      {
        "id": "ws_abc123",
        "name": "Acme Inc",
        "createdAt": 1714041800000,
        "role": "owner"
      }
    ],
    "nextCursor": null,
    "hasMore": false,
    "canPaginate": false
  }
}
```

  

{/* ── workspaces.createInvite ─────────────────────────────────────────────── */}

Create an invite link for a workspace.

  
    Workspace ID.
  
  
    Expiration as a future Unix timestamp in milliseconds.
  
  
    Maximum number of times the invite can be used.
  

  
    
```
{
  "ok": true,
  "data": {
    "id": "inv_abc123",
    "workspaceId": "ws_abc123",
    "code": "aBcDeFgH",
    "expiresAt": null,
    "maxUses": null,
    "uses": 0,
    "createdAt": 1714041851000
  }
}
```

  

{/* ── workspaces.getInvite ────────────────────────────────────────────────── */}

Get one workspace invite. Returns the same shape as `workspaces.createInvite`.

  
    Invite ID.
  

{/* ── workspaces.updateInvite ─────────────────────────────────────────────── */}

Update an existing workspace invite. Provide at least one of <code>expiresAt</code> or <code>maxUses</code>, or the call is rejected.
Returns the updated invite.

  
    Invite ID.
  
  
    New expiration timestamp in milliseconds.
  
  
    New max uses limit.
  

{/* ── workspaces.revokeInvite ─────────────────────────────────────────────── */}

Permanently revoke a workspace invite.

  
    Invite ID.
  

  
    
```
{
  "ok": true,
  "data": {
    "inviteId": "inv_abc123",
    "revoked": true
  }
}
```

  

{/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */}

<h2 id="folders" class="border-t border-border pt-8">
  Folders
</h2>

{/* ── folders.list ────────────────────────────────────────────────────────── */}

List folders in a workspace.

  
    Workspace ID.
  
  
    Page size (1–100).
  
  
    Pagination cursor.
  

  
    
```
{
  "ok": true,
  "data": {
    "items": [
      {
        "id": "fld_abc123",
        "name": "Customer Feedback",
        "workspaceId": "ws_abc123",
        "parentId": null,
        "createdAt": 1714041800000
      }
    ],
    "nextCursor": null,
    "hasMore": false,
    "canPaginate": false
  }
}
```

  

{/* ── folders.create ──────────────────────────────────────────────────────── */}

Create a folder in a workspace. Idempotent — returns the existing folder if a folder with the same name already exists.

  
    Workspace ID.
  
  
    Folder name (1–255 characters).
  
  
    Parent folder ID for nesting. Omit for root level.
  

  
    
```
{
  "ok": true,
  "data": {
    "id": "fld_new123",
    "name": "Customer Feedback",
    "workspaceId": "ws_abc123",
    "parentId": null,
    "createdAt": 1714041851000,
    "alreadyExisted": false
  }
}
```

  

{/* ── folders.update ──────────────────────────────────────────────────────── */}

Rename a folder or move it to a different parent.

  
    Folder ID.
  
  
    New folder name (1–255 characters).
  
  
    New parent folder. Pass <code>null</code> to move to root.
  

{/* ── folders.delete ──────────────────────────────────────────────────────── */}

Permanently delete a folder and all its contents (subfolders and forms).

  
    Folder ID.
  

> ⚠️ **Destructive operation**
> <p>This permanently deletes all subfolders and forms inside the folder. This action cannot be undone.</p>

  
    
```
{
  "ok": true,
  "data": {
    "folderId": "fld_abc123",
    "deletedFolderIds": ["fld_abc123"],
    "deletedFormIds": ["frm_in_folder"],
    "message": "Folder and 1 form deleted."
  }
}
```

  

<h2 id="translations" class="border-t border-border pt-8">
  Translations
</h2>

{/* ── translations.listLanguages ──────────────────────────────────────────── */}

List all languages configured on a form.

  
    Form ID.
  

  
    
```
{
  "ok": true,
  "data": {
    "formId": "frm_abc123",
    "items": [
      {
        "language": "es",
        "completion": { "total": 15, "current": 12, "outdated": 2, "missing": 1, "suggested": 0 },
        "lastUpdatedAt": 1714041851000
      }
    ],
    "nextCursor": null,
    "hasMore": false,
    "canPaginate": false
  }
}
```

  

{/* ── translations.addLanguage ────────────────────────────────────────────── */}

Register a language on a form. Every other translation method fails with `404 NOT_FOUND` until you do.

  
    Form ID.
  
  
    BCP-47 language tag (e.g. <code>"es"</code>, <code>"pt-BR"</code>).
  

  
    
```
{
  "ok": true,
  "data": {
    "formId": "frm_abc123",
    "language": "es",
    "rowId": "tl_new123"
  }
}
```

  

{/* ── translations.removeLanguage ─────────────────────────────────────────── */}

Remove a language and all its translations from a form.

  
    Form ID.
  
  
    BCP-47 language tag.
  

{/* ── translations.listEntries ────────────────────────────────────────────── */}

List every source key for one language on a form, with its current state. This is how you discover the `key` values `translations.setEntry`
takes.

  
    Form ID.
  
  
    BCP-47 language tag. Must already be on the form.
  

  
    
```
{
  "ok": true,
  "data": {
    "formId": "frm_abc123",
    "language": "es",
    "completion": { "total": 15, "current": 12, "outdated": 2, "missing": 1, "suggested": 0 },
    "items": [
      {
        "key": "block_q_1.title",
        "status": "current",
        "value": "[{\"text\":\"Tu correo electrónico\"}]",
        "sourceFragment": "[{\"text\":\"Your email\"}]",
        "updatedAt": 1714041851000,
        "block": { "id": "q_1", "type": "email-input", "label": "Your email" }
      }
    ],
    "hasMore": false
  }
}
```

  

<p>
  <code>status</code> is <code>missing</code> (nothing stored), <code>outdated</code> (the source changed since), <code>current</code>, or{' '}
  <code>suggested</code> (an AI suggestion staged but not accepted). Keys cover form content (<code>block_&lt;id&gt;.*</code>) and, once an
  author has customized them, the respondent confirmation and reminder emails (<code>email.confirmation.*</code>,{' '}
  <code>email.reminder.*</code>).
</p>

{/* ── translations.setEntry ───────────────────────────────────────────────── */}

Set a single translation entry. The language must have been added via <code>translations.addLanguage</code> first.

  
    Form ID.
  
  
    BCP-47 language tag.
  
  
    A key from <code>translations.listEntries</code>. Do not construct one by hand.
  
  
    The translated fragment, JSON-stringified. Its mark structure must match the source fragment.
  

  
```
curl -X POST https://api.formbase.so/api/v1 \
  -H "Authorization: Bearer fb_..." \
  -H "Content-Type: application/json" \
  -d '{
    "method": "translations.setEntry",
    "params": {
      "formId": "frm_abc123",
      "language": "es",
      "key": "block_q_1.title",
      "value": "[{\"text\":\"Tu correo electrónico\"}]"
    }
  }'
```

> ⚠️ **Writes here are live**
> <p>
>     The API has no draft-then-publish step: a <code>setEntry</code> or <code>deleteEntry</code> reaches respondents immediately. The
>     dashboard and the MCP translation tools use a draft instead.
>   </p>

<p>
  Returns <code>{`{ formId, language, key }`}</code>.
</p>

{/* ── translations.deleteEntry ────────────────────────────────────────────── */}

Delete a single translation entry, reverting that key to the form's default language. Idempotent. When the last entry for a language goes,
the language drops off the form's published languages.

  
    Form ID.
  
  
    BCP-47 language tag.
  
  
    Translation key to delete.
  

{/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */}

<h2 id="me" class="border-t border-border pt-8">
  Account
</h2>

{/* ── me.get ──────────────────────────────────────────────────────────────── */}

Get information about the authenticated user.

  
    
```
{
  "ok": true,
  "data": {
    "id": "usr_abc123",
    "email": "you@example.com",
    "name": "Jane Doe"
  }
}
```

  

{/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */}

<h2 id="meta" class="border-t border-border pt-8">
  Meta
</h2>

List every method name this deployment serves, sorted. The authoritative answer when this page and the server disagree.

  
    
```
{
  "ok": true,
  "data": {
    "methods": [
      "methods.list",
      "analytics.get",
      "folders.create",
      "folders.delete",
      "folders.list",
      "folders.update",
      "formSettings.get",
      "formSettings.update",
      "forms.create",
      "forms.delete",
      "forms.get",
      "forms.list",
      "..."
    ]
  }
}
```

  

{/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */}

<h2 id="error-reference" class="border-t border-border pt-8">
  Error reference
</h2>

Every error response has the same shape. The top-level `code` set is closed on purpose: a new failure mode never adds a code, it adds a
`reason`. Branch on `code` for the HTTP-level outcome and on `details.reason` for the fix.

```
{
  "ok": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "No field with key \"company\" on this form.",
    "details": { "reason": "UNKNOWN_FIELD_KEY", "field": "prefill.company", "validKeys": ["company_name", "contacts"] }
  }
}
```

<p>
  <code>details</code> is present whenever the server can name the cause. Besides <code>reason</code>, it may carry <code>field</code> (the
  offending parameter, dotted for nesting), <code>validKeys</code>, <code>validValues</code> (the option <strong>values</strong> a choice
  question accepts), <code>expectedType</code>, <code>feature</code> (on <code>UPGRADE_REQUIRED</code>), and <code>retryAfterMs</code> (on a
  throttled call). Reasons for the request surface are listed with each method above.
</p>

<p>These are all the codes:</p>

  
    Invalid or missing parameters in the request.
  
  
    Missing or invalid API token.
  
  
    Token lacks access to the requested resource.
  
  
    Resource does not exist.
  
  
    Unknown method name. Use <code>methods.list</code> to see available methods.
  
  
    The resource is not in a state that allows this call — a request that is no longer pending, an idempotency key reused with a different
    body.
  
  
    Over 120 calls a minute on this token, over 60 <code>requests.create</code> calls a minute, or too many failed authentications from this
    IP.
  
  
    Feature requires a higher subscription tier, the workspace has spent its monthly allowance (reason{' '}
    <code>MONTHLY_ALLOWANCE_REACHED</code>), or a Free account has spent its 10 free invitations (reason <code>FREE_INVITATIONS_USED</code>
    ).
  
  
    Unexpected server error. Try again later.
  

<h2 id="next-steps">Next steps</h2>
<div class="not-prose grid gap-3 sm:grid-cols-2">
  - [API tokens](/developers/api-tokens) — Create and manage tokens
  - [MCP server](/developers/mcp-server) — Use formbase from AI agents
  - [Webhooks reference](/developers/webhooks-reference) — Payload schema and signing
</div>
