{"openapi":"3.1.0","info":{"title":"Forms API","version":"1.0.0","description":"Everything the Forms builder UI can do. Authenticate with a workspace API key (it sees only its own workspace): `Authorization: Bearer fk_…`. Errors are `{ \"error\": { \"code\", \"message\", \"issues\"? } }`. Full docs: /docs/api.\n\n## Pagination\n\nLists that can grow without limit come back one page at a time, and you follow a cursor to the next page.\n\n### Cursors\n\nA paged list answers with `items` and `nextCursor`. To get the next page, send the same request again with `cursor` set to the `nextCursor` you got. `nextCursor` is `null` on the last page.\n\nKeep the same filters and sort on every page. A cursor only works with the list it came from.\n\n### Short pages\n\nA page can hold fewer items than you asked for, or none, while `nextCursor` is not `null`. With `search`, each page checks `limit` responses and returns only the ones that match. Webhook delivery logs leave out deliveries of responses older than the plan shows. See [Plan history and retention](/docs/api/retention). Keep going until `nextCursor` is `null`.\n\n## Response states\n\nA response is saved while the person answers, so every response is finished, started or left.\n\n### The three states\n\nYou can tell the state from `status` and `leftAt`.\n\n- Finished means `status` is `complete`. The person reached an ending, which submitted the response.\n- Started means `status` is `partial` and `leftAt` is `null`. The person may still be answering.\n- Left means `status` is `partial` and `leftAt` is set. The person closed the page or did nothing for 5 minutes. If they come back and continue, `leftAt` is cleared.\n\nTo filter `GET /v1/submissions` by state, pass `state` as `finished`, `started` or `left`. Passing `status` as `partial` gets started and left responses together.\n\n### Endings and leads\n\nEvery path through a form stops at an ending, such as \"Order placed\". Each ending says whether it counts as a lead in `countsAsLead`. A response that reached an ending that doesn't count is a disqualified one. `GET /v1/submissions/endings` lists the endings a form's responses reached, including ones deleted from the form since.\n\n### Earlier unfinished responses\n\nWhen the same email or phone left an unfinished response on the same form within 30 days, `earlierSubmissionId` links to it. It's only a link, and neither response is changed.\n\n## Consent\n\nEvery response says whether the person agreed to be called and texted, when, to which wording, and for which number.\n\n### Asking for consent\n\nA phone question asks for consent when its `consent` is set. Its wording shows under the phone input. With `agreeBy: \"submit\"`, submitting that step is the agreement, whether by Continue, Submit or Enter, including the button of the field-and-button starter. With `agreeBy: \"checkbox\"`, the person ticks a required box. New phone questions ask for consent when the brand has Jornaya or TrustedForm on.\n\nA `consent` question does the same on its own step, with a box or by continuing past it.\n\n### The three statuses\n\n- `agreed` means the person agreed to every consent they got to. `consentAt` is when, and `consentText` is the wording exactly as shown.\n- `not_agreed` means the form asks for consent and the person hasn't given it. They left before submitting that step, changed the number since, or left a box unticked. The response is still kept, with its contact details.\n- `not_asked` means the form asks for none.\n\nWith several consents, `consentAt` is the last one given and `consentText` holds each wording in form order, separated by a blank line. `consents` lists each agreement with its field, time, wording, how it was given and the number it covers.\n\nFilter `GET /v1/submissions` and exports by `consent`.\n\n### The number it covers\n\nConsent on a phone question is for the number the person gave when they submitted the step. If they go back and change the number, the consent no longer counts until they submit the step again, which records it for the new number.\n\n### Consent and verification\n\nConsent is recorded when the phone step is submitted, before a code is texted. Verifying the number never changes it. A person who agreed and left at the code step is `agreed` with `phoneVerified` false. To deliver only verified, consented leads, check both `consent` and `phoneVerified`.\n\n### In webhooks, postings and exports\n\nWebhook payloads carry the same fields, for finished and abandoned responses. A webhook with `onlyConsented` sends only `agreed` responses. Posting specs can send `consent`, `consentAt`, `consentText` and `phoneVerified`, and yes/no values can be written as true/false, 1/0 or Y/N. Exports end with Consent status, Consent given at and Consent wording columns.\n\nUnfinished responses carry the Jornaya LeadiD token and the TrustedForm certificate too, as soon as the page has them.\n\n## Plan history and retention\n\nResponses are kept forever. A plan shows a number of days of them, and older ones are hidden until a plan shows them again.\n\n### How much history a plan shows\n\n`retentionDays` in `GET /v1/workspaces/{workspaceId}/entitlements` is how many days of responses the workspace's plan shows.\n\nResponses older than that are left out everywhere. That means lists, exports, single reads and webhook delivery logs, since a delivery carries a response. They're kept, not deleted, and they come back on a plan that shows them.\n\n### Knowing that older responses exist\n\n`olderHidden` is `true` on an empty first page of `GET /v1/submissions` when the form, brand or workspace has responses older than the plan shows. Use it to say why a list is empty.\n\n`GET /v1/submissions/{submissionId}` answers `404` for a hidden response.\n\n### Deleting hidden responses\n\n`DELETE /v1/submissions/{submissionId}` works on hidden responses too, so an erasure request can always be honoured.\n\n## Brand access\n\nA workspace member can be limited to some brands. They, and the API keys they make, see only those brands.\n\n### What a limited member sees\n\nOutside their brands, brands don't exist for them. Lists of brands, forms and responses hold only their brands, and anything else answers `404`, as if it were missing.\n\nBrands they create join their list.\n\nAn API key made by a limited member, or made with such a key, is limited the same way. A limited member sees and can revoke only their own API keys. Revoking any other key answers `403 brand_access_limited`.\n\n### The owner\n\nThe workspace owner always sees every brand and can't be limited. Trying answers `409 owner_has_every_brand`. Only the owner sets a member's brands, with `PATCH /v1/workspaces/{workspaceId}/members/{userId}/brand-access`.\n\n### Plans\n\nLimiting a member needs a plan with per-brand access. Without it, limiting answers `403 plan_feature`, but giving every brand back always works. On a plan without per-brand access nobody is limited. A member's list applies again once the plan has it.\n\n## Files\n\nFiles people upload with a form stay private. You read them with your API key, which gives you a short-lived download link.\n\n### Files in responses\n\nA `file_upload` answer lists its files. Each carries `fileId` and `apiUrl`, the API address of that file.\n\nIn exports, a file is a permanent link into the app, `<app>/files/<fileId>`, that asks for sign-in. That way a forwarded export gives nobody the files. The `fileId` is the end of that link.\n\n### Downloading a file\n\nCall `GET /v1/files/{fileId}`, which is the `apiUrl`, with your API key. It answers with the file's name, size and type, and a `url` that works for 5 minutes. The `url` needs no API key. Fetch it right away, or call again for a fresh one.\n\n`GET /v1/submissions/{submissionId}/file-urls` gives links for all of one response's files at once. Each works for 1 hour.\n\nWebhook deliveries carry a `url` that works for 1 hour next to the `apiUrl`. That's long enough to fetch the file when the delivery arrives. After that, use the `apiUrl`.\n\nLinks stop working once the response is deleted.\n\n## Owner-only and app-only\n\nA few endpoints act on the workspace itself or on money. Some need the workspace owner, and some only work from the app, never with an API key.\n\n### Owner only\n\nSome endpoints are for the workspace owner alone: deleting the workspace, handing it to another member, limiting a member to some brands, paying for a plan, changing the plan, opening the billing portal, topping up credits, and saving or removing provider keys. Anyone else gets `403 owner_only`.\n\nThese also only work from the app, as described below.\n\n### App only\n\nAn API key belongs to a workspace, not to a person. So endpoints that act for a person only work when someone is signed in to the app, and an API key gets `400 session_only` there.\n\nThat covers every owner-only endpoint above, plus creating a workspace, leaving one, syncing your account, and keeping what you made as a guest.\n\nEach of these endpoints says so in its description.\n\n## Paid checks\n\nSome form features need the plan to include them. You need the plan to turn one on, but whatever is already on stays on.\n\n### Which features\n\n- A phone field's `verify`, an email field's `validate` and a Telegram field's `check` need the plan feature `verification`.\n- An `ai_autocomplete` question needs `aiFields`.\n- `settings.hideBadge` needs `hideBadge`.\n- `settings.partialCapture` needs `partialCapture`.\n\nThe plan's features are in `features` of `GET /v1/workspaces/{workspaceId}/entitlements`.\n\n### Features in alpha\n\n`verification` and `aiFields` are in alpha. A workspace whose stage doesn't see them yet gets `403 feature_unavailable` instead of `403 plan_feature`, and its entitlements show them as `false` whatever the plan. `planFeatures` in the entitlements shows what the plan itself includes.\n\n### Turning one on\n\nCreating or updating a draft that turns on a check the workspace can't use answers `403 plan_feature`. For a feature in alpha, it answers `403 feature_unavailable` instead.\n\nTurning a check on means switching it on for a question, by id, that didn't have it before, or adding a new `ai_autocomplete` question. Whatever is already on stays on, even if the plan no longer includes it.\n\nDuplicating a form or restoring a version copies its checks as they are, whatever the plan.\n\nOn a plan without `partialCapture`, a new form starts with `settings.partialCapture` false, whether it starts blank or from an AI build.\n\nAI builds and fixes never turn one on. Instead a phone isn't verified, an email or Telegram username isn't checked, and an AI autocomplete question becomes a short answer that asks the same thing.\n\n### On a live form\n\nSuch a check never blocks publishing. A live form skips a check its plan doesn't include and still keeps the lead. It shows the badge until the plan has `hideBadge`. With `settings.partialCapture` on, it saves only finished responses until the plan has `partialCapture`: no unfinished ones, no coming back to them, and no `submission.abandoned` events.\n\n## Webhook deliveries\n\nEach new response is POSTed to a brand's or form's webhooks as signed JSON, with retries.\n\n### Payload\n\nDeliveries are POSTed as JSON `{ id, event, createdAt, data }` with the headers `Forms-Event`, `Forms-Delivery` and `Forms-Signature`.\n\n`data` is `{ submission, form: { id, name, version } }`. It holds the response with its answers, contact and attribution, and the form and version it was answered on. The delivery log shows `data` as `payload`. A form webhook with a posting spec posts the buyer's own format instead, as described [below](#posting-specs).\n\n### Checking the signature\n\n`Forms-Signature` is `t=<unix>,v1=<hex>`. The hex is `HMAC-SHA256(secret, \"<t>.<body>\")`, keyed with the webhook's signing secret, which starts with `whsec_`.\n\n### Retries\n\nA non-2xx answer or a 10s timeout is retried after 60, 300, 1800, 7200 seconds. After 5 attempts the delivery is failed.\n\nInactive webhooks get no new deliveries. Every attempt shows in the webhook's delivery log, `GET /v1/webhooks/{webhookId}/deliveries`. Any delivery can be sent again as a new one with its own retries.\n\n### Posting in a buyer's format\n\nA posting spec makes a form webhook post each lead in a buyer's own format. The body holds exactly the spec's keys, in order. With `format: \"json\"` it is a flat JSON object of strings, and with `\"form\"` it is form-encoded. It carries the spec's extra headers, such as the buyer's API key, plus the usual `Forms-Event`, `Forms-Delivery` and `Forms-Signature`. The signature covers that body.\n\nEach key takes a question's answer, a contact or tracking value, or a constant. Every value is a string. A question that's gone or wasn't answered sends an empty string, and the webhook's `postingSpec.warnings` names the keys to check. Header values are never returned.\n\nSet one with `POST /v1/webhooks/{webhookId}/posting-spec` and try it with `POST /v1/webhooks/{webhookId}/posting-spec/preview`.\n\n## Export columns\n\nExports turn a form's responses into spreadsheet rows. Every way to export uses the same columns.\n\n### Three ways to export\n\n- `POST /v1/submissions/export` gives rows as JSON, one page at a time.\n- `POST /v1/submissions/export.csv` gives the whole CSV file in one answer, up to 15 MB.\n- `POST /v1/exports` prepares a CSV file of any size in the background.\n\nAll three take the same filters and sort as `GET /v1/submissions`. With no filters, they export every response of the form. The CSV file is UTF-8 with a byte order mark and CRLF lines.\n\n### Columns\n\nThe columns come in this order.\n\n- `id`, `createdAt`, `updatedAt`, `status`, `leftAt`, `completedAt`, `ending`, `countsAsLead`, `version` and `phoneVerified`. Times are ISO 8601 in UTC. `status` reads Started, Left or Finished, `ending` is the ending's name, and `countsAsLead` reads Yes or No.\n- One column per question, in form order, keyed `answers.<answer key>` and headed by the question's current label. Questions removed in later versions keep their last label. Answers to questions in no known version are headed by their key. They come after the other answer columns in JSON pages and in the one-call CSV, and at the very end of a background export.\n- `attribution.*` columns for the UTM parameters, landing page, referrer, ad click IDs, Jornaya LeadiD and Meta event ID.\n- `earlierSubmissionId`, `emailCheck`, `trustedFormCertUrl` and `telegramCheck`. `emailCheck` reads Valid, Invalid, Disposable, Accepts any address, Unknown or Not checked. `telegramCheck` reads Personal account, Not found on Telegram, Channel, Group, Bot or Not checked. Each is blank when the response has nothing checked or no certificate.\n- `consent`, `consentAt` and `consentText`. `consent` reads Agreed, Not agreed or Not asked. The time and wording are blank unless agreed. See [Consent](/docs/api/consent).\n- Last, a country column for each address question, keyed `answers.<answer key>.country` and headed by the question and \"(country)\". It holds an ISO 3166-1 alpha-2 code.\n\n### Values\n\nNames split into `.first` and `.last` columns. Addresses split into `.line1`, `.line2`, `.city`, `.state` and `.zip`, where `.state` is the region and `.zip` the postal code. Picked options show their labels, with several joined by \"; \". Phones are in E.164 format and dates are YYYY-MM-DD.\n\nFiles are permanent links into the app, one per line, that ask for sign-in. See [Files](/docs/api/files).\n\n### Merging pages\n\nMerge JSON pages by column key. A later page can add columns for answers to questions in older versions.\n\nTo download a file over the API, take the file id from the end of its link and call `GET /v1/files/{fileId}`.\n\n## AI builds and fixes\n\nAI can build a form from a description or fix a draft's errors. While it works, the form's draft is locked.\n\n### While AI works\n\nWhile a build started with `POST /v1/forms/generate` is planning or building, changing the draft with `PATCH /v1/forms/{formId}`, publishing and restoring a version answer `409 form_building`.\n\nWhile AI fixes the draft's errors after `POST /v1/forms/{formId}/fix`, the same calls answer `409 form_fixing`.\n\nFollow a build with `GET /v1/forms/{formId}/build` and a fix with `GET /v1/forms/{formId}/fix`, and try again once it's over.\n\n### What a fix changes\n\nA fix lands as one change to the draft, and only if it leaves fewer errors and no new ones.\n\nIt never renames or reuses ids or answer values, and never adds questions or answers. It never removes a step, question, answer or ending that a published version has.\n\nIt doesn't publish. You can undo it until the draft changes again or the form is published.\n\n### Pictures\n\nWhen seeing the options helps people answer, a build may ask with pictures, in at most 2 image choice questions. A fix gives a picture to an image choice option that has none, in the form's picture style. Each fix pass covers at most 2 questions with 6 options each.\n\nAI makes these pictures in the background, even after the build or fix is over. Follow them in `pictures` of `GET /v1/forms/{formId}`.\n\n### Paid checks\n\nAI never turns on a check the workspace can't use, whether its plan or its stage is the reason. See [Paid checks](/docs/api/plan-checks)."},"servers":[{"url":"https://careful-sparrow-950.convex.site"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Workspace API key (fk_…): sees only the workspace it was created in. Create one in the builder or via POST /v1/api-keys"}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string"},"message":{"type":"string"},"issues":{"type":"array","items":{}}}}}}},"responses":{"Error400":{"description":"Invalid input (`issues` lists each problem)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Error401":{"description":"Missing, unknown or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Error404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Error409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Error422":{"description":"Publish check failed (`issues` lists each problem)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Error429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/v1/brands":{"get":{"operationId":"brands.list","summary":"List brands","description":"List the workspace's brands, newest first. A member limited to some brands gets just those. See [Brand access](/docs/api/brand-access).","tags":["brands"],"parameters":[{"name":"workspaceId","in":"query","required":false,"schema":{"description":"Optional. Your API key already belongs to one workspace.","type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The brand's id."},"name":{"type":"string","description":"The brand's name."},"domains":{"type":"array","items":{"type":"string"},"description":"Hostnames the brand's forms may be embedded on. Subdomains match too. An empty list means any site."},"avatarUrl":{"description":"The brand's favicon, fetched in the background from its first public domain. It is fetched again when the first public domain changes. Null until there is one, so show initials instead.","type":["string","null"]},"theme":{"type":"object","properties":{"accent":{"type":"string"},"accentText":{"type":"string"},"background":{"type":"string"},"surface":{"type":"string"},"text":{"type":"string"},"muted":{"type":"string"},"border":{"type":"string"},"radius":{"type":"number","minimum":0,"maximum":48},"buttonRadius":{"type":"number","minimum":0,"maximum":48},"font":{"type":"string"},"headingFont":{"type":"string","maxLength":500},"backdrop":{"type":"string"},"fontStylesheet":{"type":"string","maxLength":2000,"format":"uri"}},"additionalProperties":false,"description":"The theme values the brand sets. The rest use the defaults."},"tracking":{"type":"object","properties":{"meta":{"type":"object","properties":{"enabled":{"type":"boolean"},"pixelId":{"type":"string"},"pageView":{"type":"boolean"},"capi":{"type":"boolean"}},"required":["enabled","pageView","capi"],"additionalProperties":false},"ga4":{"type":"object","properties":{"enabled":{"type":"boolean"},"measurementId":{"type":"string"}},"required":["enabled"],"additionalProperties":false},"jornaya":{"type":"object","properties":{"enabled":{"type":"boolean"},"campaignKey":{"type":"string"}},"required":["enabled"],"additionalProperties":false},"trustedForm":{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"],"additionalProperties":false}},"required":["meta","ga4","jornaya","trustedForm"],"additionalProperties":false,"description":"The tracking settings in effect, which are the brand's values on top of the defaults."},"hasMetaCapiToken":{"type":"boolean","description":"Whether a Meta Conversions API token is saved. The token itself is never returned."},"webhookCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many webhooks the brand has, its own and its forms', counted up to 100. `GET /v1/webhooks` lists them."},"starter":{"type":"boolean","description":"True for a guest's starter brand, which has no website or name yet. False for every other brand."},"createdAt":{"type":"number","description":"When the brand was made, in milliseconds since the epoch."},"workspaceId":{"type":"string","description":"The workspace the brand belongs to."}},"required":["id","name","domains","avatarUrl","theme","tracking","hasMetaCapiToken","webhookCount","starter","createdAt","workspaceId"],"additionalProperties":false}}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}},"post":{"operationId":"brands.create","summary":"Create a brand","description":"Create a brand with the domains its forms may be embedded on, its theme and its tracking. Every form and webhook belongs to a brand. Fails with `403 plan_limit` when the workspace has all the brands its plan includes. A guest's workspace has one brand, so there it fills that brand in instead and returns it.","tags":["brands"],"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The brand's id."},"name":{"type":"string","description":"The brand's name."},"domains":{"type":"array","items":{"type":"string"},"description":"Hostnames the brand's forms may be embedded on. Subdomains match too. An empty list means any site."},"avatarUrl":{"description":"The brand's favicon, fetched in the background from its first public domain. It is fetched again when the first public domain changes. Null until there is one, so show initials instead.","type":["string","null"]},"theme":{"type":"object","properties":{"accent":{"type":"string"},"accentText":{"type":"string"},"background":{"type":"string"},"surface":{"type":"string"},"text":{"type":"string"},"muted":{"type":"string"},"border":{"type":"string"},"radius":{"type":"number","minimum":0,"maximum":48},"buttonRadius":{"type":"number","minimum":0,"maximum":48},"font":{"type":"string"},"headingFont":{"type":"string","maxLength":500},"backdrop":{"type":"string"},"fontStylesheet":{"type":"string","maxLength":2000,"format":"uri"}},"additionalProperties":false,"description":"The theme values the brand sets. The rest use the defaults."},"tracking":{"type":"object","properties":{"meta":{"type":"object","properties":{"enabled":{"type":"boolean"},"pixelId":{"type":"string"},"pageView":{"type":"boolean"},"capi":{"type":"boolean"}},"required":["enabled","pageView","capi"],"additionalProperties":false},"ga4":{"type":"object","properties":{"enabled":{"type":"boolean"},"measurementId":{"type":"string"}},"required":["enabled"],"additionalProperties":false},"jornaya":{"type":"object","properties":{"enabled":{"type":"boolean"},"campaignKey":{"type":"string"}},"required":["enabled"],"additionalProperties":false},"trustedForm":{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"],"additionalProperties":false}},"required":["meta","ga4","jornaya","trustedForm"],"additionalProperties":false,"description":"The tracking settings in effect, which are the brand's values on top of the defaults."},"hasMetaCapiToken":{"type":"boolean","description":"Whether a Meta Conversions API token is saved. The token itself is never returned."},"webhookCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many webhooks the brand has, its own and its forms', counted up to 100. `GET /v1/webhooks` lists them."},"starter":{"type":"boolean","description":"True for a guest's starter brand, which has no website or name yet. False for every other brand."},"createdAt":{"type":"number","description":"When the brand was made, in milliseconds since the epoch."},"workspaceId":{"type":"string","description":"The workspace the brand belongs to."}},"required":["id","name","domains","avatarUrl","theme","tracking","hasMetaCapiToken","webhookCount","starter","createdAt","workspaceId"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"workspaceId":{"description":"Optional. Your API key already belongs to one workspace.","type":"string"},"name":{"type":"string","minLength":1,"maxLength":200,"description":"The brand's name, such as \"Bright Smile Dental\"."},"domains":{"maxItems":100,"type":"array","items":{"type":"string","minLength":1,"maxLength":253},"description":"Hostnames the brand's forms may be embedded on. Subdomains match; empty means any site."},"theme":{"type":"object","properties":{"accent":{"type":"string"},"accentText":{"type":"string"},"background":{"type":"string"},"surface":{"type":"string"},"text":{"type":"string"},"muted":{"type":"string"},"border":{"type":"string"},"radius":{"type":"number","minimum":0,"maximum":48},"buttonRadius":{"type":"number","minimum":0,"maximum":48},"font":{"type":"string"},"headingFont":{"type":"string","maxLength":500},"backdrop":{"type":"string"},"fontStylesheet":{"type":"string","maxLength":2000,"format":"uri"}},"description":"Only the theme values you want to set; the rest use the defaults."},"tracking":{"type":"object","properties":{"meta":{"type":"object","properties":{"enabled":{"type":"boolean"},"pixelId":{"type":"string"},"pageView":{"type":"boolean"},"capi":{"type":"boolean"}}},"ga4":{"type":"object","properties":{"enabled":{"type":"boolean"},"measurementId":{"type":"string"}}},"jornaya":{"type":"object","properties":{"enabled":{"type":"boolean"},"campaignKey":{"type":"string"}}},"trustedForm":{"type":"object","properties":{"enabled":{"type":"boolean"}}}},"description":"Only the tracking values you want to set. The rest use the defaults. Turning on Meta or GA4 needs the plan's `tracking` feature, and Jornaya needs `leadId`. TrustedForm needs `leadId` too, and is in alpha, so other workspaces get `403 feature_unavailable`. A missing plan feature answers `403 plan_feature`. Anything already on stays on."},"metaCapiToken":{"type":"string","minLength":1,"maxLength":1000,"description":"The Meta Conversions API access token. It is never returned. Reads show `hasMetaCapiToken` instead."}},"required":["name"]}}}}}},"/v1/brands/{brandId}":{"get":{"operationId":"brands.get","summary":"Get a brand","description":"Get one brand with its domains, theme and effective tracking settings.","tags":["brands"],"parameters":[{"name":"brandId","in":"path","required":true,"description":"The brand's `id`.","schema":{"type":"string","description":"The brand's `id`."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The brand's id."},"name":{"type":"string","description":"The brand's name."},"domains":{"type":"array","items":{"type":"string"},"description":"Hostnames the brand's forms may be embedded on. Subdomains match too. An empty list means any site."},"avatarUrl":{"description":"The brand's favicon, fetched in the background from its first public domain. It is fetched again when the first public domain changes. Null until there is one, so show initials instead.","type":["string","null"]},"theme":{"type":"object","properties":{"accent":{"type":"string"},"accentText":{"type":"string"},"background":{"type":"string"},"surface":{"type":"string"},"text":{"type":"string"},"muted":{"type":"string"},"border":{"type":"string"},"radius":{"type":"number","minimum":0,"maximum":48},"buttonRadius":{"type":"number","minimum":0,"maximum":48},"font":{"type":"string"},"headingFont":{"type":"string","maxLength":500},"backdrop":{"type":"string"},"fontStylesheet":{"type":"string","maxLength":2000,"format":"uri"}},"additionalProperties":false,"description":"The theme values the brand sets. The rest use the defaults."},"tracking":{"type":"object","properties":{"meta":{"type":"object","properties":{"enabled":{"type":"boolean"},"pixelId":{"type":"string"},"pageView":{"type":"boolean"},"capi":{"type":"boolean"}},"required":["enabled","pageView","capi"],"additionalProperties":false},"ga4":{"type":"object","properties":{"enabled":{"type":"boolean"},"measurementId":{"type":"string"}},"required":["enabled"],"additionalProperties":false},"jornaya":{"type":"object","properties":{"enabled":{"type":"boolean"},"campaignKey":{"type":"string"}},"required":["enabled"],"additionalProperties":false},"trustedForm":{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"],"additionalProperties":false}},"required":["meta","ga4","jornaya","trustedForm"],"additionalProperties":false,"description":"The tracking settings in effect, which are the brand's values on top of the defaults."},"hasMetaCapiToken":{"type":"boolean","description":"Whether a Meta Conversions API token is saved. The token itself is never returned."},"webhookCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many webhooks the brand has, its own and its forms', counted up to 100. `GET /v1/webhooks` lists them."},"starter":{"type":"boolean","description":"True for a guest's starter brand, which has no website or name yet. False for every other brand."},"createdAt":{"type":"number","description":"When the brand was made, in milliseconds since the epoch."},"workspaceId":{"type":"string","description":"The workspace the brand belongs to."}},"required":["id","name","domains","avatarUrl","theme","tracking","hasMetaCapiToken","webhookCount","starter","createdAt","workspaceId"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}},"patch":{"operationId":"brands.update","summary":"Update a brand","description":"Change a brand's name, domains, theme, tracking or Meta token. Each top-level field you send replaces the stored value. Leave out what you don't change.","tags":["brands"],"parameters":[{"name":"brandId","in":"path","required":true,"description":"The brand's `id`.","schema":{"type":"string","description":"The brand's `id`."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The brand's id."},"name":{"type":"string","description":"The brand's name."},"domains":{"type":"array","items":{"type":"string"},"description":"Hostnames the brand's forms may be embedded on. Subdomains match too. An empty list means any site."},"avatarUrl":{"description":"The brand's favicon, fetched in the background from its first public domain. It is fetched again when the first public domain changes. Null until there is one, so show initials instead.","type":["string","null"]},"theme":{"type":"object","properties":{"accent":{"type":"string"},"accentText":{"type":"string"},"background":{"type":"string"},"surface":{"type":"string"},"text":{"type":"string"},"muted":{"type":"string"},"border":{"type":"string"},"radius":{"type":"number","minimum":0,"maximum":48},"buttonRadius":{"type":"number","minimum":0,"maximum":48},"font":{"type":"string"},"headingFont":{"type":"string","maxLength":500},"backdrop":{"type":"string"},"fontStylesheet":{"type":"string","maxLength":2000,"format":"uri"}},"additionalProperties":false,"description":"The theme values the brand sets. The rest use the defaults."},"tracking":{"type":"object","properties":{"meta":{"type":"object","properties":{"enabled":{"type":"boolean"},"pixelId":{"type":"string"},"pageView":{"type":"boolean"},"capi":{"type":"boolean"}},"required":["enabled","pageView","capi"],"additionalProperties":false},"ga4":{"type":"object","properties":{"enabled":{"type":"boolean"},"measurementId":{"type":"string"}},"required":["enabled"],"additionalProperties":false},"jornaya":{"type":"object","properties":{"enabled":{"type":"boolean"},"campaignKey":{"type":"string"}},"required":["enabled"],"additionalProperties":false},"trustedForm":{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"],"additionalProperties":false}},"required":["meta","ga4","jornaya","trustedForm"],"additionalProperties":false,"description":"The tracking settings in effect, which are the brand's values on top of the defaults."},"hasMetaCapiToken":{"type":"boolean","description":"Whether a Meta Conversions API token is saved. The token itself is never returned."},"webhookCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many webhooks the brand has, its own and its forms', counted up to 100. `GET /v1/webhooks` lists them."},"starter":{"type":"boolean","description":"True for a guest's starter brand, which has no website or name yet. False for every other brand."},"createdAt":{"type":"number","description":"When the brand was made, in milliseconds since the epoch."},"workspaceId":{"type":"string","description":"The workspace the brand belongs to."}},"required":["id","name","domains","avatarUrl","theme","tracking","hasMetaCapiToken","webhookCount","starter","createdAt","workspaceId"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200,"description":"The brand's name, such as \"Bright Smile Dental\"."},"domains":{"description":"Replaces the list. When the first public domain changes, `avatarUrl` is refetched in the background.","maxItems":100,"type":"array","items":{"type":"string","minLength":1,"maxLength":253}},"theme":{"description":"Replaces the stored theme as a whole, so send every value you want to keep.","type":"object","properties":{"accent":{"type":"string"},"accentText":{"type":"string"},"background":{"type":"string"},"surface":{"type":"string"},"text":{"type":"string"},"muted":{"type":"string"},"border":{"type":"string"},"radius":{"type":"number","minimum":0,"maximum":48},"buttonRadius":{"type":"number","minimum":0,"maximum":48},"font":{"type":"string"},"headingFont":{"type":"string","maxLength":500},"backdrop":{"type":"string"},"fontStylesheet":{"type":"string","maxLength":2000,"format":"uri"}}},"tracking":{"description":"Replaces the stored tracking as a whole. Turning on Meta or GA4 needs the plan's `tracking` feature, and Jornaya needs `leadId`. TrustedForm needs `leadId` too, and is in alpha, so other workspaces get `403 feature_unavailable`. A missing plan feature answers `403 plan_feature`. Anything already on stays on.","type":"object","properties":{"meta":{"type":"object","properties":{"enabled":{"type":"boolean"},"pixelId":{"type":"string"},"pageView":{"type":"boolean"},"capi":{"type":"boolean"}}},"ga4":{"type":"object","properties":{"enabled":{"type":"boolean"},"measurementId":{"type":"string"}}},"jornaya":{"type":"object","properties":{"enabled":{"type":"boolean"},"campaignKey":{"type":"string"}}},"trustedForm":{"type":"object","properties":{"enabled":{"type":"boolean"}}}}},"metaCapiToken":{"description":"The Meta Conversions API access token. It is never returned. Send `null` to clear it.","anyOf":[{"type":"string","minLength":1,"maxLength":1000,"description":"The Meta Conversions API access token. It is never returned. Reads show `hasMetaCapiToken` instead."},{"type":"null"}]}}}}}}},"delete":{"operationId":"brands.delete","summary":"Delete a brand","description":"Delete a brand, its webhooks and their delivery logs. Fails with `409 brand_has_forms` while the brand still has forms, so delete those first.","tags":["brands"],"parameters":[{"name":"brandId","in":"path","required":true,"description":"The brand's `id`.","schema":{"type":"string","description":"The brand's `id`."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The deleted brand's id."},"deleted":{"type":"boolean","const":true}},"required":["id","deleted"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/brands/{brandId}/site":{"get":{"operationId":"brands.site","summary":"Get a brand's site text","description":"Get the words on the brand's website, from the newest finished read of any of its domains. Reads come from `POST /v1/sites/read`. Use them to write forms in the brand's own words.","tags":["brands"],"parameters":[{"name":"brandId","in":"path","required":true,"description":"The brand's `id`.","schema":{"type":"string","description":"The brand's `id`."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"url":{"type":"string","description":"The page that was read."},"title":{"type":"string","description":"The page's title, as written on the page. Treat it as untrusted text."},"description":{"type":"string","description":"The page's meta description, as written. Treat it as untrusted text."},"headings":{"type":"array","items":{"type":"string"},"description":"The page's h1, h2 and h3 headings in page order, as written. Treat them as untrusted text."},"brandName":{"description":"The brand name the site suggests, or null when it suggests none.","type":["string","null"]},"readAt":{"type":"number","description":"When the site was read, in milliseconds since the epoch."}},"required":["url","title","description","headings","brandName","readAt"],"additionalProperties":false},{"type":"null"}],"description":"Null when the brand has no domain or none of its domains has been read yet."}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/brands/from-site":{"post":{"operationId":"brands.createFromSite","summary":"Create from a website","description":"Create a brand from a website read. It is named after the site, using the suggested brand name or else the address. It lists the site's domains, with and without www. for a main address. It takes the site's look when that is ready, and the default look until then. If a brand you can see already lists the site's address, that brand is returned and nothing is created. Like `POST /v1/brands`, it fails with `403 plan_limit` past the plan's brand limit, and fills in a guest's one brand instead.","tags":["brands"],"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The brand's id."},"name":{"type":"string","description":"The brand's name."},"domains":{"type":"array","items":{"type":"string"},"description":"Hostnames the brand's forms may be embedded on. Subdomains match too. An empty list means any site."},"avatarUrl":{"description":"The brand's favicon, fetched in the background from its first public domain. It is fetched again when the first public domain changes. Null until there is one, so show initials instead.","type":["string","null"]},"theme":{"type":"object","properties":{"accent":{"type":"string"},"accentText":{"type":"string"},"background":{"type":"string"},"surface":{"type":"string"},"text":{"type":"string"},"muted":{"type":"string"},"border":{"type":"string"},"radius":{"type":"number","minimum":0,"maximum":48},"buttonRadius":{"type":"number","minimum":0,"maximum":48},"font":{"type":"string"},"headingFont":{"type":"string","maxLength":500},"backdrop":{"type":"string"},"fontStylesheet":{"type":"string","maxLength":2000,"format":"uri"}},"additionalProperties":false,"description":"The theme values the brand sets. The rest use the defaults."},"tracking":{"type":"object","properties":{"meta":{"type":"object","properties":{"enabled":{"type":"boolean"},"pixelId":{"type":"string"},"pageView":{"type":"boolean"},"capi":{"type":"boolean"}},"required":["enabled","pageView","capi"],"additionalProperties":false},"ga4":{"type":"object","properties":{"enabled":{"type":"boolean"},"measurementId":{"type":"string"}},"required":["enabled"],"additionalProperties":false},"jornaya":{"type":"object","properties":{"enabled":{"type":"boolean"},"campaignKey":{"type":"string"}},"required":["enabled"],"additionalProperties":false},"trustedForm":{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"],"additionalProperties":false}},"required":["meta","ga4","jornaya","trustedForm"],"additionalProperties":false,"description":"The tracking settings in effect, which are the brand's values on top of the defaults."},"hasMetaCapiToken":{"type":"boolean","description":"Whether a Meta Conversions API token is saved. The token itself is never returned."},"webhookCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many webhooks the brand has, its own and its forms', counted up to 100. `GET /v1/webhooks` lists them."},"starter":{"type":"boolean","description":"True for a guest's starter brand, which has no website or name yet. False for every other brand."},"createdAt":{"type":"number","description":"When the brand was made, in milliseconds since the epoch."},"workspaceId":{"type":"string","description":"The workspace the brand belongs to."}},"required":["id","name","domains","avatarUrl","theme","tracking","hasMetaCapiToken","webhookCount","starter","createdAt","workspaceId"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"workspaceId":{"description":"Optional. Your API key already belongs to one workspace.","type":"string"},"readId":{"type":"string","minLength":1,"maxLength":64,"description":"The read's `id` from `POST /v1/sites/read`. A read still in progress fills in the brand's look and name when they land, unless the brand has its own by then."}},"required":["readId"]}}}}}},"/v1/brands/{brandId}/import-theme":{"post":{"operationId":"brands.importTheme","summary":"Import a site's theme","description":"Read a website's styling and return it as theme values. It picks up the button color, text and background colors, corner radius, and the font with its Google Fonts, Bunny or Adobe stylesheet. It uses no AI, so the same page gives the same theme. It always returns a complete, readable theme. When the page can't be fetched, because it is down, blocked or not a web page, you get the default theme and `warnings` says why.","tags":["brands"],"parameters":[{"name":"brandId","in":"path","required":true,"description":"The brand's `id`.","schema":{"type":"string","description":"The brand's `id`."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"theme":{"type":"object","properties":{"accent":{"type":"string"},"accentText":{"type":"string"},"background":{"type":"string"},"surface":{"type":"string"},"text":{"type":"string"},"muted":{"type":"string"},"border":{"type":"string"},"radius":{"type":"number","minimum":0,"maximum":48},"buttonRadius":{"type":"number","minimum":0,"maximum":48},"font":{"type":"string"},"headingFont":{"type":"string","maxLength":500},"backdrop":{"type":"string"},"fontStylesheet":{"type":"string","maxLength":2000,"format":"uri"}},"additionalProperties":false,"description":"The imported theme values."},"sources":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"},"description":"Where each value came from, by theme key. For example, `\".btn-primary\" background`."},"warnings":{"type":"array","items":{"type":"string"},"description":"Which values fell back to a default. If the page couldn't be read, this says why."},"notes":{"type":"array","items":{"type":"string"},"description":"Things good to know that need no fixing, such as a self-hosted Google font."},"brand":{"description":"The brand with the merged theme. Only present with `apply: true`.","type":"object","properties":{"id":{"type":"string","description":"The brand's id."},"name":{"type":"string","description":"The brand's name."},"domains":{"type":"array","items":{"type":"string"},"description":"Hostnames the brand's forms may be embedded on. Subdomains match too. An empty list means any site."},"avatarUrl":{"description":"The brand's favicon, fetched in the background from its first public domain. It is fetched again when the first public domain changes. Null until there is one, so show initials instead.","type":["string","null"]},"theme":{"type":"object","properties":{"accent":{"type":"string"},"accentText":{"type":"string"},"background":{"type":"string"},"surface":{"type":"string"},"text":{"type":"string"},"muted":{"type":"string"},"border":{"type":"string"},"radius":{"type":"number","minimum":0,"maximum":48},"buttonRadius":{"type":"number","minimum":0,"maximum":48},"font":{"type":"string"},"headingFont":{"type":"string","maxLength":500},"backdrop":{"type":"string"},"fontStylesheet":{"type":"string","maxLength":2000,"format":"uri"}},"additionalProperties":false,"description":"The theme values the brand sets. The rest use the defaults."},"tracking":{"type":"object","properties":{"meta":{"type":"object","properties":{"enabled":{"type":"boolean"},"pixelId":{"type":"string"},"pageView":{"type":"boolean"},"capi":{"type":"boolean"}},"required":["enabled","pageView","capi"],"additionalProperties":false},"ga4":{"type":"object","properties":{"enabled":{"type":"boolean"},"measurementId":{"type":"string"}},"required":["enabled"],"additionalProperties":false},"jornaya":{"type":"object","properties":{"enabled":{"type":"boolean"},"campaignKey":{"type":"string"}},"required":["enabled"],"additionalProperties":false},"trustedForm":{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"],"additionalProperties":false}},"required":["meta","ga4","jornaya","trustedForm"],"additionalProperties":false,"description":"The tracking settings in effect, which are the brand's values on top of the defaults."},"hasMetaCapiToken":{"type":"boolean","description":"Whether a Meta Conversions API token is saved. The token itself is never returned."},"webhookCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many webhooks the brand has, its own and its forms', counted up to 100. `GET /v1/webhooks` lists them."},"starter":{"type":"boolean","description":"True for a guest's starter brand, which has no website or name yet. False for every other brand."},"createdAt":{"type":"number","description":"When the brand was made, in milliseconds since the epoch."},"workspaceId":{"type":"string","description":"The workspace the brand belongs to."}},"required":["id","name","domains","avatarUrl","theme","tracking","hasMetaCapiToken","webhookCount","starter","createdAt","workspaceId"],"additionalProperties":false}},"required":["theme","sources","warnings","notes"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","maxLength":2000,"format":"uri","description":"The public page to read, usually the brand's home page. Private and local addresses are refused with `400 import_failed`."},"apply":{"description":"Set to true to also merge the result into the brand's theme. The backdrop is never changed. The default, false, only previews.","type":"boolean"}},"required":["url"]}}}}}},"/v1/forms":{"get":{"operationId":"forms.list","summary":"List forms","description":"List the workspace's forms, newest first, up to 500. Drafts aren't included. A member limited to some brands gets only those brands' forms (see [Brand access](/docs/api/brand-access)).","tags":["forms"],"parameters":[{"name":"workspaceId","in":"query","required":false,"schema":{"description":"Optional. Your API key already belongs to one workspace.","type":"string"}},{"name":"brandId","in":"query","required":false,"schema":{"description":"Only list this brand's forms.","type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The form's id."},"brandId":{"type":"string","description":"The brand the form belongs to."},"workspaceId":{"type":"string","description":"The workspace the form belongs to."},"name":{"type":"string","description":"The form's name."},"status":{"type":"string","enum":["draft","published","unpublished"],"description":"`draft` until it's first published, then `published` while it's live. `unpublished` once it's taken offline; its versions are kept."},"publishedVersion":{"description":"The number of the live version. Null if the form was never published.","type":["number","null"]},"publishedAt":{"description":"When the latest version was published, in milliseconds since the epoch. Null if never.","type":["number","null"]},"hasUnpublishedChanges":{"type":"boolean","description":"Whether the draft or theme overrides changed since the form was last published."},"changedAt":{"description":"When the draft or theme overrides last changed, in milliseconds since the epoch. Null if they were never edited.","type":["number","null"]},"partialCapture":{"type":"boolean","description":"Whether the draft has \"Save unfinished responses\" on. The webhook event for people who left (`submission.abandoned`) needs it, and the plan feature `partialCapture` too."},"stepCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many steps the draft has."},"blockCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many questions and other blocks the draft has, across all steps."},"createdAt":{"type":"number","description":"When the form was created, in milliseconds since the epoch."},"updatedAt":{"type":"number","description":"When anything about the form last changed, in milliseconds since the epoch."}},"required":["id","brandId","workspaceId","name","status","publishedVersion","publishedAt","hasUnpublishedChanges","changedAt","partialCapture","stepCount","blockCount","createdAt","updatedAt"],"additionalProperties":false}}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}},"post":{"operationId":"forms.create","summary":"Create a form","description":"Create a draft form for a brand. When the workspace already has all the forms its plan includes, it answers `403 plan_limit`. Drafts count toward that limit. See [Paid checks](/docs/api/plan-checks).","tags":["forms"],"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The form's id."},"brandId":{"type":"string","description":"The brand the form belongs to."},"workspaceId":{"type":"string","description":"The workspace the form belongs to."},"name":{"type":"string","description":"The form's name."},"status":{"type":"string","enum":["draft","published","unpublished"],"description":"`draft` until it's first published, then `published` while it's live. `unpublished` once it's taken offline; its versions are kept."},"publishedVersion":{"description":"The number of the live version. Null if the form was never published.","type":["number","null"]},"publishedAt":{"description":"When the latest version was published, in milliseconds since the epoch. Null if never.","type":["number","null"]},"hasUnpublishedChanges":{"type":"boolean","description":"Whether the draft or theme overrides changed since the form was last published."},"changedAt":{"description":"When the draft or theme overrides last changed, in milliseconds since the epoch. Null if they were never edited.","type":["number","null"]},"partialCapture":{"type":"boolean","description":"Whether the draft has \"Save unfinished responses\" on. The webhook event for people who left (`submission.abandoned`) needs it, and the plan feature `partialCapture` too."},"stepCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many steps the draft has."},"blockCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many questions and other blocks the draft has, across all steps."},"createdAt":{"type":"number","description":"When the form was created, in milliseconds since the epoch."},"updatedAt":{"type":"number","description":"When anything about the form last changed, in milliseconds since the epoch."},"draft":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{},"description":"The whole form as it is now, unpublished edits included. It holds `steps` in order, then `endings`, and `settings`, in the same shape as `draft` in `PATCH /v1/forms/{formId}`."},"themeOverrides":{"type":"object","properties":{"accent":{"type":"string"},"accentText":{"type":"string"},"background":{"type":"string"},"surface":{"type":"string"},"text":{"type":"string"},"muted":{"type":"string"},"border":{"type":"string"},"radius":{"type":"number","minimum":0,"maximum":48},"buttonRadius":{"type":"number","minimum":0,"maximum":48},"font":{"type":"string"},"headingFont":{"type":"string","maxLength":500},"backdrop":{"type":"string"},"fontStylesheet":{"type":"string","maxLength":2000,"format":"uri"}},"additionalProperties":false,"description":"Theme values that override the brand theme for this form."},"trackingOverrides":{"type":"object","properties":{"meta":{"type":"boolean","description":"Whether this form fires the brand's Meta Lead, in the browser and through the Conversions API."},"ga4":{"type":"boolean","description":"Whether this form fires the brand's GA4 `generate_lead` event."},"jornayaCampaignKey":{"description":"This form's own Jornaya campaign key. Leave it out to use the brand's. It applies while the brand has Jornaya on. A page loads one campaign script, so with two forms on a page the first one's key is used.","type":"string"}},"required":["meta","ga4"],"additionalProperties":false,"description":"Whether this form fires the brand's Meta and GA4 tracking, and its own Jornaya campaign key."},"pictures":{"type":"array","items":{"type":"object","properties":{"blockId":{"type":"string","description":"The id of the image choice question."},"value":{"type":"string","description":"The `value` of the option the picture is for."},"status":{"type":"string","enum":["making","added"],"description":"`making` while AI makes the picture. `added` once it's on the option as `imageId` in the draft."},"imageId":{"description":"The picture's id once it's added. Null while it's being made.","type":["string","null"]}},"required":["blockId","value","status","imageId"],"additionalProperties":false},"description":"Pictures AI is making in the background for image choice options after an AI build or fix. If a picture can't be made, or its option was removed or got a picture in the meantime, it drops off this list and the option stays as it is. You can add a picture yourself with `POST /v1/forms/{formId}/images`. Publishing while a picture is still `making` fails like a missing picture would, with a message that it's still coming."}},"required":["id","brandId","workspaceId","name","status","publishedVersion","publishedAt","hasUnpublishedChanges","changedAt","partialCapture","stepCount","blockCount","createdAt","updatedAt","draft","themeOverrides","trackingOverrides","pictures"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"brandId":{"description":"The brand the form belongs to.","type":"string"},"name":{"type":"string","minLength":1,"maxLength":200,"description":"The form's name."},"draft":{"description":"The whole form, in the same shape as in `PATCH /v1/forms/{formId}`. Leave it out to start with one empty step and one ending called \"Thank you\", which shows a message and counts as a lead.","type":"object","properties":{"steps":{"minItems":1,"type":"array","items":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"title":{"type":"string"},"description":{"type":"string"},"blocks":{"type":"array","items":{"anyOf":[{"oneOf":[{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"single_choice"},"options":{"minItems":1,"maxItems":8,"type":"array","items":{"type":"object","properties":{"value":{"type":"string","minLength":1},"label":{"type":"string","minLength":1},"goto":{"description":"Single choice and dropdown only: where choosing this answer sends the visitor (e.g. \"No, I rent\" → the \"Not a fit\" ending). Used when no rule on the step matched. Omit to follow the step's `next`.","oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"step"},"stepId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","stepId"]},{"type":"object","properties":{"kind":{"type":"string","const":"ending"},"endingId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","endingId"]}]}},"required":["value","label"]}},"autoAdvance":{"type":"boolean"}},"required":["id","label","required","type","options","autoAdvance"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"multi_choice"},"options":{"minItems":1,"maxItems":8,"type":"array","items":{"type":"object","properties":{"value":{"type":"string","minLength":1},"label":{"type":"string","minLength":1},"goto":{"description":"Single choice and dropdown only: where choosing this answer sends the visitor (e.g. \"No, I rent\" → the \"Not a fit\" ending). Used when no rule on the step matched. Omit to follow the step's `next`.","oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"step"},"stepId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","stepId"]},{"type":"object","properties":{"kind":{"type":"string","const":"ending"},"endingId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","endingId"]}]}},"required":["value","label"]}},"minSelected":{"type":"integer","minimum":0,"maximum":9007199254740991},"maxSelected":{"type":"integer","minimum":1,"maximum":9007199254740991}},"required":["id","label","required","type","options"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"image_choice"},"options":{"minItems":1,"maxItems":12,"type":"array","items":{"type":"object","properties":{"value":{"type":"string","minLength":1,"description":"The answer value (what responses, webhooks and rules see)."},"label":{"description":"Words under the picture. Optional: without one the picture shows on its own (give it `alt` text).","type":"string"},"imageId":{"description":"An image uploaded to this form (images.uploadUrl + images.create, or images.create with a url). Served to visitors at {apiBase}/v1/public/images/{imageId}.","type":"string","pattern":"^[A-Za-z0-9_;-]{1,64}$"},"imageUrl":{"description":"Or a picture on another site, shown as is (https://…). Prefer imageId: it is checked, sized and served by us.","type":"string"},"alt":{"description":"What the picture shows, for screen readers. Needed when there's no label; screen readers otherwise read the value.","type":"string"},"goto":{"description":"Single selection only (multiple: false): where choosing this picture sends the visitor, like a single choice option's goto.","oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"step"},"stepId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","stepId"]},{"type":"object","properties":{"kind":{"type":"string","const":"ending"},"endingId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","endingId"]}]}},"required":["value"]}},"multiple":{"type":"boolean","description":"Visitors can pick several pictures (the answer is a list of values); else one (the answer is a value)."},"autoAdvance":{"type":"boolean","description":"Single selection only: picking a picture moves on, like single choice. Ignored with multiple."},"minSelected":{"description":"Multiple only: fewest pictures to pick.","type":"integer","minimum":0,"maximum":9007199254740991},"maxSelected":{"description":"Multiple only: most pictures to pick.","type":"integer","minimum":1,"maximum":9007199254740991}},"required":["id","label","required","type","options","multiple","autoAdvance"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"dropdown"},"options":{"minItems":1,"type":"array","items":{"type":"object","properties":{"value":{"type":"string","minLength":1},"label":{"type":"string","minLength":1},"goto":{"description":"Single choice and dropdown only: where choosing this answer sends the visitor (e.g. \"No, I rent\" → the \"Not a fit\" ending). Used when no rule on the step matched. Omit to follow the step's `next`.","oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"step"},"stepId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","stepId"]},{"type":"object","properties":{"kind":{"type":"string","const":"ending"},"endingId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","endingId"]}]}},"required":["value","label"]}},"placeholder":{"type":"string"}},"required":["id","label","required","type","options"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"yes_no"},"routes":{"description":"Where answering Yes / No sends the visitor, like an option's `goto`. Omit either to follow the step's `next`.","type":"object","properties":{"yes":{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"step"},"stepId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","stepId"]},{"type":"object","properties":{"kind":{"type":"string","const":"ending"},"endingId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","endingId"]}],"description":"Where the visitor goes next: { kind: \"step\", stepId } shows that step; { kind: \"ending\", endingId } finishes the form at that ending (the response is submitted)."},"no":{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"step"},"stepId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","stepId"]},{"type":"object","properties":{"kind":{"type":"string","const":"ending"},"endingId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","endingId"]}],"description":"Where the visitor goes next: { kind: \"step\", stepId } shows that step; { kind: \"ending\", endingId } finishes the form at that ending (the response is submitted)."}}}},"required":["id","label","required","type"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","enum":["short_text","long_text"]},"placeholder":{"type":"string"},"maxLength":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"required":["id","label","required","type"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"name"},"split":{"type":"boolean"}},"required":["id","label","required","type","split"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"email"},"validate":{"description":"Check the address: when the visitor continues past this field's step (or submits), the address is checked with an email validation service. One that doesn't exist or is disposable gets a warning once; continuing again with the same address goes on, and the response's emailCheck records the result (\"invalid\", \"disposable\", \"valid\", \"catchall\", \"unknown\"): a check never turns a lead away. A likely typo gets a \"Did you mean …?\". Costs credits per checked address (credits.get prices.emailValidation) and needs plan feature verification (workspaces.entitlements). Out of credits, without the feature, or if the service is down, the visitor goes on and the response's emailCheck is \"skipped\". Omit or false: not checked.","type":"boolean"}},"required":["id","label","required","type"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"telegram"},"check":{"description":"Check the account exists: when the visitor continues past this field's step (or submits), the username is looked up on Telegram. One not found on Telegram, or that belongs to a channel, group or bot, gets a warning once; continuing again with the same username goes on, and the response's telegramCheck records the result (\"found\", \"not_found\", \"channel\", \"group\", \"bot\"): a check never turns a lead away (\"not_found\" isn't proof: some taken usernames look free on Telegram). A username in the wrong shape still can't be submitted. Costs credits per checked username (credits.get prices.telegramCheck) and needs plan feature verification (workspaces.entitlements). Out of credits, without the feature, or if Telegram doesn't answer, the visitor goes on and the response's telegramCheck is \"skipped\". Omit or false: not checked. The answer is stored as the username without \"@\" (\"@name\", \"t.me/name\" and links are accepted).","type":"boolean"}},"required":["id","label","required","type"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"phone"},"consent":{"description":"Consent to be called and texted at this number (TCPA), recorded with the time, the wording and the number when the visitor agrees, before any code is texted (verify). The wording always shows under the input. A consent no longer counts once the visitor changes the number; submitting the step again records it for the new one. Omit for no consent. The response's consent, consentAt and consentText report it.","type":"object","properties":{"agreeBy":{"type":"string","enum":["submit","checkbox"],"description":"\"submit\": pressing the step's Continue or Submit (or Enter) is the agreement. \"checkbox\": a required box under the input, ticked to agree."},"text":{"type":"string","minLength":1,"maxLength":2000,"description":"The disclosure under the phone input, as inline Markdown (links like [terms](https://…) allowed). It names who may call and text, says it may be by autodialer or recorded voice and that agreeing isn't required to buy."}},"required":["agreeBy","text"]},"verify":{"type":"boolean","description":"Verify by text: when the visitor continues past this field's step, a 6-digit code is texted to the number and must be entered before they can move on. Every visitor who reaches the field is verified. At most one per form. Costs credits per approved code (credits.get prices.phoneVerification) and needs plan feature verification (workspaces.entitlements); out of credits or without the feature, no code is sent, the visitor goes on and the response has phoneVerified false."},"defaultCountry":{"description":"Country selected when the form opens (ISO 3166-1 alpha-2: two capital letters). Omit for the visitor's country, guessed from the browser's time zone and language; if that isn't allowed, the first of `countries`. Must be one of `countries` when those are set.","type":"string"},"countries":{"description":"Only these countries can be picked and only their numbers are accepted (ISO 3166-1 alpha-2 codes; the first is the fallback default). One country shows a fixed prefix with no picker. Omit or [] for every country.","maxItems":250,"type":"array","items":{"type":"string"}},"mobileOnly":{"description":"Only accept mobile numbers. Numbers whose country can't tell mobiles from landlines (the US, Canada and the rest of +1) all pass.","type":"boolean"}},"required":["id","label","required","type","verify"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"number"},"min":{"type":"number"},"max":{"type":"number"}},"required":["id","label","required","type"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"currency"},"currency":{"description":"The currency of the amount, an ISO 4217 code (three capital letters). Omit for US dollars (\"USD\"). The answer is a plain number in its major unit (1234.5 = 1,234.50); min and max are in it too.","type":"string"},"min":{"type":"number"},"max":{"type":"number"}},"required":["id","label","required","type"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"slider"},"min":{"type":"number","description":"The lowest value, at the slider's left end."},"max":{"type":"number","description":"The highest value, at the right end. More than min."},"step":{"type":"number","exclusiveMinimum":0,"description":"How much each notch adds: the values are min, min + step, … up to max (max − min must be a whole number of steps)."},"prefix":{"description":"Shown before the number, e.g. \"$\".","type":"string","maxLength":10},"suffix":{"description":"Shown after the number, e.g. \"%\" or \"km\" (words get a space: \"25 km\").","type":"string","maxLength":20},"defaultValue":{"description":"Where the slider starts, one of its values. Omit to start at min. The visitor's answer is where they leave it.","type":"number"},"orMore":{"description":"The top value means \"or more\": it shows with a + after it (read out \"or more\"). The answer is still max.","type":"boolean"}},"required":["id","label","required","type","min","max","step"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"ai_autocomplete"},"prompt":{"type":"string","maxLength":500,"description":"What to suggest as the visitor types, in plain words, e.g. \"Car makes and models sold in the US\". From their first 2 characters, an AI model suggests up to 8 answers. Costs credits once per visitor who sees suggestions (credits.get prices.aiAutocomplete) and needs plan feature aiFields (workspaces.entitlements). Out of credits, without the feature, or if the model is slow or down: no suggestions, the field works as a plain text box. Required to publish."},"allowCustom":{"description":"Omit or true: the visitor may keep their own words. false: when there are suggestions for what they typed, they must pick one (the form checks it; with no suggestions to pick from, their own words are kept, and the API accepts any text).","type":"boolean"},"placeholder":{"type":"string"}},"required":["id","label","required","type","prompt"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"date"}},"required":["id","label","required","type"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"address"},"include":{"type":"object","properties":{"line2":{"type":"boolean"}},"required":["line2"]},"defaultCountry":{"description":"Country selected when the form opens (ISO 3166-1 alpha-2: two capital letters). Omit for the visitor's country, guessed from the browser's time zone and language; if that isn't allowed, the first of `countries`. Must be one of `countries` when those are set. The visitor can pick another allowed country; the address's parts, their names and order, which ones are required and the postcode's format follow the picked country.","type":"string"},"countries":{"description":"Only addresses in these countries can be entered (ISO 3166-1 alpha-2 codes; the first is the fallback default). One country shows no country picker. Omit or [] for every country.","maxItems":250,"type":"array","items":{"type":"string"}}},"required":["id","label","required","type","include"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"consent"},"text":{"type":"string","minLength":1},"input":{"type":"string","enum":["checkbox","implied"]}},"required":["id","label","required","type","text","input"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"hidden"},"source":{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"url"},"param":{"type":"string","minLength":1}},"required":["kind","param"]},{"type":"object","properties":{"kind":{"type":"string","const":"host"}},"required":["kind"]}]}},"required":["id","label","required","type","source"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"file_upload"},"maxFiles":{"description":"Most files a visitor can upload here, 1–10 (default 10). Whatever this says, a field takes at most 10 files and 20 MB in total per response.","type":"integer","minimum":1,"maximum":10},"accept":{"description":"Kinds of files allowed: any of \"images\" (jpg, png, gif, webp, heic, avif), \"videos\" (mp4, mov, webm, m4v, 3gp) and \"documents\" (pdf, doc/docx, xls/xlsx, csv, ppt/pptx, txt, rtf, odt/ods/odp). Omit or [] for any file.","type":"array","items":{"type":"string","enum":["images","videos","documents"]}}},"required":["id","label","required","type"]}]},{"oneOf":[{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"type":{"type":"string","const":"text"},"markdown":{"type":"string"}},"required":["id","type","markdown"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"type":{"type":"string","const":"image"},"url":{"type":"string"},"alt":{"type":"string"}},"required":["id","type","url","alt"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"type":{"type":"string","const":"video"},"url":{"type":"string"}},"required":["id","type","url"]}]}]}},"buttonLabel":{"type":"string"},"rules":{"type":"array","items":{"type":"object","properties":{"when":{"minItems":1,"type":"array","items":{"type":"object","properties":{"fieldId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"op":{"type":"string","enum":["equals","not_equals","in","not_in","includes","gt","lt","answered"]},"value":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"array","items":{"type":"string"}}]}},"required":["fieldId","op"]},"description":"All must match (AND)."},"goto":{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"step"},"stepId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","stepId"]},{"type":"object","properties":{"kind":{"type":"string","const":"ending"},"endingId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","endingId"]}],"description":"Where the visitor goes next: { kind: \"step\", stepId } shows that step; { kind: \"ending\", endingId } finishes the form at that ending (the response is submitted)."}},"required":["when","goto"],"description":"For cases one answer can't express (\"if A and B → X\"). May check any question on this step or an earlier one."}},"next":{"anyOf":[{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"step"},"stepId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","stepId"]},{"type":"object","properties":{"kind":{"type":"string","const":"ending"},"endingId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","endingId"]}],"description":"Where the visitor goes next: { kind: \"step\", stepId } shows that step; { kind: \"ending\", endingId } finishes the form at that ending (the response is submitted)."},{"type":"object","properties":{"kind":{"type":"string","const":"following"}},"required":["kind"]}],"description":"The default way on: a step or ending, or { kind: \"following\" }: the next step in order, or after the last step the first ending."}},"required":["id","blocks","rules","next"],"description":"Leaving a step goes to: the first matching rule's goto; else the first question on the step (in block order) whose chosen answer has a goto (option goto, yes_no routes); else `next`."}},"endings":{"minItems":1,"type":"array","items":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$","description":"Unique among endings (its own namespace)."},"name":{"type":"string","minLength":1,"description":"The builder's name for it, e.g. \"Qualified\" or \"Not a fit\". Reported in responses, webhooks and page events."},"action":{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"message"},"title":{"type":"string"},"body":{"type":"string"}},"required":["kind","title","body"],"description":"Show a thank-you message (body is Markdown)."},{"type":"object","properties":{"kind":{"type":"string","const":"redirect"},"url":{"type":"string"},"appendContact":{"type":"boolean","description":"Add first_name, last_name, email and phone to the URL."}},"required":["kind","url","appendContact"],"description":"Send the visitor to a URL (no thank-you screen)."},{"type":"object","properties":{"kind":{"type":"string","const":"handoff"}},"required":["kind"],"description":"Hand back to the site: the form closes and the page gets the forms:submit event, nothing else."}]},"countsAsLead":{"type":"boolean","description":"Reaching this ending reports a Lead to ad tracking (Meta Pixel + CAPI Lead, GA4 generate_lead). Turn off for disqualifying endings."}},"required":["id","name","action","countsAsLead"],"description":"Where a visitor's path stops. Reaching an ending submits the response."},"description":"Every path stops at one of these. The first is where the last step goes by default."},"settings":{"type":"object","properties":{"progressBar":{"type":"boolean"},"partialCapture":{"type":"boolean","description":"Save unfinished responses: keeps what visitors entered even if they don't finish, once there's a valid email, phone or Telegram username (stored with status partial). Webhook events: submission.abandoned (left without finishing after giving a valid email, phone or Telegram username: page closed, or 5 minutes with no activity; once per response), submission.completed. Needs the plan feature partialCapture (workspaces.entitlements features): turning it on without it is 403 plan_feature, and a new form on such a plan starts with it false. If the plan later loses the feature, the live form stops saving unfinished responses at once (no resume, no submission.abandoned) and this stays true, applying again after an upgrade."},"returningVisitor":{"description":"With partialCapture on: what happens when someone with an unfinished response comes back later and finishes. \"update\" (default): in the same browser (a resume token the server issued with the first save, kept in localStorage on the host site; valid 30 days after the last activity, Safari may clear it after 7 days without a visit), their earlier response is completed (same submission id). \"new\": the earlier response stays as it was and a new one is saved. The same email or phone never continues or changes a response, since anyone can type it: in another browser it saves a new response whose earlierSubmissionId links to that unfinished one. A completed response is never changed.","type":"string","enum":["update","new"]},"turnstile":{"type":"boolean"},"submitLabel":{"type":"string","minLength":1},"display":{"type":"object","properties":{"onPage":{"description":"What starts the form on the site. \"button\": the site's own buttons or links with data-forms-open. \"inline\" (Field and Button): the first question on the page with a button beside it (an element with data-forms-inline). \"embed\" (Full form): the whole form in the page, inside an element with data-forms-embed, at that element's width; every step, the progress bar, Back, a texted-code step and the ending stay in that element, and nothing opens over the page (so view and buttonLabel don't apply; the form uses its own Continue and submit labels). \"widget\" (Corner widget): a floating button in a bottom corner, shown as the page loads (an element with data-forms-widget on the page), that opens the form in a card in that corner. Left out: \"widget\" when view is \"widget\", else \"button\".","type":"string","enum":["button","inline","embed","widget"]},"view":{"type":"string","enum":["popup","widget","fullscreen"],"description":"How the form opens: \"popup\" or \"fullscreen\" for a button or inline field; \"widget\" is the corner card and goes with onPage \"widget\". Ignored by onPage \"embed\", which opens nothing (any value is accepted; a data-forms-open button or Forms.open for the same form still opens in this view)."},"side":{"type":"string","enum":["right","left"],"description":"The corner the widget docks to. Ignored by the other views."},"buttonLabel":{"description":"The button's text: the corner widget's floating button and the inline field's button (\"Get started\" when left out or blank). A site's own data-forms-open button keeps its own text; data-forms-button on an inline element overrides this. Ignored by onPage \"embed\".","type":"string","maxLength":40}},"required":["view","side"]},"hideBadge":{"description":"true: the live form leaves out the \"Powered by ayeform\" badge under it (default false: shown). Needs the plan feature hideBadge (workspaces.entitlements features): turning it on without it is 403 plan_feature. If the plan later loses the feature, the live form shows the badge again at once and this stays true, applying again after an upgrade. Like every setting, it goes live on publish.","type":"boolean"}},"required":["progressBar","partialCapture","turnstile","submitLabel"]}},"required":["steps","endings","settings"]}},"required":["brandId","name"]}}}}}},"/v1/forms/{formId}":{"get":{"operationId":"forms.get","summary":"Get a form","description":"Get a form with its draft, its theme and tracking overrides, and who last changed the draft.","tags":["forms"],"parameters":[{"name":"formId","in":"path","required":true,"description":"The form's id.","schema":{"type":"string","description":"The form's id."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The form's id."},"brandId":{"type":"string","description":"The brand the form belongs to."},"workspaceId":{"type":"string","description":"The workspace the form belongs to."},"name":{"type":"string","description":"The form's name."},"status":{"type":"string","enum":["draft","published","unpublished"],"description":"`draft` until it's first published, then `published` while it's live. `unpublished` once it's taken offline; its versions are kept."},"publishedVersion":{"description":"The number of the live version. Null if the form was never published.","type":["number","null"]},"publishedAt":{"description":"When the latest version was published, in milliseconds since the epoch. Null if never.","type":["number","null"]},"hasUnpublishedChanges":{"type":"boolean","description":"Whether the draft or theme overrides changed since the form was last published."},"changedAt":{"description":"When the draft or theme overrides last changed, in milliseconds since the epoch. Null if they were never edited.","type":["number","null"]},"partialCapture":{"type":"boolean","description":"Whether the draft has \"Save unfinished responses\" on. The webhook event for people who left (`submission.abandoned`) needs it, and the plan feature `partialCapture` too."},"stepCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many steps the draft has."},"blockCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many questions and other blocks the draft has, across all steps."},"createdAt":{"type":"number","description":"When the form was created, in milliseconds since the epoch."},"updatedAt":{"type":"number","description":"When anything about the form last changed, in milliseconds since the epoch."},"draft":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{},"description":"The whole form as it is now, unpublished edits included. It holds `steps` in order, then `endings`, and `settings`, in the same shape as `draft` in `PATCH /v1/forms/{formId}`."},"themeOverrides":{"type":"object","properties":{"accent":{"type":"string"},"accentText":{"type":"string"},"background":{"type":"string"},"surface":{"type":"string"},"text":{"type":"string"},"muted":{"type":"string"},"border":{"type":"string"},"radius":{"type":"number","minimum":0,"maximum":48},"buttonRadius":{"type":"number","minimum":0,"maximum":48},"font":{"type":"string"},"headingFont":{"type":"string","maxLength":500},"backdrop":{"type":"string"},"fontStylesheet":{"type":"string","maxLength":2000,"format":"uri"}},"additionalProperties":false,"description":"Theme values that override the brand theme for this form."},"trackingOverrides":{"type":"object","properties":{"meta":{"type":"boolean","description":"Whether this form fires the brand's Meta Lead, in the browser and through the Conversions API."},"ga4":{"type":"boolean","description":"Whether this form fires the brand's GA4 `generate_lead` event."},"jornayaCampaignKey":{"description":"This form's own Jornaya campaign key. Leave it out to use the brand's. It applies while the brand has Jornaya on. A page loads one campaign script, so with two forms on a page the first one's key is used.","type":"string"}},"required":["meta","ga4"],"additionalProperties":false,"description":"Whether this form fires the brand's Meta and GA4 tracking, and its own Jornaya campaign key."},"pictures":{"type":"array","items":{"type":"object","properties":{"blockId":{"type":"string","description":"The id of the image choice question."},"value":{"type":"string","description":"The `value` of the option the picture is for."},"status":{"type":"string","enum":["making","added"],"description":"`making` while AI makes the picture. `added` once it's on the option as `imageId` in the draft."},"imageId":{"description":"The picture's id once it's added. Null while it's being made.","type":["string","null"]}},"required":["blockId","value","status","imageId"],"additionalProperties":false},"description":"Pictures AI is making in the background for image choice options after an AI build or fix. If a picture can't be made, or its option was removed or got a picture in the meantime, it drops off this list and the option stays as it is. You can add a picture yourself with `POST /v1/forms/{formId}/images`. Publishing while a picture is still `making` fails like a missing picture would, with a message that it's still coming."},"changedBy":{"anyOf":[{"anyOf":[{"type":"object","properties":{"kind":{"type":"string","const":"user"},"email":{"type":"string","description":"The team member's email."}},"required":["kind","email"],"additionalProperties":false},{"type":"object","properties":{"kind":{"type":"string","const":"apiKey"},"name":{"type":"string","description":"The API key's name."}},"required":["kind","name"],"additionalProperties":false}]},{"type":"null"}],"description":"Who last changed the draft or theme overrides. It's a team member's email or an API key's name, or null if unknown."}},"required":["id","brandId","workspaceId","name","status","publishedVersion","publishedAt","hasUnpublishedChanges","changedAt","partialCapture","stepCount","blockCount","createdAt","updatedAt","draft","themeOverrides","trackingOverrides","pictures","changedBy"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}},"patch":{"operationId":"forms.update","summary":"Update a form","description":"Update a form's draft, name or overrides. Each field you send replaces the stored value. The live form changes only when you publish. See [Paid checks](/docs/api/plan-checks) and [AI builds and fixes](/docs/api/form-busy).","tags":["forms"],"parameters":[{"name":"formId","in":"path","required":true,"description":"The form's id.","schema":{"type":"string","description":"The form's id."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The form's id."},"brandId":{"type":"string","description":"The brand the form belongs to."},"workspaceId":{"type":"string","description":"The workspace the form belongs to."},"name":{"type":"string","description":"The form's name."},"status":{"type":"string","enum":["draft","published","unpublished"],"description":"`draft` until it's first published, then `published` while it's live. `unpublished` once it's taken offline; its versions are kept."},"publishedVersion":{"description":"The number of the live version. Null if the form was never published.","type":["number","null"]},"publishedAt":{"description":"When the latest version was published, in milliseconds since the epoch. Null if never.","type":["number","null"]},"hasUnpublishedChanges":{"type":"boolean","description":"Whether the draft or theme overrides changed since the form was last published."},"changedAt":{"description":"When the draft or theme overrides last changed, in milliseconds since the epoch. Null if they were never edited.","type":["number","null"]},"partialCapture":{"type":"boolean","description":"Whether the draft has \"Save unfinished responses\" on. The webhook event for people who left (`submission.abandoned`) needs it, and the plan feature `partialCapture` too."},"stepCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many steps the draft has."},"blockCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many questions and other blocks the draft has, across all steps."},"createdAt":{"type":"number","description":"When the form was created, in milliseconds since the epoch."},"updatedAt":{"type":"number","description":"When anything about the form last changed, in milliseconds since the epoch."},"draft":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{},"description":"The whole form as it is now, unpublished edits included. It holds `steps` in order, then `endings`, and `settings`, in the same shape as `draft` in `PATCH /v1/forms/{formId}`."},"themeOverrides":{"type":"object","properties":{"accent":{"type":"string"},"accentText":{"type":"string"},"background":{"type":"string"},"surface":{"type":"string"},"text":{"type":"string"},"muted":{"type":"string"},"border":{"type":"string"},"radius":{"type":"number","minimum":0,"maximum":48},"buttonRadius":{"type":"number","minimum":0,"maximum":48},"font":{"type":"string"},"headingFont":{"type":"string","maxLength":500},"backdrop":{"type":"string"},"fontStylesheet":{"type":"string","maxLength":2000,"format":"uri"}},"additionalProperties":false,"description":"Theme values that override the brand theme for this form."},"trackingOverrides":{"type":"object","properties":{"meta":{"type":"boolean","description":"Whether this form fires the brand's Meta Lead, in the browser and through the Conversions API."},"ga4":{"type":"boolean","description":"Whether this form fires the brand's GA4 `generate_lead` event."},"jornayaCampaignKey":{"description":"This form's own Jornaya campaign key. Leave it out to use the brand's. It applies while the brand has Jornaya on. A page loads one campaign script, so with two forms on a page the first one's key is used.","type":"string"}},"required":["meta","ga4"],"additionalProperties":false,"description":"Whether this form fires the brand's Meta and GA4 tracking, and its own Jornaya campaign key."},"pictures":{"type":"array","items":{"type":"object","properties":{"blockId":{"type":"string","description":"The id of the image choice question."},"value":{"type":"string","description":"The `value` of the option the picture is for."},"status":{"type":"string","enum":["making","added"],"description":"`making` while AI makes the picture. `added` once it's on the option as `imageId` in the draft."},"imageId":{"description":"The picture's id once it's added. Null while it's being made.","type":["string","null"]}},"required":["blockId","value","status","imageId"],"additionalProperties":false},"description":"Pictures AI is making in the background for image choice options after an AI build or fix. If a picture can't be made, or its option was removed or got a picture in the meantime, it drops off this list and the option stays as it is. You can add a picture yourself with `POST /v1/forms/{formId}/images`. Publishing while a picture is still `making` fails like a missing picture would, with a message that it's still coming."}},"required":["id","brandId","workspaceId","name","status","publishedVersion","publishedAt","hasUnpublishedChanges","changedAt","partialCapture","stepCount","blockCount","createdAt","updatedAt","draft","themeOverrides","trackingOverrides","pictures"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"description":"The form's name.","type":"string","minLength":1,"maxLength":200},"draft":{"description":"The whole form, with `steps` in order and then `endings`. A visitor walks through the steps. When they leave a step, the step's first matching rule decides where they go. Without one, the first question on the step whose chosen answer has a goto decides; that's an option's `goto` on single choice and dropdown questions, or `routes` on yes_no. Otherwise the step's `next` decides, and `\"following\"` means the next step, or the first ending after the last step. Every path stops at an ending, which submits the response. An ending shows a message, redirects, or hands back to the site, and says whether it counts as a lead for ad tracking. To disqualify someone, route them to an ending that doesn't count as a lead. Here the draft is only checked for its shape. `POST /v1/forms/{formId}/publish` checks that everything it refers to exists.","type":"object","properties":{"steps":{"minItems":1,"type":"array","items":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"title":{"type":"string"},"description":{"type":"string"},"blocks":{"type":"array","items":{"anyOf":[{"oneOf":[{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"single_choice"},"options":{"minItems":1,"maxItems":8,"type":"array","items":{"type":"object","properties":{"value":{"type":"string","minLength":1},"label":{"type":"string","minLength":1},"goto":{"description":"Single choice and dropdown only: where choosing this answer sends the visitor (e.g. \"No, I rent\" → the \"Not a fit\" ending). Used when no rule on the step matched. Omit to follow the step's `next`.","oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"step"},"stepId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","stepId"]},{"type":"object","properties":{"kind":{"type":"string","const":"ending"},"endingId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","endingId"]}]}},"required":["value","label"]}},"autoAdvance":{"type":"boolean"}},"required":["id","label","required","type","options","autoAdvance"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"multi_choice"},"options":{"minItems":1,"maxItems":8,"type":"array","items":{"type":"object","properties":{"value":{"type":"string","minLength":1},"label":{"type":"string","minLength":1},"goto":{"description":"Single choice and dropdown only: where choosing this answer sends the visitor (e.g. \"No, I rent\" → the \"Not a fit\" ending). Used when no rule on the step matched. Omit to follow the step's `next`.","oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"step"},"stepId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","stepId"]},{"type":"object","properties":{"kind":{"type":"string","const":"ending"},"endingId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","endingId"]}]}},"required":["value","label"]}},"minSelected":{"type":"integer","minimum":0,"maximum":9007199254740991},"maxSelected":{"type":"integer","minimum":1,"maximum":9007199254740991}},"required":["id","label","required","type","options"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"image_choice"},"options":{"minItems":1,"maxItems":12,"type":"array","items":{"type":"object","properties":{"value":{"type":"string","minLength":1,"description":"The answer value (what responses, webhooks and rules see)."},"label":{"description":"Words under the picture. Optional: without one the picture shows on its own (give it `alt` text).","type":"string"},"imageId":{"description":"An image uploaded to this form (images.uploadUrl + images.create, or images.create with a url). Served to visitors at {apiBase}/v1/public/images/{imageId}.","type":"string","pattern":"^[A-Za-z0-9_;-]{1,64}$"},"imageUrl":{"description":"Or a picture on another site, shown as is (https://…). Prefer imageId: it is checked, sized and served by us.","type":"string"},"alt":{"description":"What the picture shows, for screen readers. Needed when there's no label; screen readers otherwise read the value.","type":"string"},"goto":{"description":"Single selection only (multiple: false): where choosing this picture sends the visitor, like a single choice option's goto.","oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"step"},"stepId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","stepId"]},{"type":"object","properties":{"kind":{"type":"string","const":"ending"},"endingId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","endingId"]}]}},"required":["value"]}},"multiple":{"type":"boolean","description":"Visitors can pick several pictures (the answer is a list of values); else one (the answer is a value)."},"autoAdvance":{"type":"boolean","description":"Single selection only: picking a picture moves on, like single choice. Ignored with multiple."},"minSelected":{"description":"Multiple only: fewest pictures to pick.","type":"integer","minimum":0,"maximum":9007199254740991},"maxSelected":{"description":"Multiple only: most pictures to pick.","type":"integer","minimum":1,"maximum":9007199254740991}},"required":["id","label","required","type","options","multiple","autoAdvance"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"dropdown"},"options":{"minItems":1,"type":"array","items":{"type":"object","properties":{"value":{"type":"string","minLength":1},"label":{"type":"string","minLength":1},"goto":{"description":"Single choice and dropdown only: where choosing this answer sends the visitor (e.g. \"No, I rent\" → the \"Not a fit\" ending). Used when no rule on the step matched. Omit to follow the step's `next`.","oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"step"},"stepId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","stepId"]},{"type":"object","properties":{"kind":{"type":"string","const":"ending"},"endingId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","endingId"]}]}},"required":["value","label"]}},"placeholder":{"type":"string"}},"required":["id","label","required","type","options"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"yes_no"},"routes":{"description":"Where answering Yes / No sends the visitor, like an option's `goto`. Omit either to follow the step's `next`.","type":"object","properties":{"yes":{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"step"},"stepId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","stepId"]},{"type":"object","properties":{"kind":{"type":"string","const":"ending"},"endingId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","endingId"]}],"description":"Where the visitor goes next: { kind: \"step\", stepId } shows that step; { kind: \"ending\", endingId } finishes the form at that ending (the response is submitted)."},"no":{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"step"},"stepId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","stepId"]},{"type":"object","properties":{"kind":{"type":"string","const":"ending"},"endingId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","endingId"]}],"description":"Where the visitor goes next: { kind: \"step\", stepId } shows that step; { kind: \"ending\", endingId } finishes the form at that ending (the response is submitted)."}}}},"required":["id","label","required","type"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","enum":["short_text","long_text"]},"placeholder":{"type":"string"},"maxLength":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"required":["id","label","required","type"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"name"},"split":{"type":"boolean"}},"required":["id","label","required","type","split"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"email"},"validate":{"description":"Check the address: when the visitor continues past this field's step (or submits), the address is checked with an email validation service. One that doesn't exist or is disposable gets a warning once; continuing again with the same address goes on, and the response's emailCheck records the result (\"invalid\", \"disposable\", \"valid\", \"catchall\", \"unknown\"): a check never turns a lead away. A likely typo gets a \"Did you mean …?\". Costs credits per checked address (credits.get prices.emailValidation) and needs plan feature verification (workspaces.entitlements). Out of credits, without the feature, or if the service is down, the visitor goes on and the response's emailCheck is \"skipped\". Omit or false: not checked.","type":"boolean"}},"required":["id","label","required","type"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"telegram"},"check":{"description":"Check the account exists: when the visitor continues past this field's step (or submits), the username is looked up on Telegram. One not found on Telegram, or that belongs to a channel, group or bot, gets a warning once; continuing again with the same username goes on, and the response's telegramCheck records the result (\"found\", \"not_found\", \"channel\", \"group\", \"bot\"): a check never turns a lead away (\"not_found\" isn't proof: some taken usernames look free on Telegram). A username in the wrong shape still can't be submitted. Costs credits per checked username (credits.get prices.telegramCheck) and needs plan feature verification (workspaces.entitlements). Out of credits, without the feature, or if Telegram doesn't answer, the visitor goes on and the response's telegramCheck is \"skipped\". Omit or false: not checked. The answer is stored as the username without \"@\" (\"@name\", \"t.me/name\" and links are accepted).","type":"boolean"}},"required":["id","label","required","type"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"phone"},"consent":{"description":"Consent to be called and texted at this number (TCPA), recorded with the time, the wording and the number when the visitor agrees, before any code is texted (verify). The wording always shows under the input. A consent no longer counts once the visitor changes the number; submitting the step again records it for the new one. Omit for no consent. The response's consent, consentAt and consentText report it.","type":"object","properties":{"agreeBy":{"type":"string","enum":["submit","checkbox"],"description":"\"submit\": pressing the step's Continue or Submit (or Enter) is the agreement. \"checkbox\": a required box under the input, ticked to agree."},"text":{"type":"string","minLength":1,"maxLength":2000,"description":"The disclosure under the phone input, as inline Markdown (links like [terms](https://…) allowed). It names who may call and text, says it may be by autodialer or recorded voice and that agreeing isn't required to buy."}},"required":["agreeBy","text"]},"verify":{"type":"boolean","description":"Verify by text: when the visitor continues past this field's step, a 6-digit code is texted to the number and must be entered before they can move on. Every visitor who reaches the field is verified. At most one per form. Costs credits per approved code (credits.get prices.phoneVerification) and needs plan feature verification (workspaces.entitlements); out of credits or without the feature, no code is sent, the visitor goes on and the response has phoneVerified false."},"defaultCountry":{"description":"Country selected when the form opens (ISO 3166-1 alpha-2: two capital letters). Omit for the visitor's country, guessed from the browser's time zone and language; if that isn't allowed, the first of `countries`. Must be one of `countries` when those are set.","type":"string"},"countries":{"description":"Only these countries can be picked and only their numbers are accepted (ISO 3166-1 alpha-2 codes; the first is the fallback default). One country shows a fixed prefix with no picker. Omit or [] for every country.","maxItems":250,"type":"array","items":{"type":"string"}},"mobileOnly":{"description":"Only accept mobile numbers. Numbers whose country can't tell mobiles from landlines (the US, Canada and the rest of +1) all pass.","type":"boolean"}},"required":["id","label","required","type","verify"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"number"},"min":{"type":"number"},"max":{"type":"number"}},"required":["id","label","required","type"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"currency"},"currency":{"description":"The currency of the amount, an ISO 4217 code (three capital letters). Omit for US dollars (\"USD\"). The answer is a plain number in its major unit (1234.5 = 1,234.50); min and max are in it too.","type":"string"},"min":{"type":"number"},"max":{"type":"number"}},"required":["id","label","required","type"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"slider"},"min":{"type":"number","description":"The lowest value, at the slider's left end."},"max":{"type":"number","description":"The highest value, at the right end. More than min."},"step":{"type":"number","exclusiveMinimum":0,"description":"How much each notch adds: the values are min, min + step, … up to max (max − min must be a whole number of steps)."},"prefix":{"description":"Shown before the number, e.g. \"$\".","type":"string","maxLength":10},"suffix":{"description":"Shown after the number, e.g. \"%\" or \"km\" (words get a space: \"25 km\").","type":"string","maxLength":20},"defaultValue":{"description":"Where the slider starts, one of its values. Omit to start at min. The visitor's answer is where they leave it.","type":"number"},"orMore":{"description":"The top value means \"or more\": it shows with a + after it (read out \"or more\"). The answer is still max.","type":"boolean"}},"required":["id","label","required","type","min","max","step"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"ai_autocomplete"},"prompt":{"type":"string","maxLength":500,"description":"What to suggest as the visitor types, in plain words, e.g. \"Car makes and models sold in the US\". From their first 2 characters, an AI model suggests up to 8 answers. Costs credits once per visitor who sees suggestions (credits.get prices.aiAutocomplete) and needs plan feature aiFields (workspaces.entitlements). Out of credits, without the feature, or if the model is slow or down: no suggestions, the field works as a plain text box. Required to publish."},"allowCustom":{"description":"Omit or true: the visitor may keep their own words. false: when there are suggestions for what they typed, they must pick one (the form checks it; with no suggestions to pick from, their own words are kept, and the API accepts any text).","type":"boolean"},"placeholder":{"type":"string"}},"required":["id","label","required","type","prompt"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"date"}},"required":["id","label","required","type"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"address"},"include":{"type":"object","properties":{"line2":{"type":"boolean"}},"required":["line2"]},"defaultCountry":{"description":"Country selected when the form opens (ISO 3166-1 alpha-2: two capital letters). Omit for the visitor's country, guessed from the browser's time zone and language; if that isn't allowed, the first of `countries`. Must be one of `countries` when those are set. The visitor can pick another allowed country; the address's parts, their names and order, which ones are required and the postcode's format follow the picked country.","type":"string"},"countries":{"description":"Only addresses in these countries can be entered (ISO 3166-1 alpha-2 codes; the first is the fallback default). One country shows no country picker. Omit or [] for every country.","maxItems":250,"type":"array","items":{"type":"string"}}},"required":["id","label","required","type","include"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"consent"},"text":{"type":"string","minLength":1},"input":{"type":"string","enum":["checkbox","implied"]}},"required":["id","label","required","type","text","input"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"hidden"},"source":{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"url"},"param":{"type":"string","minLength":1}},"required":["kind","param"]},{"type":"object","properties":{"kind":{"type":"string","const":"host"}},"required":["kind"]}]}},"required":["id","label","required","type","source"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"label":{"type":"string"},"help":{"type":"string"},"required":{"type":"boolean"},"type":{"type":"string","const":"file_upload"},"maxFiles":{"description":"Most files a visitor can upload here, 1–10 (default 10). Whatever this says, a field takes at most 10 files and 20 MB in total per response.","type":"integer","minimum":1,"maximum":10},"accept":{"description":"Kinds of files allowed: any of \"images\" (jpg, png, gif, webp, heic, avif), \"videos\" (mp4, mov, webm, m4v, 3gp) and \"documents\" (pdf, doc/docx, xls/xlsx, csv, ppt/pptx, txt, rtf, odt/ods/odp). Omit or [] for any file.","type":"array","items":{"type":"string","enum":["images","videos","documents"]}}},"required":["id","label","required","type"]}]},{"oneOf":[{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"type":{"type":"string","const":"text"},"markdown":{"type":"string"}},"required":["id","type","markdown"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"type":{"type":"string","const":"image"},"url":{"type":"string"},"alt":{"type":"string"}},"required":["id","type","url","alt"]},{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"type":{"type":"string","const":"video"},"url":{"type":"string"}},"required":["id","type","url"]}]}]}},"buttonLabel":{"type":"string"},"rules":{"type":"array","items":{"type":"object","properties":{"when":{"minItems":1,"type":"array","items":{"type":"object","properties":{"fieldId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"},"op":{"type":"string","enum":["equals","not_equals","in","not_in","includes","gt","lt","answered"]},"value":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"array","items":{"type":"string"}}]}},"required":["fieldId","op"]},"description":"All must match (AND)."},"goto":{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"step"},"stepId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","stepId"]},{"type":"object","properties":{"kind":{"type":"string","const":"ending"},"endingId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","endingId"]}],"description":"Where the visitor goes next: { kind: \"step\", stepId } shows that step; { kind: \"ending\", endingId } finishes the form at that ending (the response is submitted)."}},"required":["when","goto"],"description":"For cases one answer can't express (\"if A and B → X\"). May check any question on this step or an earlier one."}},"next":{"anyOf":[{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"step"},"stepId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","stepId"]},{"type":"object","properties":{"kind":{"type":"string","const":"ending"},"endingId":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$"}},"required":["kind","endingId"]}],"description":"Where the visitor goes next: { kind: \"step\", stepId } shows that step; { kind: \"ending\", endingId } finishes the form at that ending (the response is submitted)."},{"type":"object","properties":{"kind":{"type":"string","const":"following"}},"required":["kind"]}],"description":"The default way on: a step or ending, or { kind: \"following\" }: the next step in order, or after the last step the first ending."}},"required":["id","blocks","rules","next"],"description":"Leaving a step goes to: the first matching rule's goto; else the first question on the step (in block order) whose chosen answer has a goto (option goto, yes_no routes); else `next`."}},"endings":{"minItems":1,"type":"array","items":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-zA-Z0-9_]{0,63}$","description":"Unique among endings (its own namespace)."},"name":{"type":"string","minLength":1,"description":"The builder's name for it, e.g. \"Qualified\" or \"Not a fit\". Reported in responses, webhooks and page events."},"action":{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"message"},"title":{"type":"string"},"body":{"type":"string"}},"required":["kind","title","body"],"description":"Show a thank-you message (body is Markdown)."},{"type":"object","properties":{"kind":{"type":"string","const":"redirect"},"url":{"type":"string"},"appendContact":{"type":"boolean","description":"Add first_name, last_name, email and phone to the URL."}},"required":["kind","url","appendContact"],"description":"Send the visitor to a URL (no thank-you screen)."},{"type":"object","properties":{"kind":{"type":"string","const":"handoff"}},"required":["kind"],"description":"Hand back to the site: the form closes and the page gets the forms:submit event, nothing else."}]},"countsAsLead":{"type":"boolean","description":"Reaching this ending reports a Lead to ad tracking (Meta Pixel + CAPI Lead, GA4 generate_lead). Turn off for disqualifying endings."}},"required":["id","name","action","countsAsLead"],"description":"Where a visitor's path stops. Reaching an ending submits the response."},"description":"Every path stops at one of these. The first is where the last step goes by default."},"settings":{"type":"object","properties":{"progressBar":{"type":"boolean"},"partialCapture":{"type":"boolean","description":"Save unfinished responses: keeps what visitors entered even if they don't finish, once there's a valid email, phone or Telegram username (stored with status partial). Webhook events: submission.abandoned (left without finishing after giving a valid email, phone or Telegram username: page closed, or 5 minutes with no activity; once per response), submission.completed. Needs the plan feature partialCapture (workspaces.entitlements features): turning it on without it is 403 plan_feature, and a new form on such a plan starts with it false. If the plan later loses the feature, the live form stops saving unfinished responses at once (no resume, no submission.abandoned) and this stays true, applying again after an upgrade."},"returningVisitor":{"description":"With partialCapture on: what happens when someone with an unfinished response comes back later and finishes. \"update\" (default): in the same browser (a resume token the server issued with the first save, kept in localStorage on the host site; valid 30 days after the last activity, Safari may clear it after 7 days without a visit), their earlier response is completed (same submission id). \"new\": the earlier response stays as it was and a new one is saved. The same email or phone never continues or changes a response, since anyone can type it: in another browser it saves a new response whose earlierSubmissionId links to that unfinished one. A completed response is never changed.","type":"string","enum":["update","new"]},"turnstile":{"type":"boolean"},"submitLabel":{"type":"string","minLength":1},"display":{"type":"object","properties":{"onPage":{"description":"What starts the form on the site. \"button\": the site's own buttons or links with data-forms-open. \"inline\" (Field and Button): the first question on the page with a button beside it (an element with data-forms-inline). \"embed\" (Full form): the whole form in the page, inside an element with data-forms-embed, at that element's width; every step, the progress bar, Back, a texted-code step and the ending stay in that element, and nothing opens over the page (so view and buttonLabel don't apply; the form uses its own Continue and submit labels). \"widget\" (Corner widget): a floating button in a bottom corner, shown as the page loads (an element with data-forms-widget on the page), that opens the form in a card in that corner. Left out: \"widget\" when view is \"widget\", else \"button\".","type":"string","enum":["button","inline","embed","widget"]},"view":{"type":"string","enum":["popup","widget","fullscreen"],"description":"How the form opens: \"popup\" or \"fullscreen\" for a button or inline field; \"widget\" is the corner card and goes with onPage \"widget\". Ignored by onPage \"embed\", which opens nothing (any value is accepted; a data-forms-open button or Forms.open for the same form still opens in this view)."},"side":{"type":"string","enum":["right","left"],"description":"The corner the widget docks to. Ignored by the other views."},"buttonLabel":{"description":"The button's text: the corner widget's floating button and the inline field's button (\"Get started\" when left out or blank). A site's own data-forms-open button keeps its own text; data-forms-button on an inline element overrides this. Ignored by onPage \"embed\".","type":"string","maxLength":40}},"required":["view","side"]},"hideBadge":{"description":"true: the live form leaves out the \"Powered by ayeform\" badge under it (default false: shown). Needs the plan feature hideBadge (workspaces.entitlements features): turning it on without it is 403 plan_feature. If the plan later loses the feature, the live form shows the badge again at once and this stays true, applying again after an upgrade. Like every setting, it goes live on publish.","type":"boolean"}},"required":["progressBar","partialCapture","turnstile","submitLabel"]}},"required":["steps","endings","settings"]},"themeOverrides":{"description":"Theme values that override the brand theme for this form.","type":"object","properties":{"accent":{"type":"string"},"accentText":{"type":"string"},"background":{"type":"string"},"surface":{"type":"string"},"text":{"type":"string"},"muted":{"type":"string"},"border":{"type":"string"},"radius":{"type":"number","minimum":0,"maximum":48},"buttonRadius":{"type":"number","minimum":0,"maximum":48},"font":{"type":"string"},"headingFont":{"type":"string","maxLength":500},"backdrop":{"type":"string"},"fontStylesheet":{"type":"string","maxLength":2000,"format":"uri"}}},"trackingOverrides":{"description":"Whether this form fires the brand's Meta and GA4 tracking, and its own Jornaya campaign key. Setting a campaign key needs the plan's `leadId` feature, or it answers `403 plan_feature`.","type":"object","properties":{"meta":{"type":"boolean","description":"Whether this form fires the brand's Meta Lead, in the browser and through the Conversions API."},"ga4":{"type":"boolean","description":"Whether this form fires the brand's GA4 `generate_lead` event."},"jornayaCampaignKey":{"description":"This form's own Jornaya campaign key. Leave it out to use the brand's. It applies while the brand has Jornaya on. A page loads one campaign script, so with two forms on a page the first one's key is used.","type":"string"}},"required":["meta","ga4"]}}}}}}},"delete":{"operationId":"forms.delete","summary":"Delete a form","description":"Delete a form with its versions and its pictures. Its responses are kept.","tags":["forms"],"parameters":[{"name":"formId","in":"path","required":true,"description":"The form's id.","schema":{"type":"string","description":"The form's id."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The deleted form's id."},"deleted":{"type":"boolean","const":true}},"required":["id","deleted"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/forms/{formId}/publish":{"post":{"operationId":"forms.publish","summary":"Publish a form","description":"Publish the current draft as a new version, live wherever the form is embedded. If the draft has errors, it answers `422 publish_check_failed` and nothing is published. Fix them yourself, or have AI fix them with `POST /v1/forms/{formId}/fix`. A suspended workspace gets `402 workspace_suspended`. That happens when its trial ended unpaid or its subscription lapsed (see `GET /v1/workspaces/{workspaceId}/billing`). Once the owner picks a plan, its published forms come back on their own. See [Paid checks](/docs/api/plan-checks) and [AI builds and fixes](/docs/api/form-busy).","tags":["forms"],"parameters":[{"name":"formId","in":"path","required":true,"description":"The form's id.","schema":{"type":"string","description":"The form's id."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"formId":{"type":"string","description":"The form's id."},"version":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The new version's number."},"publishedAt":{"type":"number","description":"When it was published, in milliseconds since the epoch."},"warnings":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Where in the draft the problem is, such as `steps.2.blocks.0`."},"message":{"type":"string","description":"The problem, in plain words."},"severity":{"type":"string","const":"warning"}},"required":["path","message","severity"],"additionalProperties":false},"description":"Problems that don't block publishing, such as a step nobody sees, an ending nobody reaches, or a goto to something deleted, which the live form skips. Errors do block publishing. They include duplicate ids, a missing URL, a rule that checks a question asked later, and a loop that never reaches an ending. Errors come back in the 422's `issues` in the same shape, with severity `\"error\"`."}},"required":["formId","version","publishedAt","warnings"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}},"/v1/forms/{formId}/unpublish":{"post":{"operationId":"forms.unpublish","summary":"Unpublish a form","description":"Take a form offline, so embeds stop loading it. Its versions are kept, and publishing again brings it back.","tags":["forms"],"parameters":[{"name":"formId","in":"path","required":true,"description":"The form's id.","schema":{"type":"string","description":"The form's id."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The form's id."},"brandId":{"type":"string","description":"The brand the form belongs to."},"workspaceId":{"type":"string","description":"The workspace the form belongs to."},"name":{"type":"string","description":"The form's name."},"status":{"type":"string","enum":["draft","published","unpublished"],"description":"`draft` until it's first published, then `published` while it's live. `unpublished` once it's taken offline; its versions are kept."},"publishedVersion":{"description":"The number of the live version. Null if the form was never published.","type":["number","null"]},"publishedAt":{"description":"When the latest version was published, in milliseconds since the epoch. Null if never.","type":["number","null"]},"hasUnpublishedChanges":{"type":"boolean","description":"Whether the draft or theme overrides changed since the form was last published."},"changedAt":{"description":"When the draft or theme overrides last changed, in milliseconds since the epoch. Null if they were never edited.","type":["number","null"]},"partialCapture":{"type":"boolean","description":"Whether the draft has \"Save unfinished responses\" on. The webhook event for people who left (`submission.abandoned`) needs it, and the plan feature `partialCapture` too."},"stepCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many steps the draft has."},"blockCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many questions and other blocks the draft has, across all steps."},"createdAt":{"type":"number","description":"When the form was created, in milliseconds since the epoch."},"updatedAt":{"type":"number","description":"When anything about the form last changed, in milliseconds since the epoch."},"draft":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{},"description":"The whole form as it is now, unpublished edits included. It holds `steps` in order, then `endings`, and `settings`, in the same shape as `draft` in `PATCH /v1/forms/{formId}`."},"themeOverrides":{"type":"object","properties":{"accent":{"type":"string"},"accentText":{"type":"string"},"background":{"type":"string"},"surface":{"type":"string"},"text":{"type":"string"},"muted":{"type":"string"},"border":{"type":"string"},"radius":{"type":"number","minimum":0,"maximum":48},"buttonRadius":{"type":"number","minimum":0,"maximum":48},"font":{"type":"string"},"headingFont":{"type":"string","maxLength":500},"backdrop":{"type":"string"},"fontStylesheet":{"type":"string","maxLength":2000,"format":"uri"}},"additionalProperties":false,"description":"Theme values that override the brand theme for this form."},"trackingOverrides":{"type":"object","properties":{"meta":{"type":"boolean","description":"Whether this form fires the brand's Meta Lead, in the browser and through the Conversions API."},"ga4":{"type":"boolean","description":"Whether this form fires the brand's GA4 `generate_lead` event."},"jornayaCampaignKey":{"description":"This form's own Jornaya campaign key. Leave it out to use the brand's. It applies while the brand has Jornaya on. A page loads one campaign script, so with two forms on a page the first one's key is used.","type":"string"}},"required":["meta","ga4"],"additionalProperties":false,"description":"Whether this form fires the brand's Meta and GA4 tracking, and its own Jornaya campaign key."},"pictures":{"type":"array","items":{"type":"object","properties":{"blockId":{"type":"string","description":"The id of the image choice question."},"value":{"type":"string","description":"The `value` of the option the picture is for."},"status":{"type":"string","enum":["making","added"],"description":"`making` while AI makes the picture. `added` once it's on the option as `imageId` in the draft."},"imageId":{"description":"The picture's id once it's added. Null while it's being made.","type":["string","null"]}},"required":["blockId","value","status","imageId"],"additionalProperties":false},"description":"Pictures AI is making in the background for image choice options after an AI build or fix. If a picture can't be made, or its option was removed or got a picture in the meantime, it drops off this list and the option stays as it is. You can add a picture yourself with `POST /v1/forms/{formId}/images`. Publishing while a picture is still `making` fails like a missing picture would, with a message that it's still coming."}},"required":["id","brandId","workspaceId","name","status","publishedVersion","publishedAt","hasUnpublishedChanges","changedAt","partialCapture","stepCount","blockCount","createdAt","updatedAt","draft","themeOverrides","trackingOverrides","pictures"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}},"/v1/forms/{formId}/duplicate":{"post":{"operationId":"forms.duplicate","summary":"Duplicate a form","description":"Copy a form's draft and overrides into a new draft named \"<name> (copy)\". Checks that are on in the draft are copied as they are, whatever the plan (see [Paid checks](/docs/api/plan-checks)). Past the plan's form limit, it answers `403 plan_limit`.","tags":["forms"],"parameters":[{"name":"formId","in":"path","required":true,"description":"The form's id.","schema":{"type":"string","description":"The form's id."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The form's id."},"brandId":{"type":"string","description":"The brand the form belongs to."},"workspaceId":{"type":"string","description":"The workspace the form belongs to."},"name":{"type":"string","description":"The form's name."},"status":{"type":"string","enum":["draft","published","unpublished"],"description":"`draft` until it's first published, then `published` while it's live. `unpublished` once it's taken offline; its versions are kept."},"publishedVersion":{"description":"The number of the live version. Null if the form was never published.","type":["number","null"]},"publishedAt":{"description":"When the latest version was published, in milliseconds since the epoch. Null if never.","type":["number","null"]},"hasUnpublishedChanges":{"type":"boolean","description":"Whether the draft or theme overrides changed since the form was last published."},"changedAt":{"description":"When the draft or theme overrides last changed, in milliseconds since the epoch. Null if they were never edited.","type":["number","null"]},"partialCapture":{"type":"boolean","description":"Whether the draft has \"Save unfinished responses\" on. The webhook event for people who left (`submission.abandoned`) needs it, and the plan feature `partialCapture` too."},"stepCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many steps the draft has."},"blockCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many questions and other blocks the draft has, across all steps."},"createdAt":{"type":"number","description":"When the form was created, in milliseconds since the epoch."},"updatedAt":{"type":"number","description":"When anything about the form last changed, in milliseconds since the epoch."},"draft":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{},"description":"The whole form as it is now, unpublished edits included. It holds `steps` in order, then `endings`, and `settings`, in the same shape as `draft` in `PATCH /v1/forms/{formId}`."},"themeOverrides":{"type":"object","properties":{"accent":{"type":"string"},"accentText":{"type":"string"},"background":{"type":"string"},"surface":{"type":"string"},"text":{"type":"string"},"muted":{"type":"string"},"border":{"type":"string"},"radius":{"type":"number","minimum":0,"maximum":48},"buttonRadius":{"type":"number","minimum":0,"maximum":48},"font":{"type":"string"},"headingFont":{"type":"string","maxLength":500},"backdrop":{"type":"string"},"fontStylesheet":{"type":"string","maxLength":2000,"format":"uri"}},"additionalProperties":false,"description":"Theme values that override the brand theme for this form."},"trackingOverrides":{"type":"object","properties":{"meta":{"type":"boolean","description":"Whether this form fires the brand's Meta Lead, in the browser and through the Conversions API."},"ga4":{"type":"boolean","description":"Whether this form fires the brand's GA4 `generate_lead` event."},"jornayaCampaignKey":{"description":"This form's own Jornaya campaign key. Leave it out to use the brand's. It applies while the brand has Jornaya on. A page loads one campaign script, so with two forms on a page the first one's key is used.","type":"string"}},"required":["meta","ga4"],"additionalProperties":false,"description":"Whether this form fires the brand's Meta and GA4 tracking, and its own Jornaya campaign key."},"pictures":{"type":"array","items":{"type":"object","properties":{"blockId":{"type":"string","description":"The id of the image choice question."},"value":{"type":"string","description":"The `value` of the option the picture is for."},"status":{"type":"string","enum":["making","added"],"description":"`making` while AI makes the picture. `added` once it's on the option as `imageId` in the draft."},"imageId":{"description":"The picture's id once it's added. Null while it's being made.","type":["string","null"]}},"required":["blockId","value","status","imageId"],"additionalProperties":false},"description":"Pictures AI is making in the background for image choice options after an AI build or fix. If a picture can't be made, or its option was removed or got a picture in the meantime, it drops off this list and the option stays as it is. You can add a picture yourself with `POST /v1/forms/{formId}/images`. Publishing while a picture is still `making` fails like a missing picture would, with a message that it's still coming."}},"required":["id","brandId","workspaceId","name","status","publishedVersion","publishedAt","hasUnpublishedChanges","changedAt","partialCapture","stepCount","blockCount","createdAt","updatedAt","draft","themeOverrides","trackingOverrides","pictures"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}},"/v1/forms/{formId}/versions":{"get":{"operationId":"forms.versions","summary":"List a form's versions","description":"List the form's published versions, newest first, up to 100, with who published each one.","tags":["forms"],"parameters":[{"name":"formId","in":"path","required":true,"description":"The form's id.","schema":{"type":"string","description":"The form's id."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"formId":{"type":"string","description":"The form's id."},"version":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The version's number."},"definition":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{},"description":"The form as it was published, in the same shape as a draft."},"theme":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{},"description":"The full theme the version went live with: the brand theme with the form's overrides on top."},"publishedAt":{"type":"number","description":"When it was published, in milliseconds since the epoch."},"publishedBy":{"anyOf":[{"anyOf":[{"type":"object","properties":{"kind":{"type":"string","const":"user"},"email":{"type":"string","description":"The team member's email."}},"required":["kind","email"],"additionalProperties":false},{"type":"object","properties":{"kind":{"type":"string","const":"apiKey"},"name":{"type":"string","description":"The API key's name."}},"required":["kind","name"],"additionalProperties":false}]},{"type":"null"}],"description":"Who published it. It's a team member's email or an API key's name, or null if unknown."}},"required":["formId","version","definition","theme","publishedAt","publishedBy"],"additionalProperties":false}}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/forms/{formId}/restore":{"post":{"operationId":"forms.restoreVersion","summary":"Restore a version","description":"Copy a published version into the draft. That's its definition, plus its theme overrides if the version recorded them. It doesn't publish. The version's checks come back as they were, whatever the plan. See [AI builds and fixes](/docs/api/form-busy).","tags":["forms"],"parameters":[{"name":"formId","in":"path","required":true,"description":"The form's id.","schema":{"type":"string","description":"The form's id."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The form's id."},"brandId":{"type":"string","description":"The brand the form belongs to."},"workspaceId":{"type":"string","description":"The workspace the form belongs to."},"name":{"type":"string","description":"The form's name."},"status":{"type":"string","enum":["draft","published","unpublished"],"description":"`draft` until it's first published, then `published` while it's live. `unpublished` once it's taken offline; its versions are kept."},"publishedVersion":{"description":"The number of the live version. Null if the form was never published.","type":["number","null"]},"publishedAt":{"description":"When the latest version was published, in milliseconds since the epoch. Null if never.","type":["number","null"]},"hasUnpublishedChanges":{"type":"boolean","description":"Whether the draft or theme overrides changed since the form was last published."},"changedAt":{"description":"When the draft or theme overrides last changed, in milliseconds since the epoch. Null if they were never edited.","type":["number","null"]},"partialCapture":{"type":"boolean","description":"Whether the draft has \"Save unfinished responses\" on. The webhook event for people who left (`submission.abandoned`) needs it, and the plan feature `partialCapture` too."},"stepCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many steps the draft has."},"blockCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many questions and other blocks the draft has, across all steps."},"createdAt":{"type":"number","description":"When the form was created, in milliseconds since the epoch."},"updatedAt":{"type":"number","description":"When anything about the form last changed, in milliseconds since the epoch."},"draft":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{},"description":"The whole form as it is now, unpublished edits included. It holds `steps` in order, then `endings`, and `settings`, in the same shape as `draft` in `PATCH /v1/forms/{formId}`."},"themeOverrides":{"type":"object","properties":{"accent":{"type":"string"},"accentText":{"type":"string"},"background":{"type":"string"},"surface":{"type":"string"},"text":{"type":"string"},"muted":{"type":"string"},"border":{"type":"string"},"radius":{"type":"number","minimum":0,"maximum":48},"buttonRadius":{"type":"number","minimum":0,"maximum":48},"font":{"type":"string"},"headingFont":{"type":"string","maxLength":500},"backdrop":{"type":"string"},"fontStylesheet":{"type":"string","maxLength":2000,"format":"uri"}},"additionalProperties":false,"description":"Theme values that override the brand theme for this form."},"trackingOverrides":{"type":"object","properties":{"meta":{"type":"boolean","description":"Whether this form fires the brand's Meta Lead, in the browser and through the Conversions API."},"ga4":{"type":"boolean","description":"Whether this form fires the brand's GA4 `generate_lead` event."},"jornayaCampaignKey":{"description":"This form's own Jornaya campaign key. Leave it out to use the brand's. It applies while the brand has Jornaya on. A page loads one campaign script, so with two forms on a page the first one's key is used.","type":"string"}},"required":["meta","ga4"],"additionalProperties":false,"description":"Whether this form fires the brand's Meta and GA4 tracking, and its own Jornaya campaign key."},"pictures":{"type":"array","items":{"type":"object","properties":{"blockId":{"type":"string","description":"The id of the image choice question."},"value":{"type":"string","description":"The `value` of the option the picture is for."},"status":{"type":"string","enum":["making","added"],"description":"`making` while AI makes the picture. `added` once it's on the option as `imageId` in the draft."},"imageId":{"description":"The picture's id once it's added. Null while it's being made.","type":["string","null"]}},"required":["blockId","value","status","imageId"],"additionalProperties":false},"description":"Pictures AI is making in the background for image choice options after an AI build or fix. If a picture can't be made, or its option was removed or got a picture in the meantime, it drops off this list and the option stays as it is. You can add a picture yourself with `POST /v1/forms/{formId}/images`. Publishing while a picture is still `making` fails like a missing picture would, with a message that it's still coming."}},"required":["id","brandId","workspaceId","name","status","publishedVersion","publishedAt","hasUnpublishedChanges","changedAt","partialCapture","stepCount","blockCount","createdAt","updatedAt","draft","themeOverrides","trackingOverrides","pictures"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"version":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991,"description":"The number of the version to restore. Restoring the live version throws away the unpublished changes, and `hasUnpublishedChanges` becomes false."}},"required":["version"]}}}}}},"/v1/forms/{formId}/embed":{"get":{"operationId":"forms.embedCode","summary":"Get embed code","description":"Get the snippets that put a form on a website, and the draft's display settings that say which one to use. Display settings go live when the form is published.","tags":["forms"],"parameters":[{"name":"formId","in":"path","required":true,"description":"The form's id.","schema":{"type":"string","description":"The form's id."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"script":{"type":"string","description":"The loader script. Add it once per page."},"button":{"type":"string","description":"A button that opens the form."},"inline":{"type":"string","description":"An inline field that starts the form."},"embed":{"type":"string","description":"The whole form in the page, in the element's place."},"widget":{"type":"string","description":"The corner widget."},"jsOpen":{"type":"string","description":"The JS API call that opens the form, with prefill."},"events":{"type":"string","description":"An event listener that runs when the form is submitted."},"view":{"type":"string","enum":["popup","widget","fullscreen"],"description":"How the form opens: as a popup, in the corner widget, or full screen. `embed` doesn't use it."},"side":{"type":"string","enum":["right","left"],"description":"The corner the widget docks to."},"onPage":{"type":"string","enum":["button","inline","embed","widget"],"description":"Which of the snippets above to use. It comes from the draft's `settings.display`."},"buttonLabel":{"type":"string","description":"The text on the button."}},"required":["script","button","inline","embed","widget","jsOpen","events","view","side","onPage","buttonLabel"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/forms/generate":{"post":{"operationId":"forms.generate","summary":"Build a form with AI","description":"Start building a form from a description. It creates a draft form named \"New form\" and a build, and returns their ids right away. The form fills in over the next moments. Follow it with `GET /v1/forms/{formId}/build` and stop it with `POST /v1/forms/{formId}/build/stop`. You can start 20 builds an hour, and during launch an account can start 20 a day (an API key counts as the workspace's owner; workspaces made free by staff have no daily cap). Past either it answers `429 rate_limited`, and the message says when you can build again. When the workspace already has all the forms its plan includes, it answers `403 plan_limit`. A guest without an account gets `429 guest_limit` after a few AI builds, or when the builds all guests share for the day are used up.","tags":["forms"],"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"formId":{"type":"string","description":"The new draft form's id."},"buildId":{"type":"string","description":"The build's id."}},"required":["formId","buildId"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"brandId":{"description":"The brand the new form belongs to.","type":"string"},"prompt":{"type":"string","minLength":1,"maxLength":2000,"description":"What the form is for, in plain words. If the workspace can't use phone and email verification or AI-assisted fields, the questions AI writes leave those checks off, and an AI autocomplete question becomes a short answer. That happens when entitlements `features.verification` or `features.aiFields` is false, because of the plan or because the workspace's stage doesn't see them yet. When seeing the options helps people answer, AI may ask with pictures, in at most 2 image choice questions. Their pictures are made in the background and show up in `pictures` of `GET /v1/forms/{formId}`."},"clientRequestId":{"type":"string","minLength":1,"maxLength":200,"description":"A unique string you make up for each attempt, so retries are safe. Sending the same one again returns the same form and build."}},"required":["brandId","prompt","clientRequestId"]}}}}}},"/v1/forms/{formId}/build":{"get":{"operationId":"forms.build","summary":"Get a form's AI build","description":"Get the form's latest AI build, or null if it never had one. While a build is planning or building, the draft is locked (see [AI builds and fixes](/docs/api/form-busy)). Pictures for its image choice questions are made in the background, even after the build is done. They show up in `pictures` of `GET /v1/forms/{formId}`.","tags":["forms"],"parameters":[{"name":"formId","in":"path","required":true,"description":"The form's id.","schema":{"type":"string","description":"The form's id."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"buildId":{"type":"string","description":"The build's id."},"status":{"type":"string","enum":["planning","building","done","failed","stopped"],"description":"`planning` while AI decides the questions, then `building` while it adds them to the draft. It ends as `done`, `failed` or `stopped`."},"plan":{"anyOf":[{"type":"object","properties":{"name":{"type":"string"},"steps":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"questions":{"type":"array","items":{"type":"string"}}},"required":["title","questions"],"additionalProperties":false}}},"required":["name","steps"],"additionalProperties":false},{"type":"null"}],"description":"The form AI planned, with its name and the questions of each step. Null until the plan is ready."},"added":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many of the `total` questions are in the draft so far."},"skipped":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many questions couldn't be built."},"skippedLabels":{"type":"array","items":{"type":"string"},"description":"The labels of the questions that couldn't be built."},"total":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many questions AI planned."},"error":{"description":"Why the build failed. Null unless it failed.","type":["string","null"]}},"required":["buildId","status","plan","added","skipped","skippedLabels","total","error"],"additionalProperties":false},{"type":"null"}]}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/forms/{formId}/build/stop":{"post":{"operationId":"forms.stopBuild","summary":"Stop an AI build","description":"Stop the form's AI build. The questions added so far stay in the draft. If there's no build or it already ended, nothing happens. Returns the latest build, or null.","tags":["forms"],"parameters":[{"name":"formId","in":"path","required":true,"description":"The form's id.","schema":{"type":"string","description":"The form's id."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"buildId":{"type":"string","description":"The build's id."},"status":{"type":"string","enum":["planning","building","done","failed","stopped"],"description":"`planning` while AI decides the questions, then `building` while it adds them to the draft. It ends as `done`, `failed` or `stopped`."},"plan":{"anyOf":[{"type":"object","properties":{"name":{"type":"string"},"steps":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"questions":{"type":"array","items":{"type":"string"}}},"required":["title","questions"],"additionalProperties":false}}},"required":["name","steps"],"additionalProperties":false},{"type":"null"}],"description":"The form AI planned, with its name and the questions of each step. Null until the plan is ready."},"added":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many of the `total` questions are in the draft so far."},"skipped":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many questions couldn't be built."},"skippedLabels":{"type":"array","items":{"type":"string"},"description":"The labels of the questions that couldn't be built."},"total":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many questions AI planned."},"error":{"description":"Why the build failed. Null unless it failed.","type":["string","null"]}},"required":["buildId","status","plan","added","skipped","skippedLabels","total","error"],"additionalProperties":false},{"type":"null"}]}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}},"/v1/forms/{formId}/fix":{"post":{"operationId":"forms.fixWithAi","summary":"Fix errors with AI","description":"Have AI fix the errors that stop the draft from publishing, without publishing it. It starts a pass and returns it right away with status `fixing`. If a pass is already running, you get that one. Follow it with `GET /v1/forms/{formId}/fix` and undo it with `POST /v1/forms/{formId}/fix/undo`. See [AI builds and fixes](/docs/api/form-busy) for what a fix may change. A draft with no errors answers `422 nothing_to_fix`, and a form that's being built answers `409 form_building`. You can start 30 fixes an hour. A guest gets `429 guest_limit` after a few.","tags":["forms"],"parameters":[{"name":"formId","in":"path","required":true,"description":"The form's id.","schema":{"type":"string","description":"The form's id."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"fixId":{"type":"string","description":"The fix's id."},"status":{"type":"string","enum":["fixing","done","failed"],"description":"`fixing` while AI works. `done` when it finished; `changes` and `errorsLeft` say what it did. `failed` when it couldn't; `error` says why and the draft is unchanged."},"errorsBefore":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many errors the draft had when the fix started."},"errorsLeft":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"How many errors are left after the fix. 0 means the form is ready to publish. Null until it's done."},"changes":{"type":"array","items":{"type":"string"},"description":"What the fix changed, in plain words."},"error":{"description":"Why the fix failed. Null unless it failed.","type":["string","null"]},"canUndo":{"type":"boolean","description":"Whether `POST /v1/forms/{formId}/fix/undo` can still put the draft back. That stops being possible once the draft or theme overrides change again, or the form is published."},"undone":{"type":"boolean","description":"Whether the fix was undone."},"startedAt":{"type":"number","description":"When the fix started, in milliseconds since the epoch."},"endedAt":{"description":"When the fix ended, in milliseconds since the epoch. Null while it's running.","type":["number","null"]}},"required":["fixId","status","errorsBefore","errorsLeft","changes","error","canUndo","undone","startedAt","endedAt"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}}},"get":{"operationId":"forms.aiFix","summary":"Get a form's AI fix","description":"Get the form's latest AI fix, or null if it never had one.","tags":["forms"],"parameters":[{"name":"formId","in":"path","required":true,"description":"The form's id.","schema":{"type":"string","description":"The form's id."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"fixId":{"type":"string","description":"The fix's id."},"status":{"type":"string","enum":["fixing","done","failed"],"description":"`fixing` while AI works. `done` when it finished; `changes` and `errorsLeft` say what it did. `failed` when it couldn't; `error` says why and the draft is unchanged."},"errorsBefore":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many errors the draft had when the fix started."},"errorsLeft":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"How many errors are left after the fix. 0 means the form is ready to publish. Null until it's done."},"changes":{"type":"array","items":{"type":"string"},"description":"What the fix changed, in plain words."},"error":{"description":"Why the fix failed. Null unless it failed.","type":["string","null"]},"canUndo":{"type":"boolean","description":"Whether `POST /v1/forms/{formId}/fix/undo` can still put the draft back. That stops being possible once the draft or theme overrides change again, or the form is published."},"undone":{"type":"boolean","description":"Whether the fix was undone."},"startedAt":{"type":"number","description":"When the fix started, in milliseconds since the epoch."},"endedAt":{"description":"When the fix ended, in milliseconds since the epoch. Null while it's running.","type":["number","null"]}},"required":["fixId","status","errorsBefore","errorsLeft","changes","error","canUndo","undone","startedAt","endedAt"],"additionalProperties":false},{"type":"null"}]}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/forms/{formId}/fix/undo":{"post":{"operationId":"forms.undoAiFix","summary":"Undo an AI fix","description":"Put the draft back exactly as it was before the latest AI fix. This works only while the draft is still the one the fix left, as `canUndo` shows. Any later change to the draft or theme overrides, or a publish, ends it, and undo answers `409 undo_unavailable` with the reason. `hasUnpublishedChanges` goes back to what it was, so undoing to exactly the live version leaves nothing to publish. Returns the fix with `undone` set to true.","tags":["forms"],"parameters":[{"name":"formId","in":"path","required":true,"description":"The form's id.","schema":{"type":"string","description":"The form's id."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"fixId":{"type":"string","description":"The fix's id."},"status":{"type":"string","enum":["fixing","done","failed"],"description":"`fixing` while AI works. `done` when it finished; `changes` and `errorsLeft` say what it did. `failed` when it couldn't; `error` says why and the draft is unchanged."},"errorsBefore":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many errors the draft had when the fix started."},"errorsLeft":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"How many errors are left after the fix. 0 means the form is ready to publish. Null until it's done."},"changes":{"type":"array","items":{"type":"string"},"description":"What the fix changed, in plain words."},"error":{"description":"Why the fix failed. Null unless it failed.","type":["string","null"]},"canUndo":{"type":"boolean","description":"Whether `POST /v1/forms/{formId}/fix/undo` can still put the draft back. That stops being possible once the draft or theme overrides change again, or the form is published."},"undone":{"type":"boolean","description":"Whether the fix was undone."},"startedAt":{"type":"number","description":"When the fix started, in milliseconds since the epoch."},"endedAt":{"description":"When the fix ended, in milliseconds since the epoch. Null while it's running.","type":["number","null"]}},"required":["fixId","status","errorsBefore","errorsLeft","changes","error","canUndo","undone","startedAt","endedAt"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}},"/v1/forms/{formId}/autocomplete":{"post":{"operationId":"forms.suggestAutocomplete","summary":"Try AI autocomplete","description":"See the suggestions a visitor would get after typing `query` into an AI autocomplete question, before you publish. It uses the prompt saved in the draft, so save the draft first. It's never charged, and it shares the cache visitors use. AI-assisted fields are in alpha, so a workspace outside the alpha gets `403 feature_unavailable`. A plan without them (entitlements `features.aiFields`) gets `403 plan_feature`. A draft with no AI autocomplete question of that id gets `400 not_autocomplete`. Past 300 calls an hour per person or API key, it answers `429 rate_limited`.","tags":["forms"],"parameters":[{"name":"formId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"suggestions":{"type":"array","items":{"type":"string"},"description":"Up to 8 suggestions, best first. The list is empty when the query is shorter than 2 characters, the prompt is empty, or the model has nothing or fails."}},"required":["suggestions"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"fieldId":{"type":"string","minLength":1,"maxLength":64,"description":"The id of the AI autocomplete question in the draft."},"query":{"type":"string","maxLength":1000,"description":"What the visitor typed."}},"required":["fieldId","query"]}}}}}},"/v1/forms/{formId}/images/upload-url":{"post":{"operationId":"images.uploadUrl","summary":"Get an image upload URL","description":"Get a one-time `uploadUrl` for a picture for this form's image choice questions. Upload the file there, then add it with `POST /v1/forms/{formId}/images` and the `storageId` you get back.","tags":["images"],"parameters":[{"name":"formId","in":"path","required":true,"description":"The form's id.","schema":{"type":"string","description":"The form's id."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"uploadUrl":{"type":"string","description":"POST the file to this address as the request body, with its Content-Type and no Authorization header. It answers with `{ storageId }`. The address takes one file and works for an hour, and the `storageId` works only in this workspace. A file that's never added as a picture is deleted after a day."}},"required":["uploadUrl"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}},"/v1/forms/{formId}/images":{"post":{"operationId":"images.create","summary":"Add an image","description":"Add a picture to this form for an image choice option, from an upload or a public URL. A file that isn't a usable image answers `400 invalid_image`, and the upload is deleted. A URL that can't be fetched answers `400 import_failed`. Pictures that neither the draft nor a published version uses are deleted. That happens an hour after their option is removed, or a day after an upload that was never used.","tags":["images"],"parameters":[{"name":"formId","in":"path","required":true,"description":"The form's id.","schema":{"type":"string","description":"The form's id."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The picture's id. Put it in an image choice option's `imageId`."},"formId":{"type":"string"},"url":{"type":"string","description":"Where visitors load the picture from. The address is public and never changes, and it's cached for a year. A new picture always gets a new id."},"contentType":{"type":"string","description":"PNG, JPEG, WebP, GIF or SVG, read from the file's bytes."},"size":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Size in bytes."},"width":{"type":"number","description":"Width in pixels."},"height":{"type":"number","description":"Height in pixels."},"sourceUrl":{"description":"The `url` the picture was fetched from. Null for an upload.","type":["string","null"]},"createdAt":{"type":"number","description":"When it was added, in milliseconds since the epoch."}},"required":["id","formId","url","contentType","size","width","height","sourceUrl","createdAt"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"storageId":{"description":"The `storageId` from an upload (see `POST /v1/forms/{formId}/images/upload-url`). Give this or `url`.","type":"string","minLength":1,"maxLength":64},"url":{"description":"A public image to fetch and keep. Private and local addresses are refused. Give this or `storageId`. Either way, the file must be PNG, JPEG, WebP, GIF or SVG, checked by its bytes, and at most 5 MB. SVGs are cleaned and can be at most 200 KB. Large photos aren't resized, so shrink them to about 1200 px wide first, as the app does.","type":"string","maxLength":2000,"format":"uri"}}}}}}},"get":{"operationId":"images.list","summary":"List images","description":"List the pictures added to this form, newest first, up to 200. Each has its public `url`.","tags":["images"],"parameters":[{"name":"formId","in":"path","required":true,"description":"The form's id.","schema":{"type":"string","description":"The form's id."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The picture's id. Put it in an image choice option's `imageId`."},"formId":{"type":"string"},"url":{"type":"string","description":"Where visitors load the picture from. The address is public and never changes, and it's cached for a year. A new picture always gets a new id."},"contentType":{"type":"string","description":"PNG, JPEG, WebP, GIF or SVG, read from the file's bytes."},"size":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Size in bytes."},"width":{"type":"number","description":"Width in pixels."},"height":{"type":"number","description":"Height in pixels."},"sourceUrl":{"description":"The `url` the picture was fetched from. Null for an upload.","type":["string","null"]},"createdAt":{"type":"number","description":"When it was added, in milliseconds since the epoch."}},"required":["id","formId","url","contentType","size","width","height","sourceUrl","createdAt"],"additionalProperties":false}}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/forms/{formId}/agent-handoff":{"post":{"operationId":"forms.agentHandoff","summary":"Brief an AI coding agent","description":"Write a one-time brief in markdown that an AI coding agent follows to embed, publish and check this form on its brand's website. By default it creates a new workspace API key for the brief. Revoke that key when the agent is done.","tags":["forms"],"parameters":[{"name":"formId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string","description":"The brief, in markdown."},"apiKeyId":{"description":"The API key created for the brief. Null when `includeSecrets` is false.","type":["string","null"]},"apiKeyName":{"description":"The name of that key. Null when `includeSecrets` is false.","type":["string","null"]}},"required":["prompt","apiKeyId","apiKeyName"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"notes":{"description":"Where and how to place the form, for the agent. It goes into the brief word for word.","type":"string","maxLength":5000},"includeSecrets":{"description":"Defaults to true. When true, a new API key is created and put in the brief, along with the signing secrets of the webhooks that get this form's responses. When false, no key is created and the brief has the placeholders YOUR_API_KEY and YOUR_WEBHOOK_SECRET instead.","type":"boolean"}}}}}}}},"/v1/api-keys":{"get":{"operationId":"apiKeys.list","summary":"List API keys","description":"List the workspace's API keys, newest first. A member limited to some brands sees only their own keys. See [Brand access](/docs/api/brand-access).","tags":["apiKeys"],"parameters":[{"name":"workspaceId","in":"query","required":false,"schema":{"description":"Optional. Your API key already belongs to one workspace.","type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The key's id. Use it to revoke the key."},"name":{"type":"string","description":"The name given when the key was made."},"prefix":{"type":"string","description":"The first 7 characters of the key, `fk_` and 4 more, to tell keys apart. The key itself is never returned again."},"createdAt":{"type":"number","description":"When the key was made, in milliseconds since the epoch."},"lastUsedAt":{"description":"When a request last used the key, in milliseconds since the epoch. Null if it was never used.","type":["number","null"]},"revokedAt":{"description":"When the key was revoked, in milliseconds since the epoch. Null while the key works.","type":["number","null"]},"createdBy":{"description":"The user id of the member who made the key. A key made with another key gets that key's maker. Null when unknown.","type":["string","null"]},"staff":{"type":"boolean","description":"True for a staff key, which may also call staff endpoints. False for every key you make."},"source":{"anyOf":[{"type":"string","enum":["handoff"]},{"type":"null"}],"description":"What made the key: `handoff` when an AI handoff made it for its brief. Null for a key made by hand."}},"required":["id","name","prefix","createdAt","lastUsedAt","revokedAt","createdBy","staff","source"],"additionalProperties":false},"description":"The keys, without the key itself."}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}},"post":{"operationId":"apiKeys.create","summary":"Create an API key","description":"Create an API key for the workspace. The key sees only that workspace. A key made by a member limited to some brands sees only those brands. See [Brand access](/docs/api/brand-access).","tags":["apiKeys"],"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The key's id. Use it to revoke the key."},"name":{"type":"string","description":"The name given when the key was made."},"prefix":{"type":"string","description":"The first 7 characters of the key, `fk_` and 4 more, to tell keys apart. The key itself is never returned again."},"createdAt":{"type":"number","description":"When the key was made, in milliseconds since the epoch."},"lastUsedAt":{"description":"When a request last used the key, in milliseconds since the epoch. Null if it was never used.","type":["number","null"]},"revokedAt":{"description":"When the key was revoked, in milliseconds since the epoch. Null while the key works.","type":["number","null"]},"createdBy":{"description":"The user id of the member who made the key. A key made with another key gets that key's maker. Null when unknown.","type":["string","null"]},"staff":{"type":"boolean","description":"True for a staff key, which may also call staff endpoints. False for every key you make."},"source":{"anyOf":[{"type":"string","enum":["handoff"]},{"type":"null"}],"description":"What made the key: `handoff` when an AI handoff made it for its brief. Null for a key made by hand."},"key":{"type":"string","description":"The key itself, starting with `fk_`. It is returned only in this response, so store it now."}},"required":["id","name","prefix","createdAt","lastUsedAt","revokedAt","createdBy","staff","source","key"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"workspaceId":{"description":"Optional. Your API key already belongs to one workspace.","type":"string"},"name":{"type":"string","minLength":1,"maxLength":200,"description":"A name to tell the key apart, such as \"Zapier\"."}},"required":["name"]}}}}}},"/v1/api-keys/{apiKeyId}/revoke":{"post":{"operationId":"apiKeys.revoke","summary":"Revoke an API key","description":"Revoke an API key. Requests that use it get `401` from now on. A member limited to some brands can revoke only their own keys, and gets `403 brand_access_limited` for any other.","tags":["apiKeys"],"parameters":[{"name":"apiKeyId","in":"path","required":true,"description":"The key's `id` from `GET /v1/api-keys`.","schema":{"type":"string","description":"The key's `id` from `GET /v1/api-keys`."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The key's id. Use it to revoke the key."},"name":{"type":"string","description":"The name given when the key was made."},"prefix":{"type":"string","description":"The first 7 characters of the key, `fk_` and 4 more, to tell keys apart. The key itself is never returned again."},"createdAt":{"type":"number","description":"When the key was made, in milliseconds since the epoch."},"lastUsedAt":{"description":"When a request last used the key, in milliseconds since the epoch. Null if it was never used.","type":["number","null"]},"revokedAt":{"description":"When the key was revoked, in milliseconds since the epoch. Null while the key works.","type":["number","null"]},"createdBy":{"description":"The user id of the member who made the key. A key made with another key gets that key's maker. Null when unknown.","type":["string","null"]},"staff":{"type":"boolean","description":"True for a staff key, which may also call staff endpoints. False for every key you make."},"source":{"anyOf":[{"type":"string","enum":["handoff"]},{"type":"null"}],"description":"What made the key: `handoff` when an AI handoff made it for its brief. Null for a key made by hand."}},"required":["id","name","prefix","createdAt","lastUsedAt","revokedAt","createdBy","staff","source"],"additionalProperties":false,"description":"The key, with `revokedAt` set."}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}},"/v1/api-keys/revoke-unused":{"post":{"operationId":"apiKeys.revokeUnused","summary":"Revoke unused API keys","description":"Revoke every key of one `source` that no request has used yet, such as the keys AI handoffs made for agents that never ran. A member limited to some brands revokes only their own keys.","tags":["apiKeys"],"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The key's id. Use it to revoke the key."},"name":{"type":"string","description":"The name given when the key was made."},"prefix":{"type":"string","description":"The first 7 characters of the key, `fk_` and 4 more, to tell keys apart. The key itself is never returned again."},"createdAt":{"type":"number","description":"When the key was made, in milliseconds since the epoch."},"lastUsedAt":{"description":"When a request last used the key, in milliseconds since the epoch. Null if it was never used.","type":["number","null"]},"revokedAt":{"description":"When the key was revoked, in milliseconds since the epoch. Null while the key works.","type":["number","null"]},"createdBy":{"description":"The user id of the member who made the key. A key made with another key gets that key's maker. Null when unknown.","type":["string","null"]},"staff":{"type":"boolean","description":"True for a staff key, which may also call staff endpoints. False for every key you make."},"source":{"anyOf":[{"type":"string","enum":["handoff"]},{"type":"null"}],"description":"What made the key: `handoff` when an AI handoff made it for its brief. Null for a key made by hand."}},"required":["id","name","prefix","createdAt","lastUsedAt","revokedAt","createdBy","staff","source"],"additionalProperties":false},"description":"The keys it revoked, with `revokedAt` set. Empty when there were none."}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"workspaceId":{"description":"Optional. Your API key already belongs to one workspace.","type":"string"},"source":{"type":"string","enum":["handoff"],"description":"Which keys: `handoff` for the keys AI handoffs made."}},"required":["source"]}}}}}},"/v1/submissions":{"get":{"operationId":"submissions.list","summary":"List responses","description":"List the workspace's responses, newest first, or only one form's or brand's. Filter them by state, ending, time and text. A key limited to some brands only gets those brands' responses. See [Pagination](/docs/api/pagination), [Response states](/docs/api/response-states), [Brand access](/docs/api/brand-access) and [Plan history and retention](/docs/api/retention).","tags":["submissions"],"parameters":[{"name":"workspaceId","in":"query","required":false,"schema":{"description":"Optional. Your API key already belongs to one workspace.","type":"string"}},{"name":"formId","in":"query","required":false,"schema":{"description":"Only this form's responses. Every sort except `createdAt` needs it, and so does `endingId`.","type":"string"}},{"name":"brandId","in":"query","required":false,"schema":{"description":"Only this brand's responses.","type":"string"}},{"name":"status","in":"query","required":false,"schema":{"description":"`complete` for responses where the person finished the form, `partial` for the rest. Use `state` to tell people still filling it in from people who left.","type":"string","enum":["partial","complete"]}},{"name":"state","in":"query","required":false,"schema":{"description":"`started` for people still filling in the form, `left` for people who left without finishing, `finished` for people who finished. See [Response states](/docs/api/response-states).","type":"string","enum":["started","left","finished"]}},{"name":"endingId","in":"query","required":false,"schema":{"description":"Only finished responses that reached this ending. Use an ending id of the form, and pass `formId` too.","type":"string","minLength":1,"maxLength":64}},{"name":"consent","in":"query","required":false,"schema":{"description":"Only responses with this `consent`. `agreed` for people who agreed to be called and texted, `not_agreed` for people the form asked who haven't agreed, `not_asked` for forms with no consent field. See [Consent](/docs/api/consent).","type":"string","enum":["agreed","not_agreed","not_asked"]}},{"name":"from","in":"query","required":false,"schema":{"description":"Only responses created at or after this time, in milliseconds since the epoch.","type":"number"}},{"name":"to","in":"query","required":false,"schema":{"description":"Only responses created before this time, in milliseconds since the epoch.","type":"number"}},{"name":"search","in":"query","required":false,"schema":{"description":"Words to find in the contact details or in any answer. It looks at the name, email and phone, the answers, the labels of picked options and file names. Every word must match, and case and accents don't matter. Digits also match phone numbers written in any format. With a search, each page checks `limit` responses and returns the ones that match. A page can be short or even empty while `nextCursor` is not null, so keep paging until it is null.","type":"string","maxLength":200}},{"name":"sort","in":"query","required":false,"schema":{"description":"How to order the responses. `createdAt` is the default and sorts by when they were created. `status` puts left responses first, then started, then finished, and `asc` reverses that. `ending` sorts by the ending's name, with unfinished responses at one end. `contact` sorts by the person's name, or their email or phone when there's no name. Ties go by creation time. Every sort except `createdAt` needs `formId`.","type":"string","enum":["createdAt","status","ending","contact"]}},{"name":"order","in":"query","required":false,"schema":{"description":"`desc` is the default. With the default sort, `desc` lists the newest responses first.","type":"string","enum":["desc","asc"]}},{"name":"cursor","in":"query","required":false,"schema":{"description":"The `nextCursor` from the previous page. Leave it out for the first page, and keep the same filters and sort on every page. See [Pagination](/docs/api/pagination).","type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"description":"How many responses a page holds, from 1 to 200. The default is 50. With a search, it's how many responses each page checks.","type":"integer","minimum":1,"maximum":200}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The response's id."},"formId":{"type":"string","description":"The form the person answered."},"brandId":{"type":"string","description":"The form's brand."},"version":{"type":"number","description":"The number of the published version the person answered."},"sessionId":{"type":"string","description":"The visitor's session on the page."},"status":{"type":"string","enum":["partial","complete"],"description":"`complete` once the person finishes the form, otherwise `partial`. For a partial response, `leftAt` tells you whether the person is still filling it in or left. See [Response states](/docs/api/response-states)."},"ending":{"anyOf":[{"type":"object","properties":{"id":{"type":"string","description":"The ending's id in the form. Pass it as `endingId` to filter `GET /v1/submissions`."},"name":{"type":"string","description":"The ending's name at the time the response reached it, such as \"Order placed\"."},"countsAsLead":{"type":"boolean","description":"Whether reaching this ending counts as a lead for ad tracking. Reaching an ending that doesn't count means the person was disqualified."}},"required":["id","name","countsAsLead"],"additionalProperties":false},{"type":"null"}],"description":"The ending the person reached. Null until they finish."},"answers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"fileId":{"type":"string","description":"The file's id."},"name":{"type":"string","description":"The file's name."},"size":{"type":"number","description":"The file's size in bytes."},"type":{"type":"string","description":"The file's MIME type, as uploaded."},"apiUrl":{"type":"string","description":"The API address of this file, `GET /v1/files/{fileId}`. Call it with your API key to get a download link. See [Files](/docs/api/files)."}},"required":["fileId","name","size","type","apiUrl"],"additionalProperties":false},"description":"The files uploaded to a `file_upload` question."},{}]},"description":"The answers by field id, as stored. Choices are their option values, not labels. A money answer is a plain number in its currency's major unit, and `currencies` names the currency. A `file_upload` answer is a list of files, each with `fileId`, `name`, `size`, `type` and `apiUrl`. Call the `apiUrl` with your API key to get a download link."},"currencies":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"},"description":"The currency of each money answer by field id, as an ISO 4217 code, such as `{ \"tip\": \"USD\" }`. It is the currency the form used when the person answered. The answer itself is a plain number in that currency's major unit."},"contact":{"type":"object","properties":{"email":{"type":"string"},"phone":{"type":"string"},"telegram":{"description":"The Telegram username, without the \"@\".","type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"address":{"type":"object","properties":{"line1":{"type":"string"},"line2":{"type":"string"},"city":{"type":"string"},"state":{"type":"string","description":"The region, such as a state, province or county."},"zip":{"type":"string","description":"The postal code."},"country":{"description":"The country as an ISO 3166-1 alpha-2 code. Missing on responses saved before addresses had a country.","type":"string"}},"required":["line1","city","state","zip"],"additionalProperties":false}},"additionalProperties":false,"description":"The contact details the form's contact questions collected."},"attribution":{"description":"Where the visitor came from. It holds the UTM parameters, landing page, referrer and ad click IDs."},"leadidToken":{"description":"The Jornaya LeadiD token, or null.","type":["string","null"]},"trustedFormCertUrl":{"description":"The TrustedForm certificate URL, which starts with https://cert.trustedform.com/. It is set when the brand has TrustedForm on, or the page runs it, and its script had loaded. Null otherwise.","type":["string","null"]},"trustedFormPingUrl":{"description":"The TrustedForm ping URL. It is set under the same conditions as `trustedFormCertUrl`, and null otherwise.","type":["string","null"]},"eventId":{"type":"string","description":"The Meta event id sent with the lead. Meta uses it to match the browser and server events for the same lead."},"phoneVerified":{"type":"boolean","description":"Whether the phone was verified with a code."},"emailCheck":{"anyOf":[{"type":"string","enum":["valid","invalid","disposable","catchall","unknown","skipped"]},{"type":"null"}],"description":"The result of checking email fields marked `validate`. `valid` means the address exists and `invalid` means it doesn't. `disposable` is a throwaway address. `catchall` means the domain accepts any address. `unknown` means its mail server didn't answer. `skipped` means it wasn't checked, because the workspace was out of credits or the check was unavailable. With several emails, you get the least sure result. A check never turns a lead away. Null when the response has no checked email or is unfinished."},"telegramCheck":{"anyOf":[{"type":"string","enum":["found","not_found","channel","group","bot","skipped"]},{"type":"null"}],"description":"The result of checking Telegram fields marked `check`. `found` is a personal account. `not_found` means Telegram didn't find it, which doesn't prove it doesn't exist. `channel`, `group` and `bot` say what the username belongs to. `skipped` means it wasn't checked, because the workspace was out of credits or Telegram didn't answer. With several usernames, you get the least sure result. Null when the response has no checked username or is unfinished."},"ip":{"description":"The visitor's IP address, or null.","type":["string","null"]},"userAgent":{"description":"The visitor's browser user agent, or null.","type":["string","null"]},"createdAt":{"type":"number","description":"Created, in milliseconds since the epoch."},"completedAt":{"description":"When the person finished, in milliseconds since the epoch. Null until then.","type":["number","null"]},"leftAt":{"description":"When the person left without finishing, in milliseconds since the epoch. A person has left when they close the page or do nothing for 5 minutes. Null while they're still filling it in, and once the response is complete. See [Response states](/docs/api/response-states).","type":["number","null"]},"earlierSubmissionId":{"description":"An earlier unfinished response on this form from the same email or phone, within the last 30 days. Null when there is none. It's only a link, and neither response is changed.","type":["string","null"]},"updatedAt":{"type":"number","description":"The last time the person's answers were saved, in milliseconds since the epoch."},"readableAnswers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"question":{"description":"The question as the person saw it. Null when that version of the form is gone or doesn't have the question.","type":["string","null"]},"type":{"description":"The question type, such as `single_choice`. Null when `question` is null.","type":["string","null"]},"answer":{"type":"string","description":"The answer in words. When `question` is null, it's the stored value as text."}},"required":["question","type","answer"],"additionalProperties":false},"description":"Each answer as the person saw it, by field id, from the version of the form they answered. For example `{ \"crust\": { \"question\": \"Which crust?\", \"type\": \"single_choice\", \"answer\": \"Thin crust\" } }`. Choices show their option labels rather than values, with several joined by \", \". Yes/no answers read \"Yes\" or \"No\". Names and addresses are one line, and files are their names."},"consent":{"type":"string","enum":["agreed","not_agreed","not_asked"],"description":"Whether the person agreed to be called and texted. `agreed` means they agreed to every consent they got to. `not_agreed` means the form asks for consent and they haven't given it: they left before submitting that step, changed the number since, or left a box unticked. `not_asked` means the form asks for none. It says nothing about the phone being verified: that's `phoneVerified`. See [Consent](/docs/api/consent)."},"consentAt":{"description":"When the person agreed, in milliseconds since the epoch. With several consent fields, it's the last one they agreed to. Null unless `consent` is `agreed`.","type":["number","null"]},"consentText":{"description":"The wording the person agreed to, exactly as shown. With several consent fields, each one in form order with a blank line between. Null unless `consent` is `agreed`.","type":["string","null"]},"consents":{"type":"array","items":{"type":"object","properties":{"fieldId":{"type":"string","description":"The field that asked: a phone question with consent, or a consent question."},"at":{"type":"number","description":"When the person agreed, in milliseconds since the epoch."},"text":{"type":"string","description":"The wording, exactly as shown."},"by":{"type":"string","enum":["checkbox","submit"],"description":"How they agreed. `checkbox` means they ticked the box. `submit` means they submitted its step with Continue, Submit or Enter."},"phone":{"description":"The number the consent covers, in E.164 format. For a phone question, the number they agreed for. For a consent question, the response's phone at that moment, or null.","type":["string","null"]}},"required":["fieldId","at","text","by","phone"],"additionalProperties":false},"description":"Each consent the person agreed to. A phone consent counts only for the number still answered, and a box they unticked again is left out."}},"required":["id","formId","brandId","version","sessionId","status","ending","answers","currencies","contact","attribution","leadidToken","trustedFormCertUrl","trustedFormPingUrl","eventId","phoneVerified","emailCheck","telegramCheck","ip","userAgent","createdAt","completedAt","leftAt","earlierSubmissionId","updatedAt","readableAnswers","consent","consentAt","consentText","consents"],"additionalProperties":false},"description":"The responses on this page, in the order you asked for."},"nextCursor":{"description":"Send it as `cursor` to get the next page, with the same filters and sort. It is null on the last page.","type":["string","null"]},"olderHidden":{"type":"boolean","description":"True on an empty first page when the form, brand or workspace has responses older than the plan shows. The plan's history is `retentionDays` in `GET /v1/workspaces/{workspaceId}/entitlements`. Older responses are kept, and they come back on a plan that shows them. See [Plan history and retention](/docs/api/retention)."}},"required":["items","nextCursor","olderHidden"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/submissions/endings":{"get":{"operationId":"submissions.endings","summary":"List reached endings","description":"The endings this form's responses reached, including endings deleted from the form since. Pass an ending's id as `endingId` to filter `GET /v1/submissions`.","tags":["submissions"],"parameters":[{"name":"formId","in":"query","required":true,"schema":{"description":"The form.","type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The ending's id in the form. Pass it as `endingId` to filter `GET /v1/submissions`."},"name":{"type":"string","description":"The ending's name at the time the response reached it, such as \"Order placed\"."},"countsAsLead":{"type":"boolean","description":"Whether reaching this ending counts as a lead for ad tracking. Reaching an ending that doesn't count means the person was disqualified."}},"required":["id","name","countsAsLead"],"additionalProperties":false},"description":"Up to 100 endings, one per id. Each shows the name and lead setting its latest response recorded. Endings reached only by responses older than the plan shows are left out."}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/submissions/export":{"post":{"operationId":"submissions.export","summary":"Export responses as rows","description":"A form's responses as spreadsheet rows, one page at a time, with the same filters and sort as `GET /v1/submissions`. To get the whole file at any size, use `POST /v1/exports`, which prepares it in the background. See [Export columns](/docs/api/exports) and [Pagination](/docs/api/pagination).","tags":["submissions"],"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"formName":{"description":"The form's name. Null once the form is deleted.","type":["string","null"]},"columns":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"The column's key, such as `answers.crust`. Each row uses these keys."},"header":{"type":"string","description":"The column's heading. For a question, it's the question's current label."}},"required":["key","header"],"additionalProperties":false},"description":"The columns, in order. Merge pages by column key, because a later page can add columns for answers to questions in older versions. See [Export columns](/docs/api/exports)."},"items":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"description":"One row per response, holding text by column key."},"nextCursor":{"description":"Send it as `cursor` to get the next page, with the same filters and sort. It is null on the last page.","type":["string","null"]},"linksExpireAt":{"type":"null","description":"Always null, because file links in exports are permanent links into the app that ask for sign-in. To download a file over the API, use `GET /v1/files/{fileId}`."}},"required":["formName","columns","items","nextCursor","linksExpireAt"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"formId":{"description":"The form whose responses to export.","type":"string"},"status":{"description":"`complete` for responses where the person finished the form, `partial` for the rest. Use `state` to tell people still filling it in from people who left.","type":"string","enum":["partial","complete"]},"state":{"description":"`started` for people still filling in the form, `left` for people who left without finishing, `finished` for people who finished. See [Response states](/docs/api/response-states).","type":"string","enum":["started","left","finished"]},"endingId":{"description":"Only finished responses that reached this ending. Use an ending id of the form, and pass `formId` too.","type":"string","minLength":1,"maxLength":64},"consent":{"description":"Only responses with this `consent`. `agreed` for people who agreed to be called and texted, `not_agreed` for people the form asked who haven't agreed, `not_asked` for forms with no consent field. See [Consent](/docs/api/consent).","type":"string","enum":["agreed","not_agreed","not_asked"]},"from":{"description":"Only responses created at or after this time, in milliseconds since the epoch.","type":"number"},"to":{"description":"Only responses created before this time, in milliseconds since the epoch.","type":"number"},"search":{"description":"Words to find in the contact details or in any answer. It looks at the name, email and phone, the answers, the labels of picked options and file names. Every word must match, and case and accents don't matter. Digits also match phone numbers written in any format. With a search, each page checks `limit` responses and returns the ones that match. A page can be short or even empty while `nextCursor` is not null, so keep paging until it is null.","type":"string","maxLength":200},"sort":{"description":"How to order the responses. `createdAt` is the default and sorts by when they were created. `status` puts left responses first, then started, then finished, and `asc` reverses that. `ending` sorts by the ending's name, with unfinished responses at one end. `contact` sorts by the person's name, or their email or phone when there's no name. Ties go by creation time. Every sort except `createdAt` needs `formId`.","type":"string","enum":["createdAt","status","ending","contact"]},"order":{"description":"`desc` is the default. With the default sort, `desc` lists the newest responses first.","type":"string","enum":["desc","asc"]},"cursor":{"description":"The `nextCursor` from the previous page. Leave it out for the first page, and keep the same filters and sort on every page. See [Pagination](/docs/api/pagination).","type":"string"},"limit":{"description":"How many rows a page holds, from 1 to 500. The default is 200. With a search, it's how many responses each page checks.","type":"integer","minimum":1,"maximum":500}},"required":["formId"]}}}}}},"/v1/submissions/export.csv":{"post":{"operationId":"submissions.exportCsv","summary":"Export a CSV file","description":"The whole export of a form's responses as one CSV file, with the same filters and sort as `GET /v1/submissions`. Each column is headed by its header text. The answer is the file itself, and `Content-Disposition` gives its name. A file bigger than 15 MB answers `413 too_large`. For bigger files use `POST /v1/exports`, which prepares a file of any size in the background. See [Export columns](/docs/api/exports).","tags":["submissions"],"parameters":[],"responses":{"200":{"description":"The file","content":{"text/csv":{"schema":{"type":"string"}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"formId":{"description":"The form whose responses to export.","type":"string"},"status":{"description":"`complete` for responses where the person finished the form, `partial` for the rest. Use `state` to tell people still filling it in from people who left.","type":"string","enum":["partial","complete"]},"state":{"description":"`started` for people still filling in the form, `left` for people who left without finishing, `finished` for people who finished. See [Response states](/docs/api/response-states).","type":"string","enum":["started","left","finished"]},"endingId":{"description":"Only finished responses that reached this ending. Use an ending id of the form, and pass `formId` too.","type":"string","minLength":1,"maxLength":64},"consent":{"description":"Only responses with this `consent`. `agreed` for people who agreed to be called and texted, `not_agreed` for people the form asked who haven't agreed, `not_asked` for forms with no consent field. See [Consent](/docs/api/consent).","type":"string","enum":["agreed","not_agreed","not_asked"]},"from":{"description":"Only responses created at or after this time, in milliseconds since the epoch.","type":"number"},"to":{"description":"Only responses created before this time, in milliseconds since the epoch.","type":"number"},"search":{"description":"Words to find in the contact details or in any answer. It looks at the name, email and phone, the answers, the labels of picked options and file names. Every word must match, and case and accents don't matter. Digits also match phone numbers written in any format. With a search, each page checks `limit` responses and returns the ones that match. A page can be short or even empty while `nextCursor` is not null, so keep paging until it is null.","type":"string","maxLength":200},"sort":{"description":"How to order the responses. `createdAt` is the default and sorts by when they were created. `status` puts left responses first, then started, then finished, and `asc` reverses that. `ending` sorts by the ending's name, with unfinished responses at one end. `contact` sorts by the person's name, or their email or phone when there's no name. Ties go by creation time. Every sort except `createdAt` needs `formId`.","type":"string","enum":["createdAt","status","ending","contact"]},"order":{"description":"`desc` is the default. With the default sort, `desc` lists the newest responses first.","type":"string","enum":["desc","asc"]}},"required":["formId"]}}}}}},"/v1/submissions/{submissionId}":{"get":{"operationId":"submissions.get","summary":"Get a response","description":"One response with its answers, contact details and attribution. A response older than the plan shows answers `404`. See [Plan history and retention](/docs/api/retention).","tags":["submissions"],"parameters":[{"name":"submissionId","in":"path","required":true,"description":"The response's id.","schema":{"type":"string","description":"The response's id."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The response's id."},"formId":{"type":"string","description":"The form the person answered."},"brandId":{"type":"string","description":"The form's brand."},"version":{"type":"number","description":"The number of the published version the person answered."},"sessionId":{"type":"string","description":"The visitor's session on the page."},"status":{"type":"string","enum":["partial","complete"],"description":"`complete` once the person finishes the form, otherwise `partial`. For a partial response, `leftAt` tells you whether the person is still filling it in or left. See [Response states](/docs/api/response-states)."},"ending":{"anyOf":[{"type":"object","properties":{"id":{"type":"string","description":"The ending's id in the form. Pass it as `endingId` to filter `GET /v1/submissions`."},"name":{"type":"string","description":"The ending's name at the time the response reached it, such as \"Order placed\"."},"countsAsLead":{"type":"boolean","description":"Whether reaching this ending counts as a lead for ad tracking. Reaching an ending that doesn't count means the person was disqualified."}},"required":["id","name","countsAsLead"],"additionalProperties":false},{"type":"null"}],"description":"The ending the person reached. Null until they finish."},"answers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"fileId":{"type":"string","description":"The file's id."},"name":{"type":"string","description":"The file's name."},"size":{"type":"number","description":"The file's size in bytes."},"type":{"type":"string","description":"The file's MIME type, as uploaded."},"apiUrl":{"type":"string","description":"The API address of this file, `GET /v1/files/{fileId}`. Call it with your API key to get a download link. See [Files](/docs/api/files)."}},"required":["fileId","name","size","type","apiUrl"],"additionalProperties":false},"description":"The files uploaded to a `file_upload` question."},{}]},"description":"The answers by field id, as stored. Choices are their option values, not labels. A money answer is a plain number in its currency's major unit, and `currencies` names the currency. A `file_upload` answer is a list of files, each with `fileId`, `name`, `size`, `type` and `apiUrl`. Call the `apiUrl` with your API key to get a download link."},"currencies":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"},"description":"The currency of each money answer by field id, as an ISO 4217 code, such as `{ \"tip\": \"USD\" }`. It is the currency the form used when the person answered. The answer itself is a plain number in that currency's major unit."},"contact":{"type":"object","properties":{"email":{"type":"string"},"phone":{"type":"string"},"telegram":{"description":"The Telegram username, without the \"@\".","type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"address":{"type":"object","properties":{"line1":{"type":"string"},"line2":{"type":"string"},"city":{"type":"string"},"state":{"type":"string","description":"The region, such as a state, province or county."},"zip":{"type":"string","description":"The postal code."},"country":{"description":"The country as an ISO 3166-1 alpha-2 code. Missing on responses saved before addresses had a country.","type":"string"}},"required":["line1","city","state","zip"],"additionalProperties":false}},"additionalProperties":false,"description":"The contact details the form's contact questions collected."},"attribution":{"description":"Where the visitor came from. It holds the UTM parameters, landing page, referrer and ad click IDs."},"leadidToken":{"description":"The Jornaya LeadiD token, or null.","type":["string","null"]},"trustedFormCertUrl":{"description":"The TrustedForm certificate URL, which starts with https://cert.trustedform.com/. It is set when the brand has TrustedForm on, or the page runs it, and its script had loaded. Null otherwise.","type":["string","null"]},"trustedFormPingUrl":{"description":"The TrustedForm ping URL. It is set under the same conditions as `trustedFormCertUrl`, and null otherwise.","type":["string","null"]},"eventId":{"type":"string","description":"The Meta event id sent with the lead. Meta uses it to match the browser and server events for the same lead."},"phoneVerified":{"type":"boolean","description":"Whether the phone was verified with a code."},"emailCheck":{"anyOf":[{"type":"string","enum":["valid","invalid","disposable","catchall","unknown","skipped"]},{"type":"null"}],"description":"The result of checking email fields marked `validate`. `valid` means the address exists and `invalid` means it doesn't. `disposable` is a throwaway address. `catchall` means the domain accepts any address. `unknown` means its mail server didn't answer. `skipped` means it wasn't checked, because the workspace was out of credits or the check was unavailable. With several emails, you get the least sure result. A check never turns a lead away. Null when the response has no checked email or is unfinished."},"telegramCheck":{"anyOf":[{"type":"string","enum":["found","not_found","channel","group","bot","skipped"]},{"type":"null"}],"description":"The result of checking Telegram fields marked `check`. `found` is a personal account. `not_found` means Telegram didn't find it, which doesn't prove it doesn't exist. `channel`, `group` and `bot` say what the username belongs to. `skipped` means it wasn't checked, because the workspace was out of credits or Telegram didn't answer. With several usernames, you get the least sure result. Null when the response has no checked username or is unfinished."},"ip":{"description":"The visitor's IP address, or null.","type":["string","null"]},"userAgent":{"description":"The visitor's browser user agent, or null.","type":["string","null"]},"createdAt":{"type":"number","description":"Created, in milliseconds since the epoch."},"completedAt":{"description":"When the person finished, in milliseconds since the epoch. Null until then.","type":["number","null"]},"leftAt":{"description":"When the person left without finishing, in milliseconds since the epoch. A person has left when they close the page or do nothing for 5 minutes. Null while they're still filling it in, and once the response is complete. See [Response states](/docs/api/response-states).","type":["number","null"]},"earlierSubmissionId":{"description":"An earlier unfinished response on this form from the same email or phone, within the last 30 days. Null when there is none. It's only a link, and neither response is changed.","type":["string","null"]},"updatedAt":{"type":"number","description":"The last time the person's answers were saved, in milliseconds since the epoch."},"readableAnswers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"question":{"description":"The question as the person saw it. Null when that version of the form is gone or doesn't have the question.","type":["string","null"]},"type":{"description":"The question type, such as `single_choice`. Null when `question` is null.","type":["string","null"]},"answer":{"type":"string","description":"The answer in words. When `question` is null, it's the stored value as text."}},"required":["question","type","answer"],"additionalProperties":false},"description":"Each answer as the person saw it, by field id, from the version of the form they answered. For example `{ \"crust\": { \"question\": \"Which crust?\", \"type\": \"single_choice\", \"answer\": \"Thin crust\" } }`. Choices show their option labels rather than values, with several joined by \", \". Yes/no answers read \"Yes\" or \"No\". Names and addresses are one line, and files are their names."},"consent":{"type":"string","enum":["agreed","not_agreed","not_asked"],"description":"Whether the person agreed to be called and texted. `agreed` means they agreed to every consent they got to. `not_agreed` means the form asks for consent and they haven't given it: they left before submitting that step, changed the number since, or left a box unticked. `not_asked` means the form asks for none. It says nothing about the phone being verified: that's `phoneVerified`. See [Consent](/docs/api/consent)."},"consentAt":{"description":"When the person agreed, in milliseconds since the epoch. With several consent fields, it's the last one they agreed to. Null unless `consent` is `agreed`.","type":["number","null"]},"consentText":{"description":"The wording the person agreed to, exactly as shown. With several consent fields, each one in form order with a blank line between. Null unless `consent` is `agreed`.","type":["string","null"]},"consents":{"type":"array","items":{"type":"object","properties":{"fieldId":{"type":"string","description":"The field that asked: a phone question with consent, or a consent question."},"at":{"type":"number","description":"When the person agreed, in milliseconds since the epoch."},"text":{"type":"string","description":"The wording, exactly as shown."},"by":{"type":"string","enum":["checkbox","submit"],"description":"How they agreed. `checkbox` means they ticked the box. `submit` means they submitted its step with Continue, Submit or Enter."},"phone":{"description":"The number the consent covers, in E.164 format. For a phone question, the number they agreed for. For a consent question, the response's phone at that moment, or null.","type":["string","null"]}},"required":["fieldId","at","text","by","phone"],"additionalProperties":false},"description":"Each consent the person agreed to. A phone consent counts only for the number still answered, and a box they unticked again is left out."}},"required":["id","formId","brandId","version","sessionId","status","ending","answers","currencies","contact","attribution","leadidToken","trustedFormCertUrl","trustedFormPingUrl","eventId","phoneVerified","emailCheck","telegramCheck","ip","userAgent","createdAt","completedAt","leftAt","earlierSubmissionId","updatedAt","readableAnswers","consent","consentAt","consentText","consents"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}},"delete":{"operationId":"submissions.delete","summary":"Delete a response","description":"Delete a response for good, with its uploaded files and its webhook deliveries. It also works on responses older than the plan shows, which are hidden but not gone, so you can always honour an erasure request.","tags":["submissions"],"parameters":[{"name":"submissionId","in":"path","required":true,"description":"The response's id.","schema":{"type":"string","description":"The response's id."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The deleted response's id."},"deleted":{"type":"boolean","const":true}},"required":["id","deleted"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/submissions/{submissionId}/deliveries":{"get":{"operationId":"submissions.deliveries","summary":"List response deliveries","description":"The webhook deliveries of one response, newest first, up to 50. Each comes with its webhook's URL. See [Webhook deliveries](/docs/api/webhook-deliveries).","tags":["submissions"],"parameters":[{"name":"submissionId","in":"path","required":true,"description":"The response's id.","schema":{"type":"string","description":"The response's id."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The delivery's id."},"webhookId":{"type":"string","description":"The webhook it went to."},"submissionId":{"description":"The response it carries. Null for test sends.","type":["string","null"]},"event":{"type":"string","description":"`submission.completed` or `submission.abandoned`, or `ping` for test sends."},"status":{"type":"string","enum":["pending","succeeded","failed"],"description":"`pending` while it's still being tried, then `succeeded`, or `failed` once it runs out of attempts."},"attempts":{"type":"number","description":"How many times it was sent so far."},"lastStatus":{"description":"The HTTP status the last attempt got, or null.","type":["number","null"]},"lastError":{"description":"What went wrong on the last attempt, or null.","type":["string","null"]},"nextAttemptAt":{"description":"When it will be tried next, in milliseconds since the epoch, or null.","type":["number","null"]},"createdAt":{"type":"number","description":"Created, in milliseconds since the epoch."},"payload":{"description":"The `data` field of the request body."},"webhookUrl":{"description":"The webhook's URL. Null once the webhook is deleted.","type":["string","null"]}},"required":["id","webhookId","submissionId","event","status","attempts","lastStatus","lastError","nextAttemptAt","createdAt","payload","webhookUrl"],"additionalProperties":false}}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/submissions/{submissionId}/file-urls":{"post":{"operationId":"submissions.fileUrls","summary":"Get response file links","description":"Download links for the files uploaded with a response to its `file_upload` questions. Each link works for 1 hour. See [Files](/docs/api/files).","tags":["submissions"],"parameters":[{"name":"submissionId","in":"path","required":true,"description":"The response's id.","schema":{"type":"string","description":"The response's id."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"fileId":{"type":"string","description":"The file's id."},"fieldId":{"type":"string","description":"The `file_upload` question it answers."},"name":{"type":"string","description":"The file's name."},"size":{"type":"number","description":"The file's size in bytes."},"type":{"type":"string","description":"The file's MIME type, as uploaded."},"url":{"type":"string","description":"A download link that works for 1 hour and needs no API key. It stops working when it expires or the response is deleted."},"expiresAt":{"type":"number","description":"When `url` stops working, in milliseconds since the epoch."}},"required":["fileId","fieldId","name","size","type","url","expiresAt"],"additionalProperties":false}}},"required":["items"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"fileId":{"description":"Only this file. Leave it out to get every file. A file the response doesn't have answers `404`.","type":"string"}}}}}}}},"/v1/files/{fileId}":{"get":{"operationId":"files.get","summary":"Get a file download link","description":"One file uploaded to a `file_upload` question, with a download link that works for 5 minutes. Call again for a fresh link. Once the response is deleted, it answers `404`. See [Files](/docs/api/files).","tags":["files"],"parameters":[{"name":"fileId","in":"path","required":true,"description":"The file's id. You find it in the answer, next to its `apiUrl`, and at the end of the file's link in an export, `<app>/files/<fileId>`.","schema":{"type":"string","description":"The file's id. You find it in the answer, next to its `apiUrl`, and at the end of the file's link in an export, `<app>/files/<fileId>`."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"fileId":{"type":"string","description":"The file's id."},"submissionId":{"type":"string","description":"The response it was uploaded with."},"fieldId":{"type":"string","description":"The `file_upload` question it answers."},"name":{"type":"string","description":"The file's name."},"size":{"type":"number","description":"The file's size in bytes."},"type":{"type":"string","description":"The file's MIME type, as uploaded."},"url":{"type":"string","description":"A download link that works for 5 minutes and needs no API key. It stops working when it expires or the response is deleted."},"expiresAt":{"type":"number","description":"When `url` stops working, in milliseconds since the epoch."}},"required":["fileId","submissionId","fieldId","name","size","type","url","expiresAt"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/exports":{"post":{"operationId":"exports.create","summary":"Start a CSV export","description":"Start exporting a form's responses as one CSV file of any size, prepared in the background, with the same filters and sort as `GET /v1/submissions`. Poll `GET /v1/exports/{exportId}` until `status` is `ready`, then call `POST /v1/exports/{exportId}/download` within a day. After a day the file is deleted. Asking again for the same export while it runs gives you that one back. You can start 20 an hour, and more answer `429`. See [Export columns](/docs/api/exports).","tags":["exports"],"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The export's id."},"formId":{"type":"string","description":"The form whose responses it exports."},"status":{"type":"string","enum":["running","ready","failed","canceled","expired"],"description":"`running` while the file is being prepared, with `rows` counting the responses written so far. `ready` once you can call `POST /v1/exports/{exportId}/download`, and the file is kept for a day. `expired` once the file was deleted, so export again. `failed` when it went wrong, and `error` says why. `canceled` when someone stopped it."},"rows":{"type":"number","description":"How many responses were written so far. Once ready, it's all of them."},"bytes":{"type":"number","description":"The file's size so far, in bytes."},"filename":{"type":"string","description":"The CSV file's name."},"filtered":{"type":"boolean","description":"Whether any filter was on. A filtered file has \"-filtered-\" in its name."},"filters":{"type":"object","properties":{"status":{"type":"string","enum":["partial","complete"]},"state":{"type":"string","enum":["started","left","finished"]},"endingId":{"type":"string"},"consent":{"type":"string","enum":["agreed","not_agreed","not_asked"]},"from":{"type":"number"},"to":{"type":"number"},"search":{"type":"string"},"sort":{"type":"string","enum":["createdAt","status","ending","contact"]},"order":{"type":"string","enum":["desc","asc"]}},"additionalProperties":false,"description":"The filters and sort it was asked for, as in `GET /v1/submissions`."},"error":{"description":"Why it failed. Null otherwise.","type":["string","null"]},"mine":{"type":"boolean","description":"Whether you started it. Over the API, that means with this key."},"createdAt":{"type":"number","description":"When it started, in milliseconds since the epoch."},"finishedAt":{"description":"When it stopped running, in milliseconds since the epoch. Null while it runs.","type":["number","null"]}},"required":["id","formId","status","rows","bytes","filename","filtered","filters","error","mine","createdAt","finishedAt"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"formId":{"description":"The form whose responses to export.","type":"string"},"status":{"description":"`complete` for responses where the person finished the form, `partial` for the rest. Use `state` to tell people still filling it in from people who left.","type":"string","enum":["partial","complete"]},"state":{"description":"`started` for people still filling in the form, `left` for people who left without finishing, `finished` for people who finished. See [Response states](/docs/api/response-states).","type":"string","enum":["started","left","finished"]},"endingId":{"description":"Only finished responses that reached this ending. Use an ending id of the form, and pass `formId` too.","type":"string","minLength":1,"maxLength":64},"consent":{"description":"Only responses with this `consent`. `agreed` for people who agreed to be called and texted, `not_agreed` for people the form asked who haven't agreed, `not_asked` for forms with no consent field. See [Consent](/docs/api/consent).","type":"string","enum":["agreed","not_agreed","not_asked"]},"from":{"description":"Only responses created at or after this time, in milliseconds since the epoch.","type":"number"},"to":{"description":"Only responses created before this time, in milliseconds since the epoch.","type":"number"},"search":{"description":"Words to find in the contact details or in any answer. It looks at the name, email and phone, the answers, the labels of picked options and file names. Every word must match, and case and accents don't matter. Digits also match phone numbers written in any format. With a search, each page checks `limit` responses and returns the ones that match. A page can be short or even empty while `nextCursor` is not null, so keep paging until it is null.","type":"string","maxLength":200},"sort":{"description":"How to order the responses. `createdAt` is the default and sorts by when they were created. `status` puts left responses first, then started, then finished, and `asc` reverses that. `ending` sorts by the ending's name, with unfinished responses at one end. `contact` sorts by the person's name, or their email or phone when there's no name. Ties go by creation time. Every sort except `createdAt` needs `formId`.","type":"string","enum":["createdAt","status","ending","contact"]},"order":{"description":"`desc` is the default. With the default sort, `desc` lists the newest responses first.","type":"string","enum":["desc","asc"]}},"required":["formId"]}}}}},"get":{"operationId":"exports.list","summary":"List a form's exports","description":"A form's exports, newest first, up to 20. Each looks like `GET /v1/exports/{exportId}`. A deleted form's exports stay readable until they expire.","tags":["exports"],"parameters":[{"name":"formId","in":"query","required":true,"schema":{"description":"The form.","type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The export's id."},"formId":{"type":"string","description":"The form whose responses it exports."},"status":{"type":"string","enum":["running","ready","failed","canceled","expired"],"description":"`running` while the file is being prepared, with `rows` counting the responses written so far. `ready` once you can call `POST /v1/exports/{exportId}/download`, and the file is kept for a day. `expired` once the file was deleted, so export again. `failed` when it went wrong, and `error` says why. `canceled` when someone stopped it."},"rows":{"type":"number","description":"How many responses were written so far. Once ready, it's all of them."},"bytes":{"type":"number","description":"The file's size so far, in bytes."},"filename":{"type":"string","description":"The CSV file's name."},"filtered":{"type":"boolean","description":"Whether any filter was on. A filtered file has \"-filtered-\" in its name."},"filters":{"type":"object","properties":{"status":{"type":"string","enum":["partial","complete"]},"state":{"type":"string","enum":["started","left","finished"]},"endingId":{"type":"string"},"consent":{"type":"string","enum":["agreed","not_agreed","not_asked"]},"from":{"type":"number"},"to":{"type":"number"},"search":{"type":"string"},"sort":{"type":"string","enum":["createdAt","status","ending","contact"]},"order":{"type":"string","enum":["desc","asc"]}},"additionalProperties":false,"description":"The filters and sort it was asked for, as in `GET /v1/submissions`."},"error":{"description":"Why it failed. Null otherwise.","type":["string","null"]},"mine":{"type":"boolean","description":"Whether you started it. Over the API, that means with this key."},"createdAt":{"type":"number","description":"When it started, in milliseconds since the epoch."},"finishedAt":{"description":"When it stopped running, in milliseconds since the epoch. Null while it runs.","type":["number","null"]}},"required":["id","formId","status","rows","bytes","filename","filtered","filters","error","mine","createdAt","finishedAt"],"additionalProperties":false},"description":"Newest first, up to 20. `mine` marks the ones you started."}},"required":["items"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/exports/{exportId}":{"get":{"operationId":"exports.get","summary":"Get an export","description":"One export and how far it got. Poll it after starting an export until `status` is `ready`.","tags":["exports"],"parameters":[{"name":"exportId","in":"path","required":true,"description":"The export's id.","schema":{"type":"string","description":"The export's id."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The export's id."},"formId":{"type":"string","description":"The form whose responses it exports."},"status":{"type":"string","enum":["running","ready","failed","canceled","expired"],"description":"`running` while the file is being prepared, with `rows` counting the responses written so far. `ready` once you can call `POST /v1/exports/{exportId}/download`, and the file is kept for a day. `expired` once the file was deleted, so export again. `failed` when it went wrong, and `error` says why. `canceled` when someone stopped it."},"rows":{"type":"number","description":"How many responses were written so far. Once ready, it's all of them."},"bytes":{"type":"number","description":"The file's size so far, in bytes."},"filename":{"type":"string","description":"The CSV file's name."},"filtered":{"type":"boolean","description":"Whether any filter was on. A filtered file has \"-filtered-\" in its name."},"filters":{"type":"object","properties":{"status":{"type":"string","enum":["partial","complete"]},"state":{"type":"string","enum":["started","left","finished"]},"endingId":{"type":"string"},"consent":{"type":"string","enum":["agreed","not_agreed","not_asked"]},"from":{"type":"number"},"to":{"type":"number"},"search":{"type":"string"},"sort":{"type":"string","enum":["createdAt","status","ending","contact"]},"order":{"type":"string","enum":["desc","asc"]}},"additionalProperties":false,"description":"The filters and sort it was asked for, as in `GET /v1/submissions`."},"error":{"description":"Why it failed. Null otherwise.","type":["string","null"]},"mine":{"type":"boolean","description":"Whether you started it. Over the API, that means with this key."},"createdAt":{"type":"number","description":"When it started, in milliseconds since the epoch."},"finishedAt":{"description":"When it stopped running, in milliseconds since the epoch. Null while it runs.","type":["number","null"]}},"required":["id","formId","status","rows","bytes","filename","filtered","filters","error","mine","createdAt","finishedAt"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/exports/{exportId}/cancel":{"post":{"operationId":"exports.cancel","summary":"Cancel an export","description":"Stop an export that is still being prepared, and delete what it wrote so far. An export that already finished stays as it is. Returns the export.","tags":["exports"],"parameters":[{"name":"exportId","in":"path","required":true,"description":"The export's id.","schema":{"type":"string","description":"The export's id."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The export's id."},"formId":{"type":"string","description":"The form whose responses it exports."},"status":{"type":"string","enum":["running","ready","failed","canceled","expired"],"description":"`running` while the file is being prepared, with `rows` counting the responses written so far. `ready` once you can call `POST /v1/exports/{exportId}/download`, and the file is kept for a day. `expired` once the file was deleted, so export again. `failed` when it went wrong, and `error` says why. `canceled` when someone stopped it."},"rows":{"type":"number","description":"How many responses were written so far. Once ready, it's all of them."},"bytes":{"type":"number","description":"The file's size so far, in bytes."},"filename":{"type":"string","description":"The CSV file's name."},"filtered":{"type":"boolean","description":"Whether any filter was on. A filtered file has \"-filtered-\" in its name."},"filters":{"type":"object","properties":{"status":{"type":"string","enum":["partial","complete"]},"state":{"type":"string","enum":["started","left","finished"]},"endingId":{"type":"string"},"consent":{"type":"string","enum":["agreed","not_agreed","not_asked"]},"from":{"type":"number"},"to":{"type":"number"},"search":{"type":"string"},"sort":{"type":"string","enum":["createdAt","status","ending","contact"]},"order":{"type":"string","enum":["desc","asc"]}},"additionalProperties":false,"description":"The filters and sort it was asked for, as in `GET /v1/submissions`."},"error":{"description":"Why it failed. Null otherwise.","type":["string","null"]},"mine":{"type":"boolean","description":"Whether you started it. Over the API, that means with this key."},"createdAt":{"type":"number","description":"When it started, in milliseconds since the epoch."},"finishedAt":{"description":"When it stopped running, in milliseconds since the epoch. Null while it runs.","type":["number","null"]}},"required":["id","formId","status","rows","bytes","filename","filtered","filters","error","mine","createdAt","finishedAt"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}},"/v1/exports/{exportId}/download":{"post":{"operationId":"exports.download","summary":"Get download links","description":"Download links for a ready export. They work for 10 minutes and need no API key, so call again for fresh ones. Before the export is ready it answers `409 not_ready`. A day after it was ready the file is deleted, and it answers `410 expired`, so export again. A file of more than 8000 parts, about 32 GB, answers `413 too_large`.","tags":["exports"],"parameters":[{"name":"exportId","in":"path","required":true,"description":"The export's id.","schema":{"type":"string","description":"The export's id."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"filename":{"type":"string","description":"The CSV file's name."},"rows":{"type":"number","description":"How many responses the file holds."},"bytes":{"type":"number","description":"The file's size in bytes."},"url":{"type":"string","description":"A link to the whole CSV file. It needs no API key."},"expiresAt":{"type":"number","description":"When the links stop working, 10 minutes from now, in milliseconds since the epoch."},"parts":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string"},"bytes":{"type":"number"}},"required":["url","bytes"],"additionalProperties":false},"description":"The same file in pieces of a few MB, in order. Joined together they make the file. Use them if you want smaller requests."}},"required":["filename","rows","bytes","url","expiresAt","parts"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}},"/v1/webhooks":{"get":{"operationId":"webhooks.list","summary":"List webhooks","description":"List a brand's webhooks, both brand-wide and per form. Secrets aren't included. Use `POST /v1/webhooks/{webhookId}/reveal-secret` to see one.","tags":["webhooks"],"parameters":[{"name":"brandId","in":"query","required":true,"schema":{"description":"The brand's `id`.","type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The webhook's id."},"brandId":{"type":"string","description":"The brand it belongs to."},"formId":{"description":"The one form it delivers for. Null means every form of the brand.","type":["string","null"]},"url":{"type":"string","description":"Where deliveries are POSTed."},"events":{"type":"array","items":{"type":"string","enum":["submission.abandoned","submission.completed"]},"description":"The events it receives."},"active":{"type":"boolean","description":"Whether it gets new deliveries. An inactive webhook gets none."},"createdAt":{"type":"number","description":"When it was made, in milliseconds since the epoch."},"postingSpec":{"anyOf":[{"type":"object","properties":{"format":{"type":"string","enum":["json","form"],"description":"How the body is encoded. `\"json\"` sends a flat JSON object of strings, and `\"form\"` sends it form-encoded."},"entries":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":200,"description":"The key in the body, exactly as the buyer names it."},"value":{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"field","description":"A question's answer. Lists are joined with commas and files are sent as their links. A question that's gone or wasn't answered sends an empty string."},"fieldId":{"type":"string","minLength":1,"maxLength":100,"description":"A question's id in the form's definition."},"part":{"description":"One part of the answer. For a name question, `first` or `last`. For an address question, `line1`, `line2`, `city`, `state`, `zip` or `country`. `state` is the region, such as a state, province or county. `zip` is the postal code. `country` is the ISO 3166-1 alpha-2 code. Leave it out to send the whole answer. A whole address is one line, written the way its country writes addresses, with the country's name.","type":"string","enum":["first","last","line1","line2","city","state","zip","country"]}},"required":["kind","fieldId"],"additionalProperties":false},{"type":"object","properties":{"kind":{"type":"string","const":"meta","description":"A contact or tracking value."},"source":{"type":"string","enum":["email","phone","fullName","firstName","lastName","ip","userAgent","pageUrl","submittedAt","responseId","formName","jornayaLeadId","trustedFormCertUrl","consent","consentAt","consentText","phoneVerified"],"description":"Which value to send. `trustedFormCertUrl` is the response's TrustedForm certificate URL. It is empty until responses carry one. `consent` is `yes` when the person agreed to be called and texted and `no` otherwise. `consentAt` is when they agreed, and `consentText` the wording they agreed to. Both are empty unless they agreed. `phoneVerified` is `yes` when the number was verified with a texted code, separate from consent."}},"required":["kind","source"],"additionalProperties":false},{"type":"object","properties":{"kind":{"type":"string","const":"constant","description":"The same text on every lead."},"value":{"type":"string","maxLength":2000}},"required":["kind","value"],"additionalProperties":false}],"description":"What to send under the key. Every value is sent as a string."},"format":{"description":"How to write the value. For phones, `phoneDigits` gives just the digits, without the +1 for US and Canada numbers and with the country code for others. `phoneE164` gives E.164, and `phoneUs` gives (415) 555-0123. For dates, `submittedAt` and `consentAt`, `dateIso` gives YYYY-MM-DD and `dateUs` gives MM/DD/YYYY. For yes/no values, such as `consent`, a yes/no question or a consent question, `boolTrueFalse` gives true or false, `boolOneZero` gives 1 or 0, and `boolYN` gives Y or N. Leave it out to send the value as stored. Constants ignore it.","type":"string","enum":["phoneDigits","phoneE164","phoneUs","dateIso","dateUs","boolTrueFalse","boolOneZero","boolYN"]}},"required":["key","value"],"additionalProperties":false},"description":"The body's keys, in order."},"headers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"}},"required":["name"],"additionalProperties":false},"description":"The extra headers' names. Values are never returned."},"warnings":{"type":"array","items":{"type":"object","properties":{"index":{"type":"number","description":"The entry's position in `entries`."},"key":{"type":"string","description":"The entry's key."},"message":{"type":"string","description":"What to check, in plain words."}},"required":["index","key","message"],"additionalProperties":false},"description":"Entries to check against the form's current questions, such as one whose question is gone. Such an entry sends an empty string."}},"required":["format","entries","headers","warnings"],"additionalProperties":false},{"type":"null"}],"description":"How it posts in a buyer's own format. Null means it sends the standard JSON payload."},"onlyConsented":{"type":"boolean","description":"Whether it sends only responses where the person agreed to be called and texted."}},"required":["id","brandId","formId","url","events","active","createdAt","postingSpec","onlyConsented"],"additionalProperties":false}}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}},"post":{"operationId":"webhooks.create","summary":"Create a webhook","description":"Send each new response of a brand, or of one form, to a URL. See [Webhook deliveries](/docs/api/webhook-deliveries) for the payload, signature and retries.","tags":["webhooks"],"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The webhook's id."},"brandId":{"type":"string","description":"The brand it belongs to."},"formId":{"description":"The one form it delivers for. Null means every form of the brand.","type":["string","null"]},"url":{"type":"string","description":"Where deliveries are POSTed."},"events":{"type":"array","items":{"type":"string","enum":["submission.abandoned","submission.completed"]},"description":"The events it receives."},"active":{"type":"boolean","description":"Whether it gets new deliveries. An inactive webhook gets none."},"createdAt":{"type":"number","description":"When it was made, in milliseconds since the epoch."},"postingSpec":{"anyOf":[{"type":"object","properties":{"format":{"type":"string","enum":["json","form"],"description":"How the body is encoded. `\"json\"` sends a flat JSON object of strings, and `\"form\"` sends it form-encoded."},"entries":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":200,"description":"The key in the body, exactly as the buyer names it."},"value":{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"field","description":"A question's answer. Lists are joined with commas and files are sent as their links. A question that's gone or wasn't answered sends an empty string."},"fieldId":{"type":"string","minLength":1,"maxLength":100,"description":"A question's id in the form's definition."},"part":{"description":"One part of the answer. For a name question, `first` or `last`. For an address question, `line1`, `line2`, `city`, `state`, `zip` or `country`. `state` is the region, such as a state, province or county. `zip` is the postal code. `country` is the ISO 3166-1 alpha-2 code. Leave it out to send the whole answer. A whole address is one line, written the way its country writes addresses, with the country's name.","type":"string","enum":["first","last","line1","line2","city","state","zip","country"]}},"required":["kind","fieldId"],"additionalProperties":false},{"type":"object","properties":{"kind":{"type":"string","const":"meta","description":"A contact or tracking value."},"source":{"type":"string","enum":["email","phone","fullName","firstName","lastName","ip","userAgent","pageUrl","submittedAt","responseId","formName","jornayaLeadId","trustedFormCertUrl","consent","consentAt","consentText","phoneVerified"],"description":"Which value to send. `trustedFormCertUrl` is the response's TrustedForm certificate URL. It is empty until responses carry one. `consent` is `yes` when the person agreed to be called and texted and `no` otherwise. `consentAt` is when they agreed, and `consentText` the wording they agreed to. Both are empty unless they agreed. `phoneVerified` is `yes` when the number was verified with a texted code, separate from consent."}},"required":["kind","source"],"additionalProperties":false},{"type":"object","properties":{"kind":{"type":"string","const":"constant","description":"The same text on every lead."},"value":{"type":"string","maxLength":2000}},"required":["kind","value"],"additionalProperties":false}],"description":"What to send under the key. Every value is sent as a string."},"format":{"description":"How to write the value. For phones, `phoneDigits` gives just the digits, without the +1 for US and Canada numbers and with the country code for others. `phoneE164` gives E.164, and `phoneUs` gives (415) 555-0123. For dates, `submittedAt` and `consentAt`, `dateIso` gives YYYY-MM-DD and `dateUs` gives MM/DD/YYYY. For yes/no values, such as `consent`, a yes/no question or a consent question, `boolTrueFalse` gives true or false, `boolOneZero` gives 1 or 0, and `boolYN` gives Y or N. Leave it out to send the value as stored. Constants ignore it.","type":"string","enum":["phoneDigits","phoneE164","phoneUs","dateIso","dateUs","boolTrueFalse","boolOneZero","boolYN"]}},"required":["key","value"],"additionalProperties":false},"description":"The body's keys, in order."},"headers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"}},"required":["name"],"additionalProperties":false},"description":"The extra headers' names. Values are never returned."},"warnings":{"type":"array","items":{"type":"object","properties":{"index":{"type":"number","description":"The entry's position in `entries`."},"key":{"type":"string","description":"The entry's key."},"message":{"type":"string","description":"What to check, in plain words."}},"required":["index","key","message"],"additionalProperties":false},"description":"Entries to check against the form's current questions, such as one whose question is gone. Such an entry sends an empty string."}},"required":["format","entries","headers","warnings"],"additionalProperties":false},{"type":"null"}],"description":"How it posts in a buyer's own format. Null means it sends the standard JSON payload."},"onlyConsented":{"type":"boolean","description":"Whether it sends only responses where the person agreed to be called and texted."},"secret":{"type":"string","description":"The signing secret, starting with `whsec_`. See [Webhook deliveries](/docs/api/webhook-deliveries#signature)."}},"required":["id","brandId","formId","url","events","active","createdAt","postingSpec","onlyConsented","secret"],"additionalProperties":false,"description":"The webhook with its signing secret. `POST /v1/webhooks/{webhookId}/reveal-secret` shows it again."}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"brandId":{"description":"The brand whose responses it receives.","type":"string"},"formId":{"description":"Send only this form's responses. Leave it out to send every form of the brand.","type":"string"},"url":{"type":"string","maxLength":2000,"description":"The URL deliveries are POSTed to. It must be https://. Plain http:// works only for localhost and 127.0.0.1."},"events":{"minItems":1,"maxItems":2,"type":"array","items":{"type":"string","enum":["submission.abandoned","submission.completed"]},"description":"The events to receive. `submission.completed` fires once per response, when the person reaches an ending. `submission.abandoned` fires once per response when the person gave a valid email, phone or Telegram username and then left without finishing. Leaving means they closed the page or did nothing for 5 minutes. It carries the latest answers, and needs the form's \"Save unfinished responses\" setting."},"active":{"type":"boolean","description":"Whether the webhook gets new deliveries. Defaults to true."},"onlyConsented":{"type":"boolean","description":"Send only responses where the person agreed to be called and texted, that is `consent` is `agreed`. Use it for a buyer who takes only consented leads, on either event. Defaults to false. See [Consent](/docs/api/consent)."}},"required":["brandId","url","events"]}}}}}},"/v1/webhooks/{webhookId}":{"patch":{"operationId":"webhooks.update","summary":"Update a webhook","description":"Change a webhook's URL, events, form, whether it's active or whether it sends only consented responses. Leave out what you don't change.","tags":["webhooks"],"parameters":[{"name":"webhookId","in":"path","required":true,"description":"The webhook's `id`.","schema":{"type":"string","description":"The webhook's `id`."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The webhook's id."},"brandId":{"type":"string","description":"The brand it belongs to."},"formId":{"description":"The one form it delivers for. Null means every form of the brand.","type":["string","null"]},"url":{"type":"string","description":"Where deliveries are POSTed."},"events":{"type":"array","items":{"type":"string","enum":["submission.abandoned","submission.completed"]},"description":"The events it receives."},"active":{"type":"boolean","description":"Whether it gets new deliveries. An inactive webhook gets none."},"createdAt":{"type":"number","description":"When it was made, in milliseconds since the epoch."},"postingSpec":{"anyOf":[{"type":"object","properties":{"format":{"type":"string","enum":["json","form"],"description":"How the body is encoded. `\"json\"` sends a flat JSON object of strings, and `\"form\"` sends it form-encoded."},"entries":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":200,"description":"The key in the body, exactly as the buyer names it."},"value":{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"field","description":"A question's answer. Lists are joined with commas and files are sent as their links. A question that's gone or wasn't answered sends an empty string."},"fieldId":{"type":"string","minLength":1,"maxLength":100,"description":"A question's id in the form's definition."},"part":{"description":"One part of the answer. For a name question, `first` or `last`. For an address question, `line1`, `line2`, `city`, `state`, `zip` or `country`. `state` is the region, such as a state, province or county. `zip` is the postal code. `country` is the ISO 3166-1 alpha-2 code. Leave it out to send the whole answer. A whole address is one line, written the way its country writes addresses, with the country's name.","type":"string","enum":["first","last","line1","line2","city","state","zip","country"]}},"required":["kind","fieldId"],"additionalProperties":false},{"type":"object","properties":{"kind":{"type":"string","const":"meta","description":"A contact or tracking value."},"source":{"type":"string","enum":["email","phone","fullName","firstName","lastName","ip","userAgent","pageUrl","submittedAt","responseId","formName","jornayaLeadId","trustedFormCertUrl","consent","consentAt","consentText","phoneVerified"],"description":"Which value to send. `trustedFormCertUrl` is the response's TrustedForm certificate URL. It is empty until responses carry one. `consent` is `yes` when the person agreed to be called and texted and `no` otherwise. `consentAt` is when they agreed, and `consentText` the wording they agreed to. Both are empty unless they agreed. `phoneVerified` is `yes` when the number was verified with a texted code, separate from consent."}},"required":["kind","source"],"additionalProperties":false},{"type":"object","properties":{"kind":{"type":"string","const":"constant","description":"The same text on every lead."},"value":{"type":"string","maxLength":2000}},"required":["kind","value"],"additionalProperties":false}],"description":"What to send under the key. Every value is sent as a string."},"format":{"description":"How to write the value. For phones, `phoneDigits` gives just the digits, without the +1 for US and Canada numbers and with the country code for others. `phoneE164` gives E.164, and `phoneUs` gives (415) 555-0123. For dates, `submittedAt` and `consentAt`, `dateIso` gives YYYY-MM-DD and `dateUs` gives MM/DD/YYYY. For yes/no values, such as `consent`, a yes/no question or a consent question, `boolTrueFalse` gives true or false, `boolOneZero` gives 1 or 0, and `boolYN` gives Y or N. Leave it out to send the value as stored. Constants ignore it.","type":"string","enum":["phoneDigits","phoneE164","phoneUs","dateIso","dateUs","boolTrueFalse","boolOneZero","boolYN"]}},"required":["key","value"],"additionalProperties":false},"description":"The body's keys, in order."},"headers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"}},"required":["name"],"additionalProperties":false},"description":"The extra headers' names. Values are never returned."},"warnings":{"type":"array","items":{"type":"object","properties":{"index":{"type":"number","description":"The entry's position in `entries`."},"key":{"type":"string","description":"The entry's key."},"message":{"type":"string","description":"What to check, in plain words."}},"required":["index","key","message"],"additionalProperties":false},"description":"Entries to check against the form's current questions, such as one whose question is gone. Such an entry sends an empty string."}},"required":["format","entries","headers","warnings"],"additionalProperties":false},{"type":"null"}],"description":"How it posts in a buyer's own format. Null means it sends the standard JSON payload."},"onlyConsented":{"type":"boolean","description":"Whether it sends only responses where the person agreed to be called and texted."}},"required":["id","brandId","formId","url","events","active","createdAt","postingSpec","onlyConsented"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"formId":{"description":"Send only this form's responses. `null` makes the webhook brand-wide, which fails with `400 posting_spec_needs_form` while it has a posting spec.","type":["string","null"]},"url":{"type":"string","maxLength":2000,"description":"The URL deliveries are POSTed to. It must be https://. Plain http:// works only for localhost and 127.0.0.1."},"events":{"minItems":1,"maxItems":2,"type":"array","items":{"type":"string","enum":["submission.abandoned","submission.completed"]},"description":"The events to receive. `submission.completed` fires once per response, when the person reaches an ending. `submission.abandoned` fires once per response when the person gave a valid email, phone or Telegram username and then left without finishing. Leaving means they closed the page or did nothing for 5 minutes. It carries the latest answers, and needs the form's \"Save unfinished responses\" setting."},"active":{"type":"boolean","description":"Whether the webhook gets new deliveries. Defaults to true."},"onlyConsented":{"type":"boolean","description":"Send only responses where the person agreed to be called and texted, that is `consent` is `agreed`. Use it for a buyer who takes only consented leads, on either event. Defaults to false. See [Consent](/docs/api/consent)."}}}}}}},"delete":{"operationId":"webhooks.delete","summary":"Delete a webhook","description":"Delete a webhook and its delivery log. Pending retries stop.","tags":["webhooks"],"parameters":[{"name":"webhookId","in":"path","required":true,"description":"The webhook's `id`.","schema":{"type":"string","description":"The webhook's `id`."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The deleted webhook's id."},"deleted":{"type":"boolean","const":true}},"required":["id","deleted"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/webhooks/{webhookId}/rotate-secret":{"post":{"operationId":"webhooks.rotateSecret","summary":"Rotate a webhook secret","description":"Replace the webhook's signing secret. The old one stops working immediately.","tags":["webhooks"],"parameters":[{"name":"webhookId","in":"path","required":true,"description":"The webhook's `id`.","schema":{"type":"string","description":"The webhook's `id`."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The webhook's id."},"brandId":{"type":"string","description":"The brand it belongs to."},"formId":{"description":"The one form it delivers for. Null means every form of the brand.","type":["string","null"]},"url":{"type":"string","description":"Where deliveries are POSTed."},"events":{"type":"array","items":{"type":"string","enum":["submission.abandoned","submission.completed"]},"description":"The events it receives."},"active":{"type":"boolean","description":"Whether it gets new deliveries. An inactive webhook gets none."},"createdAt":{"type":"number","description":"When it was made, in milliseconds since the epoch."},"postingSpec":{"anyOf":[{"type":"object","properties":{"format":{"type":"string","enum":["json","form"],"description":"How the body is encoded. `\"json\"` sends a flat JSON object of strings, and `\"form\"` sends it form-encoded."},"entries":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":200,"description":"The key in the body, exactly as the buyer names it."},"value":{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"field","description":"A question's answer. Lists are joined with commas and files are sent as their links. A question that's gone or wasn't answered sends an empty string."},"fieldId":{"type":"string","minLength":1,"maxLength":100,"description":"A question's id in the form's definition."},"part":{"description":"One part of the answer. For a name question, `first` or `last`. For an address question, `line1`, `line2`, `city`, `state`, `zip` or `country`. `state` is the region, such as a state, province or county. `zip` is the postal code. `country` is the ISO 3166-1 alpha-2 code. Leave it out to send the whole answer. A whole address is one line, written the way its country writes addresses, with the country's name.","type":"string","enum":["first","last","line1","line2","city","state","zip","country"]}},"required":["kind","fieldId"],"additionalProperties":false},{"type":"object","properties":{"kind":{"type":"string","const":"meta","description":"A contact or tracking value."},"source":{"type":"string","enum":["email","phone","fullName","firstName","lastName","ip","userAgent","pageUrl","submittedAt","responseId","formName","jornayaLeadId","trustedFormCertUrl","consent","consentAt","consentText","phoneVerified"],"description":"Which value to send. `trustedFormCertUrl` is the response's TrustedForm certificate URL. It is empty until responses carry one. `consent` is `yes` when the person agreed to be called and texted and `no` otherwise. `consentAt` is when they agreed, and `consentText` the wording they agreed to. Both are empty unless they agreed. `phoneVerified` is `yes` when the number was verified with a texted code, separate from consent."}},"required":["kind","source"],"additionalProperties":false},{"type":"object","properties":{"kind":{"type":"string","const":"constant","description":"The same text on every lead."},"value":{"type":"string","maxLength":2000}},"required":["kind","value"],"additionalProperties":false}],"description":"What to send under the key. Every value is sent as a string."},"format":{"description":"How to write the value. For phones, `phoneDigits` gives just the digits, without the +1 for US and Canada numbers and with the country code for others. `phoneE164` gives E.164, and `phoneUs` gives (415) 555-0123. For dates, `submittedAt` and `consentAt`, `dateIso` gives YYYY-MM-DD and `dateUs` gives MM/DD/YYYY. For yes/no values, such as `consent`, a yes/no question or a consent question, `boolTrueFalse` gives true or false, `boolOneZero` gives 1 or 0, and `boolYN` gives Y or N. Leave it out to send the value as stored. Constants ignore it.","type":"string","enum":["phoneDigits","phoneE164","phoneUs","dateIso","dateUs","boolTrueFalse","boolOneZero","boolYN"]}},"required":["key","value"],"additionalProperties":false},"description":"The body's keys, in order."},"headers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"}},"required":["name"],"additionalProperties":false},"description":"The extra headers' names. Values are never returned."},"warnings":{"type":"array","items":{"type":"object","properties":{"index":{"type":"number","description":"The entry's position in `entries`."},"key":{"type":"string","description":"The entry's key."},"message":{"type":"string","description":"What to check, in plain words."}},"required":["index","key","message"],"additionalProperties":false},"description":"Entries to check against the form's current questions, such as one whose question is gone. Such an entry sends an empty string."}},"required":["format","entries","headers","warnings"],"additionalProperties":false},{"type":"null"}],"description":"How it posts in a buyer's own format. Null means it sends the standard JSON payload."},"onlyConsented":{"type":"boolean","description":"Whether it sends only responses where the person agreed to be called and texted."},"secret":{"type":"string","description":"The signing secret, starting with `whsec_`. See [Webhook deliveries](/docs/api/webhook-deliveries#signature)."}},"required":["id","brandId","formId","url","events","active","createdAt","postingSpec","onlyConsented","secret"],"additionalProperties":false,"description":"The webhook with its new signing secret."}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}},"/v1/webhooks/{webhookId}/reveal-secret":{"post":{"operationId":"webhooks.revealSecret","summary":"Reveal a webhook secret","description":"Get the webhook with its current signing secret. Use it to set up signature checks on the receiving end.","tags":["webhooks"],"parameters":[{"name":"webhookId","in":"path","required":true,"description":"The webhook's `id`.","schema":{"type":"string","description":"The webhook's `id`."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The webhook's id."},"brandId":{"type":"string","description":"The brand it belongs to."},"formId":{"description":"The one form it delivers for. Null means every form of the brand.","type":["string","null"]},"url":{"type":"string","description":"Where deliveries are POSTed."},"events":{"type":"array","items":{"type":"string","enum":["submission.abandoned","submission.completed"]},"description":"The events it receives."},"active":{"type":"boolean","description":"Whether it gets new deliveries. An inactive webhook gets none."},"createdAt":{"type":"number","description":"When it was made, in milliseconds since the epoch."},"postingSpec":{"anyOf":[{"type":"object","properties":{"format":{"type":"string","enum":["json","form"],"description":"How the body is encoded. `\"json\"` sends a flat JSON object of strings, and `\"form\"` sends it form-encoded."},"entries":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":200,"description":"The key in the body, exactly as the buyer names it."},"value":{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"field","description":"A question's answer. Lists are joined with commas and files are sent as their links. A question that's gone or wasn't answered sends an empty string."},"fieldId":{"type":"string","minLength":1,"maxLength":100,"description":"A question's id in the form's definition."},"part":{"description":"One part of the answer. For a name question, `first` or `last`. For an address question, `line1`, `line2`, `city`, `state`, `zip` or `country`. `state` is the region, such as a state, province or county. `zip` is the postal code. `country` is the ISO 3166-1 alpha-2 code. Leave it out to send the whole answer. A whole address is one line, written the way its country writes addresses, with the country's name.","type":"string","enum":["first","last","line1","line2","city","state","zip","country"]}},"required":["kind","fieldId"],"additionalProperties":false},{"type":"object","properties":{"kind":{"type":"string","const":"meta","description":"A contact or tracking value."},"source":{"type":"string","enum":["email","phone","fullName","firstName","lastName","ip","userAgent","pageUrl","submittedAt","responseId","formName","jornayaLeadId","trustedFormCertUrl","consent","consentAt","consentText","phoneVerified"],"description":"Which value to send. `trustedFormCertUrl` is the response's TrustedForm certificate URL. It is empty until responses carry one. `consent` is `yes` when the person agreed to be called and texted and `no` otherwise. `consentAt` is when they agreed, and `consentText` the wording they agreed to. Both are empty unless they agreed. `phoneVerified` is `yes` when the number was verified with a texted code, separate from consent."}},"required":["kind","source"],"additionalProperties":false},{"type":"object","properties":{"kind":{"type":"string","const":"constant","description":"The same text on every lead."},"value":{"type":"string","maxLength":2000}},"required":["kind","value"],"additionalProperties":false}],"description":"What to send under the key. Every value is sent as a string."},"format":{"description":"How to write the value. For phones, `phoneDigits` gives just the digits, without the +1 for US and Canada numbers and with the country code for others. `phoneE164` gives E.164, and `phoneUs` gives (415) 555-0123. For dates, `submittedAt` and `consentAt`, `dateIso` gives YYYY-MM-DD and `dateUs` gives MM/DD/YYYY. For yes/no values, such as `consent`, a yes/no question or a consent question, `boolTrueFalse` gives true or false, `boolOneZero` gives 1 or 0, and `boolYN` gives Y or N. Leave it out to send the value as stored. Constants ignore it.","type":"string","enum":["phoneDigits","phoneE164","phoneUs","dateIso","dateUs","boolTrueFalse","boolOneZero","boolYN"]}},"required":["key","value"],"additionalProperties":false},"description":"The body's keys, in order."},"headers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"}},"required":["name"],"additionalProperties":false},"description":"The extra headers' names. Values are never returned."},"warnings":{"type":"array","items":{"type":"object","properties":{"index":{"type":"number","description":"The entry's position in `entries`."},"key":{"type":"string","description":"The entry's key."},"message":{"type":"string","description":"What to check, in plain words."}},"required":["index","key","message"],"additionalProperties":false},"description":"Entries to check against the form's current questions, such as one whose question is gone. Such an entry sends an empty string."}},"required":["format","entries","headers","warnings"],"additionalProperties":false},{"type":"null"}],"description":"How it posts in a buyer's own format. Null means it sends the standard JSON payload."},"onlyConsented":{"type":"boolean","description":"Whether it sends only responses where the person agreed to be called and texted."},"secret":{"type":"string","description":"The signing secret, starting with `whsec_`. See [Webhook deliveries](/docs/api/webhook-deliveries#signature)."}},"required":["id","brandId","formId","url","events","active","createdAt","postingSpec","onlyConsented","secret"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}},"/v1/webhooks/{webhookId}/deliveries":{"get":{"operationId":"webhooks.deliveries","summary":"List webhook deliveries","description":"List a webhook's deliveries, newest first, one page at a time. Deliveries of responses older than the plan's history are left out, because their payloads are responses. See [Plan history and retention](/docs/api/retention).","tags":["webhooks"],"parameters":[{"name":"webhookId","in":"path","required":true,"description":"The webhook's `id`.","schema":{"type":"string","description":"The webhook's `id`."}},{"name":"cursor","in":"query","required":false,"schema":{"description":"The `nextCursor` from the previous page. Leave it out for the first page. See [Pagination](/docs/api/pagination).","type":["string","null"]}},{"name":"limit","in":"query","required":false,"schema":{"description":"How many deliveries to read per page, counted before hidden ones are left out. Defaults to 20.","type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The delivery's id."},"webhookId":{"type":"string","description":"The webhook it was sent to."},"submissionId":{"description":"The response it carries. Null for a test delivery.","type":["string","null"]},"event":{"type":"string","description":"The event. It is `\"submission.completed\"` or `\"submission.abandoned\"`, or `\"ping\"` for a test."},"status":{"type":"string","enum":["pending","succeeded","failed"],"description":"`pending` while it is being sent or retried, `succeeded` once the endpoint accepted it, and `failed` after the last attempt failed."},"attempts":{"type":"number","description":"Attempts made so far."},"lastStatus":{"description":"The HTTP status of the last attempt. Null before the first attempt, or when the endpoint didn't answer.","type":["number","null"]},"lastError":{"description":"What went wrong on the last attempt. Null when nothing did.","type":["string","null"]},"nextAttemptAt":{"description":"When the next retry is due, in milliseconds since the epoch. Null when none is due.","type":["number","null"]},"createdAt":{"type":"number","description":"When the delivery was made, in milliseconds since the epoch."},"payload":{"description":"The `data` field of the request body."}},"required":["id","webhookId","submissionId","event","status","attempts","lastStatus","lastError","nextAttemptAt","createdAt","payload"],"additionalProperties":false},"description":"This page's deliveries. A page can be short while `nextCursor` is not null."},"nextCursor":{"description":"Send it as `cursor` to get the next page. Null on the last page.","type":["string","null"]}},"required":["items","nextCursor"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/webhook-deliveries/{deliveryId}/redeliver":{"post":{"operationId":"webhooks.redeliver","summary":"Redeliver a delivery","description":"Send a delivery again. It goes out as a new delivery with a new id, the same event and data, and its own retries.","tags":["webhooks"],"parameters":[{"name":"deliveryId","in":"path","required":true,"description":"The delivery's `id`, from the delivery log.","schema":{"type":"string","description":"The delivery's `id`, from the delivery log."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The delivery's id."},"webhookId":{"type":"string","description":"The webhook it was sent to."},"submissionId":{"description":"The response it carries. Null for a test delivery.","type":["string","null"]},"event":{"type":"string","description":"The event. It is `\"submission.completed\"` or `\"submission.abandoned\"`, or `\"ping\"` for a test."},"status":{"type":"string","enum":["pending","succeeded","failed"],"description":"`pending` while it is being sent or retried, `succeeded` once the endpoint accepted it, and `failed` after the last attempt failed."},"attempts":{"type":"number","description":"Attempts made so far."},"lastStatus":{"description":"The HTTP status of the last attempt. Null before the first attempt, or when the endpoint didn't answer.","type":["number","null"]},"lastError":{"description":"What went wrong on the last attempt. Null when nothing did.","type":["string","null"]},"nextAttemptAt":{"description":"When the next retry is due, in milliseconds since the epoch. Null when none is due.","type":["number","null"]},"createdAt":{"type":"number","description":"When the delivery was made, in milliseconds since the epoch."},"payload":{"description":"The `data` field of the request body."}},"required":["id","webhookId","submissionId","event","status","attempts","lastStatus","lastError","nextAttemptAt","createdAt","payload"],"additionalProperties":false,"description":"The new delivery."}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}},"/v1/webhooks/{webhookId}/test":{"post":{"operationId":"webhooks.sendTest","summary":"Send a test delivery","description":"Send a signed test delivery with the event `\"ping\"`, even if the webhook is inactive. A webhook with a posting spec gets a made-up lead in the buyer's format instead. Check the result in the delivery log.","tags":["webhooks"],"parameters":[{"name":"webhookId","in":"path","required":true,"description":"The webhook's `id`.","schema":{"type":"string","description":"The webhook's `id`."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The delivery's id."},"webhookId":{"type":"string","description":"The webhook it was sent to."},"submissionId":{"description":"The response it carries. Null for a test delivery.","type":["string","null"]},"event":{"type":"string","description":"The event. It is `\"submission.completed\"` or `\"submission.abandoned\"`, or `\"ping\"` for a test."},"status":{"type":"string","enum":["pending","succeeded","failed"],"description":"`pending` while it is being sent or retried, `succeeded` once the endpoint accepted it, and `failed` after the last attempt failed."},"attempts":{"type":"number","description":"Attempts made so far."},"lastStatus":{"description":"The HTTP status of the last attempt. Null before the first attempt, or when the endpoint didn't answer.","type":["number","null"]},"lastError":{"description":"What went wrong on the last attempt. Null when nothing did.","type":["string","null"]},"nextAttemptAt":{"description":"When the next retry is due, in milliseconds since the epoch. Null when none is due.","type":["number","null"]},"createdAt":{"type":"number","description":"When the delivery was made, in milliseconds since the epoch."},"payload":{"description":"The `data` field of the request body."},"posted":{"anyOf":[{"type":"object","properties":{"contentType":{"type":"string"},"body":{"type":"string"}},"required":["contentType","body"],"additionalProperties":false},{"type":"null"}],"description":"The body the test posted, a sample lead in the buyer's format. Null when the webhook has no posting spec."}},"required":["id","webhookId","submissionId","event","status","attempts","lastStatus","lastError","nextAttemptAt","createdAt","payload","posted"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}},"/v1/webhooks/{webhookId}/posting-spec":{"post":{"operationId":"webhooks.setPostingSpec","summary":"Set a posting spec","description":"Make a form webhook post each lead in a buyer's own format, replacing any spec it had. Only a webhook for one form can have a spec. Others get `400 posting_spec_needs_form`. See [Posting in a buyer's format](/docs/api/webhook-deliveries#posting-specs).","tags":["webhooks"],"parameters":[{"name":"webhookId","in":"path","required":true,"description":"The webhook's `id`.","schema":{"type":"string","description":"The webhook's `id`."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The webhook's id."},"brandId":{"type":"string","description":"The brand it belongs to."},"formId":{"description":"The one form it delivers for. Null means every form of the brand.","type":["string","null"]},"url":{"type":"string","description":"Where deliveries are POSTed."},"events":{"type":"array","items":{"type":"string","enum":["submission.abandoned","submission.completed"]},"description":"The events it receives."},"active":{"type":"boolean","description":"Whether it gets new deliveries. An inactive webhook gets none."},"createdAt":{"type":"number","description":"When it was made, in milliseconds since the epoch."},"postingSpec":{"anyOf":[{"type":"object","properties":{"format":{"type":"string","enum":["json","form"],"description":"How the body is encoded. `\"json\"` sends a flat JSON object of strings, and `\"form\"` sends it form-encoded."},"entries":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":200,"description":"The key in the body, exactly as the buyer names it."},"value":{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"field","description":"A question's answer. Lists are joined with commas and files are sent as their links. A question that's gone or wasn't answered sends an empty string."},"fieldId":{"type":"string","minLength":1,"maxLength":100,"description":"A question's id in the form's definition."},"part":{"description":"One part of the answer. For a name question, `first` or `last`. For an address question, `line1`, `line2`, `city`, `state`, `zip` or `country`. `state` is the region, such as a state, province or county. `zip` is the postal code. `country` is the ISO 3166-1 alpha-2 code. Leave it out to send the whole answer. A whole address is one line, written the way its country writes addresses, with the country's name.","type":"string","enum":["first","last","line1","line2","city","state","zip","country"]}},"required":["kind","fieldId"],"additionalProperties":false},{"type":"object","properties":{"kind":{"type":"string","const":"meta","description":"A contact or tracking value."},"source":{"type":"string","enum":["email","phone","fullName","firstName","lastName","ip","userAgent","pageUrl","submittedAt","responseId","formName","jornayaLeadId","trustedFormCertUrl","consent","consentAt","consentText","phoneVerified"],"description":"Which value to send. `trustedFormCertUrl` is the response's TrustedForm certificate URL. It is empty until responses carry one. `consent` is `yes` when the person agreed to be called and texted and `no` otherwise. `consentAt` is when they agreed, and `consentText` the wording they agreed to. Both are empty unless they agreed. `phoneVerified` is `yes` when the number was verified with a texted code, separate from consent."}},"required":["kind","source"],"additionalProperties":false},{"type":"object","properties":{"kind":{"type":"string","const":"constant","description":"The same text on every lead."},"value":{"type":"string","maxLength":2000}},"required":["kind","value"],"additionalProperties":false}],"description":"What to send under the key. Every value is sent as a string."},"format":{"description":"How to write the value. For phones, `phoneDigits` gives just the digits, without the +1 for US and Canada numbers and with the country code for others. `phoneE164` gives E.164, and `phoneUs` gives (415) 555-0123. For dates, `submittedAt` and `consentAt`, `dateIso` gives YYYY-MM-DD and `dateUs` gives MM/DD/YYYY. For yes/no values, such as `consent`, a yes/no question or a consent question, `boolTrueFalse` gives true or false, `boolOneZero` gives 1 or 0, and `boolYN` gives Y or N. Leave it out to send the value as stored. Constants ignore it.","type":"string","enum":["phoneDigits","phoneE164","phoneUs","dateIso","dateUs","boolTrueFalse","boolOneZero","boolYN"]}},"required":["key","value"],"additionalProperties":false},"description":"The body's keys, in order."},"headers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"}},"required":["name"],"additionalProperties":false},"description":"The extra headers' names. Values are never returned."},"warnings":{"type":"array","items":{"type":"object","properties":{"index":{"type":"number","description":"The entry's position in `entries`."},"key":{"type":"string","description":"The entry's key."},"message":{"type":"string","description":"What to check, in plain words."}},"required":["index","key","message"],"additionalProperties":false},"description":"Entries to check against the form's current questions, such as one whose question is gone. Such an entry sends an empty string."}},"required":["format","entries","headers","warnings"],"additionalProperties":false},{"type":"null"}],"description":"How it posts in a buyer's own format. Null means it sends the standard JSON payload."},"onlyConsented":{"type":"boolean","description":"Whether it sends only responses where the person agreed to be called and texted."}},"required":["id","brandId","formId","url","events","active","createdAt","postingSpec","onlyConsented"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"spec":{"type":"object","properties":{"format":{"type":"string","enum":["json","form"],"description":"How the body is encoded. `\"json\"` sends application/json and `\"form\"` sends application/x-www-form-urlencoded."},"entries":{"minItems":1,"maxItems":200,"type":"array","items":{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":200,"description":"The key in the body, exactly as the buyer names it."},"value":{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"field","description":"A question's answer. Lists are joined with commas and files are sent as their links. A question that's gone or wasn't answered sends an empty string."},"fieldId":{"type":"string","minLength":1,"maxLength":100,"description":"A question's id in the form's definition."},"part":{"description":"One part of the answer. For a name question, `first` or `last`. For an address question, `line1`, `line2`, `city`, `state`, `zip` or `country`. `state` is the region, such as a state, province or county. `zip` is the postal code. `country` is the ISO 3166-1 alpha-2 code. Leave it out to send the whole answer. A whole address is one line, written the way its country writes addresses, with the country's name.","type":"string","enum":["first","last","line1","line2","city","state","zip","country"]}},"required":["kind","fieldId"]},{"type":"object","properties":{"kind":{"type":"string","const":"meta","description":"A contact or tracking value."},"source":{"type":"string","enum":["email","phone","fullName","firstName","lastName","ip","userAgent","pageUrl","submittedAt","responseId","formName","jornayaLeadId","trustedFormCertUrl","consent","consentAt","consentText","phoneVerified"],"description":"Which value to send. `trustedFormCertUrl` is the response's TrustedForm certificate URL. It is empty until responses carry one. `consent` is `yes` when the person agreed to be called and texted and `no` otherwise. `consentAt` is when they agreed, and `consentText` the wording they agreed to. Both are empty unless they agreed. `phoneVerified` is `yes` when the number was verified with a texted code, separate from consent."}},"required":["kind","source"]},{"type":"object","properties":{"kind":{"type":"string","const":"constant","description":"The same text on every lead."},"value":{"type":"string","maxLength":2000}},"required":["kind","value"]}],"description":"What to send under the key. Every value is sent as a string."},"format":{"description":"How to write the value. For phones, `phoneDigits` gives just the digits, without the +1 for US and Canada numbers and with the country code for others. `phoneE164` gives E.164, and `phoneUs` gives (415) 555-0123. For dates, `submittedAt` and `consentAt`, `dateIso` gives YYYY-MM-DD and `dateUs` gives MM/DD/YYYY. For yes/no values, such as `consent`, a yes/no question or a consent question, `boolTrueFalse` gives true or false, `boolOneZero` gives 1 or 0, and `boolYN` gives Y or N. Leave it out to send the value as stored. Constants ignore it.","type":"string","enum":["phoneDigits","phoneE164","phoneUs","dateIso","dateUs","boolTrueFalse","boolOneZero","boolYN"]}},"required":["key","value"]},"description":"The body's keys, in order. The body holds exactly these keys. As JSON, it is a flat object of strings."},"headers":{"description":"Extra headers to send, such as the buyer's API key. They go alongside Forms-Event, Forms-Delivery and Forms-Signature. Header values are never returned.","maxItems":20,"type":"array","items":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[!#$%&'*+.^_`|~0-9A-Za-z-]+$"},"value":{"description":"The header's value. Leave it out to keep the saved value. Header values are never returned.","type":"string","maxLength":2000,"pattern":"^[^\\r\\n\\0]*$"}},"required":["name"]}}},"required":["format","entries"],"description":"The format, the body's keys in order, and extra headers."}},"required":["spec"]}}}}},"delete":{"operationId":"webhooks.clearPostingSpec","summary":"Remove a posting spec","description":"Remove a webhook's posting spec and its headers. Deliveries go back to the standard JSON payload.","tags":["webhooks"],"parameters":[{"name":"webhookId","in":"path","required":true,"description":"The webhook's `id`.","schema":{"type":"string","description":"The webhook's `id`."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The webhook's id."},"brandId":{"type":"string","description":"The brand it belongs to."},"formId":{"description":"The one form it delivers for. Null means every form of the brand.","type":["string","null"]},"url":{"type":"string","description":"Where deliveries are POSTed."},"events":{"type":"array","items":{"type":"string","enum":["submission.abandoned","submission.completed"]},"description":"The events it receives."},"active":{"type":"boolean","description":"Whether it gets new deliveries. An inactive webhook gets none."},"createdAt":{"type":"number","description":"When it was made, in milliseconds since the epoch."},"postingSpec":{"anyOf":[{"type":"object","properties":{"format":{"type":"string","enum":["json","form"],"description":"How the body is encoded. `\"json\"` sends a flat JSON object of strings, and `\"form\"` sends it form-encoded."},"entries":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":200,"description":"The key in the body, exactly as the buyer names it."},"value":{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"field","description":"A question's answer. Lists are joined with commas and files are sent as their links. A question that's gone or wasn't answered sends an empty string."},"fieldId":{"type":"string","minLength":1,"maxLength":100,"description":"A question's id in the form's definition."},"part":{"description":"One part of the answer. For a name question, `first` or `last`. For an address question, `line1`, `line2`, `city`, `state`, `zip` or `country`. `state` is the region, such as a state, province or county. `zip` is the postal code. `country` is the ISO 3166-1 alpha-2 code. Leave it out to send the whole answer. A whole address is one line, written the way its country writes addresses, with the country's name.","type":"string","enum":["first","last","line1","line2","city","state","zip","country"]}},"required":["kind","fieldId"],"additionalProperties":false},{"type":"object","properties":{"kind":{"type":"string","const":"meta","description":"A contact or tracking value."},"source":{"type":"string","enum":["email","phone","fullName","firstName","lastName","ip","userAgent","pageUrl","submittedAt","responseId","formName","jornayaLeadId","trustedFormCertUrl","consent","consentAt","consentText","phoneVerified"],"description":"Which value to send. `trustedFormCertUrl` is the response's TrustedForm certificate URL. It is empty until responses carry one. `consent` is `yes` when the person agreed to be called and texted and `no` otherwise. `consentAt` is when they agreed, and `consentText` the wording they agreed to. Both are empty unless they agreed. `phoneVerified` is `yes` when the number was verified with a texted code, separate from consent."}},"required":["kind","source"],"additionalProperties":false},{"type":"object","properties":{"kind":{"type":"string","const":"constant","description":"The same text on every lead."},"value":{"type":"string","maxLength":2000}},"required":["kind","value"],"additionalProperties":false}],"description":"What to send under the key. Every value is sent as a string."},"format":{"description":"How to write the value. For phones, `phoneDigits` gives just the digits, without the +1 for US and Canada numbers and with the country code for others. `phoneE164` gives E.164, and `phoneUs` gives (415) 555-0123. For dates, `submittedAt` and `consentAt`, `dateIso` gives YYYY-MM-DD and `dateUs` gives MM/DD/YYYY. For yes/no values, such as `consent`, a yes/no question or a consent question, `boolTrueFalse` gives true or false, `boolOneZero` gives 1 or 0, and `boolYN` gives Y or N. Leave it out to send the value as stored. Constants ignore it.","type":"string","enum":["phoneDigits","phoneE164","phoneUs","dateIso","dateUs","boolTrueFalse","boolOneZero","boolYN"]}},"required":["key","value"],"additionalProperties":false},"description":"The body's keys, in order."},"headers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"}},"required":["name"],"additionalProperties":false},"description":"The extra headers' names. Values are never returned."},"warnings":{"type":"array","items":{"type":"object","properties":{"index":{"type":"number","description":"The entry's position in `entries`."},"key":{"type":"string","description":"The entry's key."},"message":{"type":"string","description":"What to check, in plain words."}},"required":["index","key","message"],"additionalProperties":false},"description":"Entries to check against the form's current questions, such as one whose question is gone. Such an entry sends an empty string."}},"required":["format","entries","headers","warnings"],"additionalProperties":false},{"type":"null"}],"description":"How it posts in a buyer's own format. Null means it sends the standard JSON payload."},"onlyConsented":{"type":"boolean","description":"Whether it sends only responses where the person agreed to be called and texted."}},"required":["id","brandId","formId","url","events","active","createdAt","postingSpec","onlyConsented"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/webhooks/{webhookId}/posting-spec/preview":{"post":{"operationId":"webhooks.previewPosting","summary":"Preview a posting spec","description":"See the body a form webhook's posting spec would post, without sending anything. By default it maps a made-up lead.","tags":["webhooks"],"parameters":[{"name":"webhookId","in":"path","required":true,"description":"The webhook's `id`.","schema":{"type":"string","description":"The webhook's `id`."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"source":{"type":"string","enum":["sample","response"],"description":"`\"sample\"` when it mapped a made-up lead, which also happens when `latest` found no response. `\"response\"` when it mapped a real one."},"submissionId":{"description":"The response it mapped. Null for a made-up lead.","type":["string","null"]},"contentType":{"type":"string","description":"The Content-Type the body is sent with."},"body":{"type":"string","description":"The body, exactly as it would be posted. Files show their names here, while real deliveries carry links."},"pairs":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"value":{"type":"string"}},"required":["key","value"],"additionalProperties":false},"description":"The keys and values, in order."},"headers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"}},"required":["name"],"additionalProperties":false},"description":"The extra headers' names."},"warnings":{"type":"array","items":{"type":"object","properties":{"index":{"type":"number","description":"The entry's position in `entries`."},"key":{"type":"string","description":"The entry's key."},"message":{"type":"string","description":"What to check, in plain words."}},"required":["index","key","message"],"additionalProperties":false},"description":"The spec's entries to check."},"questions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"},"type":{"type":"string"}},"required":["id","label","type"],"additionalProperties":false},"description":"The form's questions. These are what `fieldId` can name."}},"required":["source","submissionId","contentType","body","pairs","headers","warnings","questions"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"spec":{"description":"A draft spec to preview, in the same shape as `POST /v1/webhooks/{webhookId}/posting-spec`. Header values aren't needed. Leave it out to preview the saved spec.","type":"object","properties":{"format":{"type":"string","enum":["json","form"],"description":"How the body is encoded. `\"json\"` sends application/json and `\"form\"` sends application/x-www-form-urlencoded."},"entries":{"minItems":1,"maxItems":200,"type":"array","items":{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":200,"description":"The key in the body, exactly as the buyer names it."},"value":{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","const":"field","description":"A question's answer. Lists are joined with commas and files are sent as their links. A question that's gone or wasn't answered sends an empty string."},"fieldId":{"type":"string","minLength":1,"maxLength":100,"description":"A question's id in the form's definition."},"part":{"description":"One part of the answer. For a name question, `first` or `last`. For an address question, `line1`, `line2`, `city`, `state`, `zip` or `country`. `state` is the region, such as a state, province or county. `zip` is the postal code. `country` is the ISO 3166-1 alpha-2 code. Leave it out to send the whole answer. A whole address is one line, written the way its country writes addresses, with the country's name.","type":"string","enum":["first","last","line1","line2","city","state","zip","country"]}},"required":["kind","fieldId"]},{"type":"object","properties":{"kind":{"type":"string","const":"meta","description":"A contact or tracking value."},"source":{"type":"string","enum":["email","phone","fullName","firstName","lastName","ip","userAgent","pageUrl","submittedAt","responseId","formName","jornayaLeadId","trustedFormCertUrl","consent","consentAt","consentText","phoneVerified"],"description":"Which value to send. `trustedFormCertUrl` is the response's TrustedForm certificate URL. It is empty until responses carry one. `consent` is `yes` when the person agreed to be called and texted and `no` otherwise. `consentAt` is when they agreed, and `consentText` the wording they agreed to. Both are empty unless they agreed. `phoneVerified` is `yes` when the number was verified with a texted code, separate from consent."}},"required":["kind","source"]},{"type":"object","properties":{"kind":{"type":"string","const":"constant","description":"The same text on every lead."},"value":{"type":"string","maxLength":2000}},"required":["kind","value"]}],"description":"What to send under the key. Every value is sent as a string."},"format":{"description":"How to write the value. For phones, `phoneDigits` gives just the digits, without the +1 for US and Canada numbers and with the country code for others. `phoneE164` gives E.164, and `phoneUs` gives (415) 555-0123. For dates, `submittedAt` and `consentAt`, `dateIso` gives YYYY-MM-DD and `dateUs` gives MM/DD/YYYY. For yes/no values, such as `consent`, a yes/no question or a consent question, `boolTrueFalse` gives true or false, `boolOneZero` gives 1 or 0, and `boolYN` gives Y or N. Leave it out to send the value as stored. Constants ignore it.","type":"string","enum":["phoneDigits","phoneE164","phoneUs","dateIso","dateUs","boolTrueFalse","boolOneZero","boolYN"]}},"required":["key","value"]},"description":"The body's keys, in order. The body holds exactly these keys. As JSON, it is a flat object of strings."},"headers":{"description":"Extra headers to send, such as the buyer's API key. They go alongside Forms-Event, Forms-Delivery and Forms-Signature. Header values are never returned.","maxItems":20,"type":"array","items":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[!#$%&'*+.^_`|~0-9A-Za-z-]+$"},"value":{"description":"The header's value. Leave it out to keep the saved value. Header values are never returned.","type":"string","maxLength":2000,"pattern":"^[^\\r\\n\\0]*$"}},"required":["name"]}}},"required":["format","entries"]},"submissionId":{"description":"Map this response of the webhook's form instead of a made-up lead.","type":"string"},"latest":{"description":"Map the form's latest response instead of a made-up lead. When the form has no responses, it falls back to a made-up lead.","type":"boolean"}}}}}}}},"/v1/sites/read":{"post":{"operationId":"sites.read","summary":"Read a website","description":"Start reading a website for its look and for a brand name and three form ideas, which AI writes from the site's text. The look covers the button color, text and background colors, corner radius and font. It returns a `readId` right away. Poll `GET /v1/sites/reads/{readId}` until `themeStatus` and `suggestStatus` are no longer `reading` or `waiting`. Private and local addresses fail with `400 import_failed`, and more than 60 reads an hour that need new ideas fail with `429 rate_limited`.","tags":["sites"],"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"readId":{"type":"string","description":"The read's id. The look is read afresh on every call. The brand name and ideas from a read of the same address in the last 7 days are reused, and `suggestStatus` is then `ready` right away. If a read of the same address is still in progress, you get that read."}},"required":["readId"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"workspaceId":{"description":"Optional. Your API key already belongs to one workspace.","type":"string"},"url":{"type":"string","minLength":1,"maxLength":2000,"description":"A website address. A bare host such as `example.com` gets `https://` added."}},"required":["url"]}}}}}},"/v1/sites/reads/{readId}":{"get":{"operationId":"sites.get","summary":"Get a website read","description":"A website read's look, brand name and form ideas. A site that can't be fetched still gets a readable default theme, and ideas based on its address alone. If the AI fails, the theme is kept.","tags":["sites"],"parameters":[{"name":"readId","in":"path","required":true,"description":"The read, from `POST /v1/sites/read`.","schema":{"type":"string","description":"The read, from `POST /v1/sites/read`."}},{"name":"workspaceId","in":"query","required":false,"schema":{"description":"Optional. Your API key already belongs to one workspace.","type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","description":"The address that was read, with https and without a hash."},"host":{"type":"string","description":"The address's host, in lowercase, without `www.`."},"theme":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}],"description":"Theme values read from the site. Null until `themeStatus` is `ready`."},"themeStatus":{"type":"string","enum":["reading","ready","failed"]},"brandName":{"description":"The brand name suggested from the site. Null until `suggestStatus` is `ready`.","type":["string","null"]},"ideas":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","description":"A short label."},"prompt":{"type":"string","description":"A one-sentence prompt for building the form."}},"required":["label","prompt"],"additionalProperties":false}},{"type":"null"}],"description":"Three form ideas, the first being the suggested one. Null until `suggestStatus` is `ready`."},"suggestStatus":{"type":"string","enum":["waiting","ready","failed"]}},"required":["url","host","theme","themeStatus","brandName","ideas","suggestStatus"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/sites/icon":{"post":{"operationId":"sites.icon","summary":"Find a website's icon","description":"Find a website's icon, its favicon, without saving anything. A brand keeps its own in `avatarUrl`. Private and local addresses fail with `400 import_failed`, and more than 120 calls an hour fail with `429 rate_limited`.","tags":["sites"],"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"iconUrl":{"description":"The address of the sharpest icon offered by the site's home page, `/favicon.ico` or web manifest, checked to be a real image. Null when the site has none or can't be reached.","type":["string","null"]}},"required":["iconUrl"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"workspaceId":{"description":"Optional. Your API key already belongs to one workspace.","type":"string"},"url":{"type":"string","minLength":1,"maxLength":2000,"description":"A website address. A bare host such as `example.com` works."}},"required":["url"]}}}}}},"/v1/me":{"get":{"operationId":"me.get","summary":"Get the caller","description":"Who is calling. For an API key, that is the key and its workspace, which makes it a quick check that a key works. For a signed-in person, it is their account.","tags":["me"],"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"kind":{"type":"string","const":"apiKey"},"id":{"type":"string","description":"The key's id."},"name":{"type":"string","description":"The key's name."},"workspace":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"description":"The workspace the key works in."}},"required":["kind","id","name","workspace"],"additionalProperties":false,"description":"An API key."},{"type":"object","properties":{"kind":{"type":"string","const":"user"},"id":{"type":"string"},"email":{"type":"string"},"name":{"type":["string","null"]},"canCreateWorkspace":{"type":"boolean","description":"Whether they may create a workspace. That needs a place on the sign-up list or a workspace they already own."},"staff":{"type":"boolean","description":"Whether they may use the staff panel."},"guest":{"type":"boolean","description":"Whether they are trying the editor without an account. At sign-up, `POST /v1/guests/claim` moves their forms into a workspace."}},"required":["kind","id","email","name","canCreateWorkspace","staff","guest"],"additionalProperties":false,"description":"A signed-in person."}]}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/me/sync":{"post":{"operationId":"me.sync","summary":"Finish signing in","description":"Copies the signed-in person's WorkOS profile to their account and their workspace memberships from WorkOS (an accepted invite shows up here), and makes a first workspace for someone new. The app calls it once per session. It fails with `403 email_not_verified` until WorkOS has verified the person's email. Only works from the app, not with an API key. See [Owner-only and app-only](/docs/api/owner-and-session#session-only).","tags":["me"],"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string"},"email":{"type":"string"},"workspaces":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"role":{"type":"string","enum":["owner","member"]}},"required":["id","name","role"],"additionalProperties":false},"description":"Every workspace they're in, with their role."},"gate":{"anyOf":[{"type":"string","enum":["invite_only"]},{"type":"null"}],"description":"`invite_only` for someone who is in no workspace and may not create one. Null otherwise."},"joinedWorkspaceId":{"description":"A workspace this sync added them to through an accepted invite, or null.","type":["string","null"]}},"required":["userId","email","workspaces","gate","joinedWorkspaceId"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}},"/v1/guests/claim":{"post":{"operationId":"guests.claim","summary":"Claim a guest's forms","description":"After signing up or in, keep what you made as a guest. This moves the guest's forms, with the same ids, and their brand into the workspace. Calling it again after it worked returns the same result. It fails with `403 brand_limit` when the workspace can't take another brand and no `brandId` was given, with `403 form_limit` when the plan can't take the forms, and with `401 guest_expired` for a token that is expired, forged or claimed by someone else. Only works from the app, not with an API key. See [Owner-only and app-only](/docs/api/owner-and-session#session-only).","tags":["guests"],"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"workspaceId":{"type":"string","description":"The workspace the forms moved into."},"formIds":{"type":"array","items":{"type":"string"},"description":"The forms that moved. They keep their ids."}},"required":["workspaceId","formIds"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string","minLength":1,"maxLength":4096,"description":"The guest token that `POST /guest` gave."},"workspaceId":{"type":"string","description":"The workspace to move the forms into."},"brandId":{"description":"One of the workspace's brands. The forms go under it, and the guest's own brand is dropped.","type":"string"}},"required":["token","workspaceId"]}}}}}},"/v1/workspaces":{"get":{"operationId":"workspaces.list","summary":"List workspaces","description":"The workspaces you're in, with your role in each. An API key sees only its own workspace.","tags":["workspaces"],"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The workspace's id."},"name":{"type":"string"},"role":{"type":"string","enum":["owner","member"],"description":"Your role in the workspace, `owner` or `member`. An API key counts as a member."}},"required":["id","name","role"],"additionalProperties":false}}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}},"post":{"operationId":"workspaces.create","summary":"Create a workspace","description":"Create a workspace with you as its owner. With billing on, your first workspace gets a 7-day Agencies trial, and any other starts suspended until you pick a plan with `POST /v1/workspaces/{workspaceId}/billing/checkout`. While sign-up is invite-only, it fails with `403 invite_only` unless you are on the sign-up list or already own a workspace. Only works from the app, not with an API key. See [Owner-only and app-only](/docs/api/owner-and-session#session-only).","tags":["workspaces"],"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"description":"The workspace's name. Leave it out to name it after your email. If you are already in an \"Acme\", the new one is called \"Acme 2\".","type":"string","minLength":1,"maxLength":100}}}}}}}},"/v1/workspaces/{workspaceId}":{"get":{"operationId":"workspaces.get","summary":"Get a workspace","description":"A workspace you're in: its name, your role and its owner.","tags":["workspaces"],"parameters":[{"name":"workspaceId","in":"path","required":true,"description":"The workspace's id. An API key can only use its own workspace.","schema":{"type":"string","description":"The workspace's id. An API key can only use its own workspace."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The workspace's id."},"name":{"type":"string"},"role":{"type":"string","enum":["owner","member"],"description":"Your role in the workspace, `owner` or `member`. An API key counts as a member."},"ownerId":{"type":"string","description":"The owner's user id."}},"required":["id","name","role","ownerId"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}},"patch":{"operationId":"workspaces.rename","summary":"Rename a workspace","description":"Give the workspace a new name. Any member can.","tags":["workspaces"],"parameters":[{"name":"workspaceId","in":"path","required":true,"description":"The workspace's id. An API key can only use its own workspace.","schema":{"type":"string","description":"The workspace's id. An API key can only use its own workspace."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"The new name."}},"required":["name"]}}}}},"delete":{"operationId":"workspaces.delete","summary":"Delete a workspace","description":"Delete the workspace: its members lose access, its API keys are revoked, and its exports and own provider keys are deleted. It fails with `409 workspace_has_brands` while it has brands, and with `409 workspace_has_responses` while it has responses, since responses outlive their brands. Only the workspace owner can do this, and only from the app. See [Owner-only and app-only](/docs/api/owner-and-session).","tags":["workspaces"],"parameters":[{"name":"workspaceId","in":"path","required":true,"description":"The workspace's id. An API key can only use its own workspace.","schema":{"type":"string","description":"The workspace's id. An API key can only use its own workspace."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/workspaces/{workspaceId}/transfer":{"post":{"operationId":"workspaces.transfer","summary":"Transfer ownership","description":"Make another member the owner. You become a member. Only the workspace owner can do this, and only from the app. See [Owner-only and app-only](/docs/api/owner-and-session).","tags":["workspaces"],"parameters":[{"name":"workspaceId","in":"path","required":true,"description":"The workspace's id. An API key can only use its own workspace.","schema":{"type":"string","description":"The workspace's id. An API key can only use its own workspace."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"description":"The member to make the owner. They must already be a member, or the call answers `404`.","type":"string"}},"required":["userId"]}}}}}},"/v1/workspaces/{workspaceId}/leave":{"post":{"operationId":"workspaces.leave","summary":"Leave a workspace","description":"Leave the workspace. The owner can't leave and gets `409 owner_must_transfer`, so transfer ownership first. Only works from the app, not with an API key. See [Owner-only and app-only](/docs/api/owner-and-session#session-only).","tags":["workspaces"],"parameters":[{"name":"workspaceId","in":"path","required":true,"description":"The workspace's id. An API key can only use its own workspace.","schema":{"type":"string","description":"The workspace's id. An API key can only use its own workspace."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}},"/v1/workspaces/{workspaceId}/members":{"get":{"operationId":"members.list","summary":"List members","description":"The workspace's members with their role and brand access, the owner first. See [Brand access](/docs/api/brand-access).","tags":["members"],"parameters":[{"name":"workspaceId","in":"path","required":true,"description":"The workspace's id. An API key can only use its own workspace.","schema":{"type":"string","description":"The workspace's id. An API key can only use its own workspace."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"userId":{"type":"string"},"email":{"type":"string"},"name":{"type":["string","null"]},"role":{"type":"string","enum":["owner","member"]},"brandIds":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"The only brands this member sees, or null when they see every brand. Set it with `PATCH /v1/workspaces/{workspaceId}/members/{userId}/brand-access`. If you are limited to some brands yourself, this lists only brands you also see."},"otherBrands":{"type":"boolean","description":"True when every brand this member is limited to is one you don't see."},"keysGoOnRemoval":{"type":"boolean","description":"True when the member is limited to some brands. Removing such a member also revokes their API keys."}},"required":["userId","email","name","role","brandIds","otherBrands","keysGoOnRemoval"],"additionalProperties":false}}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/workspaces/{workspaceId}/members/{userId}/brand-access":{"patch":{"operationId":"members.setBrandAccess","summary":"Set brand access","description":"Limit a member to some brands, or give every brand back. For them and their API keys, other brands don't exist: they aren't listed and answer `404`. Brands they create join their list. See [Brand access](/docs/api/brand-access). The owner always sees every brand, so limiting the owner fails with `409 owner_has_every_brand`. Only the workspace owner can do this, and only from the app. See [Owner-only and app-only](/docs/api/owner-and-session).","tags":["members"],"parameters":[{"name":"workspaceId","in":"path","required":true,"description":"The workspace's id. An API key can only use its own workspace.","schema":{"type":"string","description":"The workspace's id. An API key can only use its own workspace."}},{"name":"userId","in":"path","required":true,"description":"The member.","schema":{"type":"string","description":"The member."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string"},"email":{"type":"string"},"name":{"type":["string","null"]},"role":{"type":"string","enum":["owner","member"]},"brandIds":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"The only brands this member sees, or null when they see every brand. Set it with `PATCH /v1/workspaces/{workspaceId}/members/{userId}/brand-access`. If you are limited to some brands yourself, this lists only brands you also see."},"otherBrands":{"type":"boolean","description":"True when every brand this member is limited to is one you don't see."},"keysGoOnRemoval":{"type":"boolean","description":"True when the member is limited to some brands. Removing such a member also revokes their API keys."}},"required":["userId","email","name","role","brandIds","otherBrands","keysGoOnRemoval"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"brandIds":{"anyOf":[{"minItems":1,"maxItems":500,"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"The brands this member may see, at least one. Send null to give every brand back, which always works. Limiting needs per-brand access, which is on the Agencies plan; other plans get `403 plan_feature`."}},"required":["brandIds"]}}}}}},"/v1/workspaces/{workspaceId}/members/{userId}":{"delete":{"operationId":"members.remove","summary":"Remove a member","description":"Remove a member. Their access ends at once. The owner can't be removed and answers `409 cannot_remove_owner`. A member limited to some brands can't remove anyone and gets `403 brand_access_limited`, but they can leave.","tags":["members"],"parameters":[{"name":"workspaceId","in":"path","required":true,"description":"The workspace's id. An API key can only use its own workspace.","schema":{"type":"string","description":"The workspace's id. An API key can only use its own workspace."}},{"name":"userId","in":"path","required":true,"description":"The member to remove.","schema":{"type":"string","description":"The member to remove."}},{"name":"revokeKeys","in":"query","required":false,"schema":{"default":false,"description":"Also revoke the API keys this person created in this workspace. Otherwise their keys belong to the workspace and keep working. A member limited to some brands always has their keys revoked, because a key reaches what its creator reaches.","type":"boolean"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/workspaces/{workspaceId}/invites":{"get":{"operationId":"invites.list","summary":"List invites","description":"The workspace's pending invites, newest first. A member limited to some brands doesn't manage invites, so for them the list is empty.","tags":["invites"],"parameters":[{"name":"workspaceId","in":"path","required":true,"description":"The workspace's id. An API key can only use its own workspace.","schema":{"type":"string","description":"The workspace's id. An API key can only use its own workspace."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string","description":"The invited email address. Only this address can accept the invite."},"acceptUrl":{"type":"string","description":"The link in the invite email. Only the invited address can accept it."},"expiresAt":{"type":"number","description":"When the link stops working, in milliseconds since the epoch."},"createdAt":{"type":"number","description":"When the invite was sent, in milliseconds since the epoch."},"invitedBy":{"anyOf":[{"type":"object","properties":{"kind":{"type":"string","const":"user"},"userId":{"type":"string"}},"required":["kind","userId"],"additionalProperties":false},{"type":"object","properties":{"kind":{"type":"string","const":"apiKey"},"apiKeyId":{"type":"string"}},"required":["kind","apiKeyId"],"additionalProperties":false}],"description":"Who sent the invite, a person or an API key."},"expired":{"type":"boolean","description":"True once `expiresAt` has passed and the link no longer works. Resending the invite sends a fresh one."}},"required":["id","email","acceptUrl","expiresAt","createdAt","invitedBy","expired"],"additionalProperties":false}}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}},"post":{"operationId":"invites.create","summary":"Invite a member","description":"Invite someone by email to join as a member. WorkOS emails them a link, and they accept by signing in or signing up with that address. It fails with `409 already_member` if they are already in the workspace, and with `403 plan_limit` when members plus pending invites fill the plan's users. A member limited to some brands can't invite, because the new member would see every brand, and gets `403 brand_access_limited`.","tags":["invites"],"parameters":[{"name":"workspaceId","in":"path","required":true,"description":"The workspace's id. An API key can only use its own workspace.","schema":{"type":"string","description":"The workspace's id. An API key can only use its own workspace."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","maxLength":320,"format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$","description":"The email address to invite. An address that already has a pending invite gets it again, or a fresh one if it expired."}},"required":["email"]}}}}}},"/v1/workspaces/{workspaceId}/invites/{inviteId}":{"delete":{"operationId":"invites.cancel","summary":"Cancel an invite","description":"Cancel a pending invite, so its link stops working. An invite already accepted or cancelled answers `409 invite_not_pending`. A member limited to some brands gets `403 brand_access_limited`.","tags":["invites"],"parameters":[{"name":"workspaceId","in":"path","required":true,"description":"The workspace's id. An API key can only use its own workspace.","schema":{"type":"string","description":"The workspace's id. An API key can only use its own workspace."}},{"name":"inviteId","in":"path","required":true,"description":"The invite.","schema":{"type":"string","description":"The invite."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/workspaces/{workspaceId}/invites/{inviteId}/resend":{"post":{"operationId":"invites.resend","summary":"Resend an invite","description":"Send a pending invite's email again and push back its expiry. An expired invite is replaced by a fresh one with a new id, so use the invite this returns. An invite already accepted or cancelled answers `409 invite_not_pending`. A member limited to some brands gets `403 brand_access_limited`.","tags":["invites"],"parameters":[{"name":"workspaceId","in":"path","required":true,"description":"The workspace's id. An API key can only use its own workspace.","schema":{"type":"string","description":"The workspace's id. An API key can only use its own workspace."}},{"name":"inviteId","in":"path","required":true,"description":"The invite.","schema":{"type":"string","description":"The invite."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}},"/v1/workspaces/{workspaceId}/entitlements":{"get":{"operationId":"workspaces.entitlements","summary":"Get plan entitlements","description":"What the workspace's plan lets it do: its limits, what it uses, its features and how much response history it shows. Creating past a limit answers `403 plan_limit`. Using a feature the plan lacks answers `403 plan_feature`, and one the workspace's stage doesn't see yet answers `403 feature_unavailable`. See [Paid checks](/docs/api/plan-checks) and [Plan history and retention](/docs/api/retention).","tags":["workspaces"],"parameters":[{"name":"workspaceId","in":"path","required":true,"description":"The workspace's id. An API key can only use its own workspace.","schema":{"type":"string","description":"The workspace's id. An API key can only use its own workspace."}},{"name":"usage","in":"query","required":false,"schema":{"description":"Whether to count what the workspace uses. Defaults to true. Send false to skip counting, and `usage` comes back null.","type":"boolean"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"type":"string","enum":["business","publishers","agencies"],"description":"The plan the limits come from."},"planName":{"type":"string"},"status":{"type":"string","enum":["trialing","active","suspended"]},"trialEndsAt":{"description":"When the trial ends, in milliseconds since the epoch. Null when there is no trial.","type":["number","null"]},"limits":{"type":"object","properties":{"forms":{"description":"Forms, drafts included. Null means unlimited.","type":["number","null"]},"brands":{"description":"Brands. Null means unlimited.","type":["number","null"]},"users":{"description":"Members, pending invites included. Null means unlimited.","type":["number","null"]}},"required":["forms","brands","users"],"additionalProperties":false,"description":"How many of each the plan includes. Null means unlimited."},"usage":{"anyOf":[{"type":"object","properties":{"forms":{"type":"number"},"brands":{"type":"number"},"users":{"type":"number"}},"required":["forms","brands","users"],"additionalProperties":false},{"type":"null"}],"description":"How much of each limit the workspace uses, counted up to 500. Null when you pass `usage=false`, and for a member limited to some brands."},"retentionDays":{"description":"Responses older than this many days are hidden, never deleted. Null means every response is shown.","type":["number","null"]},"features":{"type":"object","properties":{"tracking":{"type":"boolean"},"security":{"type":"boolean"},"botProtection":{"type":"boolean"},"exportsAndApi":{"type":"boolean"},"leadId":{"type":"boolean"},"perBrandAccess":{"type":"boolean"},"verification":{"type":"boolean","description":"Phone `verify` and email `validate` fields. In alpha, so it's true only when both the plan and the workspace's stage allow it."},"aiFields":{"type":"boolean","description":"`ai_autocomplete` questions. In alpha, so it's true only when both the plan and the workspace's stage allow it."},"hideBadge":{"type":"boolean","description":"Forms may hide the \"Powered by ayeform\" badge with the form setting `hideBadge`."},"partialCapture":{"type":"boolean","description":"Live forms save unfinished responses with the form setting `partialCapture`, so visitors can come back to them and the `submission.abandoned` webhook event fires."}},"required":["tracking","security","botProtection","exportsAndApi","leadId","perBrandAccess","verification","aiFields","hideBadge","partialCapture"],"additionalProperties":{"type":"boolean"},"description":"What the plan includes, plus every released feature by name."},"planFeatures":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"boolean"},"description":"What the plan includes whatever the stage, with the same keys as `features`. Use it to show plans, not to decide what works."},"includedCreditsUsd":{"type":"number","description":"Free credits for metered checks this month, in dollars."},"integrations":{"type":"string","enum":["some","all"],"description":"Which integrations the plan includes, `some` or `all`."},"stage":{"type":"string","enum":["alpha","beta","general"],"description":"The workspace's release stage. A feature still in alpha or beta only works for workspaces whose stage sees it."},"overrides":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{},"description":"Staff's changes to the plan."},"free":{"anyOf":[{"type":"object","properties":{"plan":{"anyOf":[{"type":"string","enum":["business","publishers","agencies"]},{"type":"null"}]},"metering":{"type":"boolean"}},"required":["plan","metering"],"additionalProperties":false},{"type":"null"}],"description":"Set when staff made the workspace free, so it is never billed. Null otherwise."},"plans":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","enum":["business","publishers","agencies"]},"name":{"type":"string"},"priceUsd":{"type":"number","description":"The monthly price when paid monthly."},"yearlyPerMonthUsd":{"type":"number","description":"The monthly price when paid yearly. A year costs 12 times this."},"limits":{"type":"object","properties":{"forms":{"description":"Forms, drafts included. Null means unlimited.","type":["number","null"]},"brands":{"description":"Brands. Null means unlimited.","type":["number","null"]},"users":{"description":"Members, pending invites included. Null means unlimited.","type":["number","null"]}},"required":["forms","brands","users"],"additionalProperties":false,"description":"How many of each the plan includes. Null means unlimited."},"retentionDays":{"type":["number","null"]},"features":{"type":"object","properties":{"tracking":{"type":"boolean"},"security":{"type":"boolean"},"botProtection":{"type":"boolean"},"exportsAndApi":{"type":"boolean"},"leadId":{"type":"boolean"},"perBrandAccess":{"type":"boolean"},"verification":{"type":"boolean","description":"Phone `verify` and email `validate` fields. In alpha, so it's true only when both the plan and the workspace's stage allow it."},"aiFields":{"type":"boolean","description":"`ai_autocomplete` questions. In alpha, so it's true only when both the plan and the workspace's stage allow it."},"hideBadge":{"type":"boolean","description":"Forms may hide the \"Powered by ayeform\" badge with the form setting `hideBadge`."},"partialCapture":{"type":"boolean","description":"Live forms save unfinished responses with the form setting `partialCapture`, so visitors can come back to them and the `submission.abandoned` webhook event fires."}},"required":["tracking","security","botProtection","exportsAndApi","leadId","perBrandAccess","verification","aiFields","hideBadge","partialCapture"],"additionalProperties":{"type":"boolean"},"description":"What the plan includes, plus every released feature by name."},"integrations":{"type":"string","enum":["some","all"]},"includedCreditsUsd":{"type":"number"}},"required":["id","name","priceUsd","yearlyPerMonthUsd","limits","retentionDays","features","integrations","includedCreditsUsd"],"additionalProperties":false},"description":"Every plan's limits and price."},"billingOn":{"type":"boolean","description":"Whether plans are for sale on this deployment. While false, no message asks anyone to upgrade."}},"required":["plan","planName","status","trialEndsAt","limits","usage","retentionDays","features","planFeatures","includedCreditsUsd","integrations","stage","overrides","free","plans","billingOn"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/workspaces/{workspaceId}/billing":{"get":{"operationId":"billing.get","summary":"Get billing","description":"The workspace's plan, subscription and usage, and every plan it could move to with its price, limits and what it would stop. Any member or API key can read it.","tags":["billing"],"parameters":[{"name":"workspaceId","in":"path","required":true,"description":"The workspace's id. An API key can only use its own workspace.","schema":{"type":"string","description":"The workspace's id. An API key can only use its own workspace."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"mode":{"type":"string","enum":["off","unconfigured","on"],"description":"`on` when billing runs on this deployment."},"plan":{"type":"string","enum":["business","publishers","agencies"]},"planName":{"type":"string"},"free":{"anyOf":[{"type":"object","properties":{"plan":{"anyOf":[{"type":"string","enum":["business","publishers","agencies"]},{"type":"null"}],"description":"The plan it is free on. Null means no plan, with everything included."},"metering":{"type":"boolean","description":"False when checks are never charged."}},"required":["plan","metering"],"additionalProperties":false},{"type":"null"}],"description":"Set by staff, or null. A free workspace is never billed and has no checkout."},"status":{"type":"string","enum":["trialing","active","suspended"],"description":"A suspended workspace's forms show \"This form is currently inactive\", and it can't publish."},"suspendedBecause":{"anyOf":[{"type":"string","enum":["trial_ended","lapsed","no_plan"]},{"type":"null"}]},"trialEndsAt":{"description":"When the trial ends, in milliseconds since the epoch. Set while trialing.","type":["number","null"]},"subscription":{"anyOf":[{"type":"object","properties":{"cancelAtPeriodEnd":{"type":"boolean"},"currentPeriodEnd":{"description":"The renewal date, in milliseconds since the epoch.","type":["number","null"]},"interval":{"type":"string","enum":["month","year"]},"scheduledChange":{"anyOf":[{"type":"object","properties":{"plan":{"type":"string","enum":["business","publishers","agencies"]},"interval":{"type":"string","enum":["month","year"]},"at":{"type":"number"}},"required":["plan","interval","at"],"additionalProperties":false},{"type":"null"}],"description":"A change that waits for the period to end, such as yearly to monthly. Null when none is waiting."}},"required":["cancelAtPeriodEnd","currentPeriodEnd","interval","scheduledChange"],"additionalProperties":false},{"type":"null"}]},"yearlyAvailable":{"type":"boolean","description":"Plans can be paid yearly here, at priceYearly, billed once a year."},"canManage":{"type":"boolean","description":"True when the caller is the owner, who alone can pay."},"usage":{"anyOf":[{"type":"object","properties":{"forms":{"type":"number"},"brands":{"type":"number"},"users":{"type":"number"}},"required":["forms","brands","users"],"additionalProperties":false},{"type":"null"}],"description":"Null for a member limited to some brands, since nothing is counted for them."},"plans":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","enum":["business","publishers","agencies"]},"name":{"type":"string"},"priceMonthly":{"type":"number"},"priceYearly":{"type":"number"},"limits":{"type":"object","properties":{"forms":{"type":["number","null"]},"brands":{"type":["number","null"]},"users":{"type":["number","null"]}},"required":["forms","brands","users"],"additionalProperties":false},"features":{"type":"array","items":{"type":"string"}},"retentionDays":{"type":["number","null"]},"includedCreditsUsd":{"type":"number","description":"Free credits each month for metered checks."},"over":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string","enum":["limit","feature"]},"key":{"type":"string"},"message":{"type":"string"}},"required":["kind","key","message"],"additionalProperties":false}},{"type":"null"}],"description":"What is over this plan's limits. A workspace can only buy a plan it fits. Null for a member limited to some brands."},"stops":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"check":{"type":"string","enum":["phoneVerification","emailValidation","telegramCheck","aiAutocomplete"]},"feature":{"type":"string","description":"The plan feature the check needs."},"forms":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false},"description":"The published forms that run it."},"message":{"type":"string"}},"required":["check","feature","forms","message"],"additionalProperties":false}},{"type":"null"}],"description":"Checks that published forms run now and that would stop on this plan. The move is still allowed, and the forms skip the check and keep the lead. Null for a member limited to some brands."}},"required":["id","name","priceMonthly","priceYearly","limits","features","retentionDays","includedCreditsUsd","over","stops"],"additionalProperties":false},"description":"Every plan."},"suggestedPlan":{"anyOf":[{"type":"string","enum":["business","publishers","agencies"]},{"type":"null"}],"description":"The cheapest plan that covers the workspace's usage. Null for a member limited to some brands."}},"required":["mode","plan","planName","free","status","suspendedBecause","trialEndsAt","subscription","yearlyAvailable","canManage","usage","plans","suggestedPlan"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/workspaces/{workspaceId}/billing/checkout":{"post":{"operationId":"billing.checkout","summary":"Start a checkout","description":"Start paying for a plan through the provider's hosted checkout. Paying there makes the workspace active on that plan at once (lifting a suspension; published forms come back without republishing). It fails with `409 plan_does_not_fit` when the workspace doesn't fit the plan, and `issues` says what is over, such as \"4 brands, Business includes 1\". A subscription that hasn't ended answers `409 already_subscribed`: change an active one with `POST /v1/workspaces/{workspaceId}/billing/plan`, and one on hold or paused in the portal. It also fails with `409 billing_off` when billing isn't configured, `409 yearly_unavailable` for a yearly interval where yearly isn't sold, and `409 free_workspace` when staff made the workspace free. Only the workspace owner can do this, and only from the app. See [Owner-only and app-only](/docs/api/owner-and-session).","tags":["billing"],"parameters":[{"name":"workspaceId","in":"path","required":true,"description":"The workspace's id. An API key can only use its own workspace.","schema":{"type":"string","description":"The workspace's id. An API key can only use its own workspace."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","description":"The provider's hosted checkout."}},"required":["url"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"type":"string","enum":["business","publishers","agencies"],"description":"The plan: `business`, `publishers` or `agencies`."},"interval":{"default":"month","description":"month (the default) or year. Yearly is billed once a year at the plan's priceYearly, where `yearlyAvailable` in the billing state is true.","type":"string","enum":["month","year"]}},"required":["plan"]}}}}}},"/v1/workspaces/{workspaceId}/billing/plan":{"post":{"operationId":"billing.changePlan","summary":"Change the plan","description":"Switch a paying workspace to another plan or billing interval. Most changes apply now, prorated (what's left of the current period is credited and the billing date restarts), including monthly to yearly; yearly to monthly waits for the paid year to end. The new plan applies when the provider confirms (`GET /v1/workspaces/{workspaceId}/billing` then shows it). The workspace must fit the new plan, as at checkout, or it gets `409 plan_does_not_fit` with `issues`. So downgrading means cutting back first. A plan without phone and email verification or AI-assisted fields never blocks the move; it goes ahead and `stops` lists what stops. It fails with `409 no_subscription` when nothing is being paid, so start a checkout instead. It also fails with `409 already_on_plan`, `409 yearly_unavailable`, and `409 free_workspace` when staff made the workspace free. Only the workspace owner can do this, and only from the app. See [Owner-only and app-only](/docs/api/owner-and-session).","tags":["billing"],"parameters":[{"name":"workspaceId","in":"path","required":true,"description":"The workspace's id. An API key can only use its own workspace.","schema":{"type":"string","description":"The workspace's id. An API key can only use its own workspace."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"type":"string","enum":["business","publishers","agencies"]},"interval":{"type":"string","enum":["month","year"]},"startsAt":{"description":"When a change that waits takes effect, in milliseconds since the epoch. Yearly to monthly waits for the end of the paid year. Null when the change applies now. Until it takes effect, `GET /v1/workspaces/{workspaceId}/billing` shows it as `subscription.scheduledChange`. Picking the current plan and interval again cancels a scheduled change.","type":["number","null"]},"stops":{"type":"array","items":{"type":"object","properties":{"check":{"type":"string","enum":["phoneVerification","emailValidation","telegramCheck","aiAutocomplete"]},"feature":{"type":"string","description":"The plan feature the check needs."},"forms":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false},"description":"The published forms that run it."},"message":{"type":"string"}},"required":["check","feature","forms","message"],"additionalProperties":false},"description":"Each check that stops and the published forms it stops on, as in `plans[].stops` of the billing state. Their leads still come in unverified, and AI autocomplete questions become plain text boxes."}},"required":["plan","interval","startsAt","stops"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"type":"string","enum":["business","publishers","agencies"],"description":"The plan: `business`, `publishers` or `agencies`."},"interval":{"description":"`month` or `year`. Leave it out to keep the current interval. Yearly is sold only where `yearlyAvailable` is true.","type":"string","enum":["month","year"]}},"required":["plan"]}}}}}},"/v1/workspaces/{workspaceId}/billing/portal":{"post":{"operationId":"billing.portal","summary":"Open the billing portal","description":"Open the provider's customer portal for cards, invoices and cancelling. Before the workspace's first checkout it fails with `409 no_billing_account`. Only the workspace owner can do this, and only from the app. See [Owner-only and app-only](/docs/api/owner-and-session).","tags":["billing"],"parameters":[{"name":"workspaceId","in":"path","required":true,"description":"The workspace's id. An API key can only use its own workspace.","schema":{"type":"string","description":"The workspace's id. An API key can only use its own workspace."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","description":"The provider's customer portal."}},"required":["url"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}},"/v1/workspaces/{workspaceId}/credits":{"get":{"operationId":"credits.get","summary":"Get credits","description":"The workspace's credits for metered checks (texting a code to confirm a phone number, checking an email address, checking a Telegram account, and AI autocomplete suggestions), in dollars, with each check's price. Any member or API key can read it. When too little is left for a check's price, the form skips the check. The response comes in marked not verified for a phone, or with `emailCheck` or `telegramCheck` set to `\"skipped\"`, and an AI autocomplete question shows a plain text box with no suggestions. A plan without the check's feature skips it the same way, whatever the credits. Phone, email and Telegram checks need `verification`, and AI autocomplete needs `aiFields` (see [Paid checks](/docs/api/plan-checks)).","tags":["credits"],"parameters":[{"name":"workspaceId","in":"path","required":true,"description":"The workspace's id. An API key can only use its own workspace.","schema":{"type":"string","description":"The workspace's id. An API key can only use its own workspace."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"month":{"type":"string","description":"The month, in UTC, such as \"2026-10\"."},"includedUsd":{"type":"number","description":"Free credits each month. They reset on the 1st and don't carry over."},"allowanceLeftUsd":{"type":"number","description":"Free credits left this month."},"balanceUsd":{"type":"number","description":"Topped-up credits. They never expire and are spent after the month's free credits. A check is charged in full even when it costs more than what's left, so this can go negative. What's owed is repaid from the next top-up or next month's free credits."},"availableUsd":{"type":"number","description":"Free credits left plus the balance. It can be 0 or less."},"prices":{"type":"object","properties":{"phoneVerification":{"type":"object","properties":{"usUsd":{"type":"number"},"byCountry":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"number"},"description":"The price per country code, such as `{ \"US\": 0.2 }`."}},"required":["usUsd","byCountry"],"additionalProperties":false,"description":"Charged only when the visitor's code is approved. A country not listed costs as much as the most expensive listed one."},"emailValidation":{"type":"object","properties":{"eachUsd":{"type":"number"}},"required":["eachUsd"],"additionalProperties":false,"description":"The same in every country. Charged once for each address a visitor's session checks."},"telegramCheck":{"type":"object","properties":{"eachUsd":{"type":"number"}},"required":["eachUsd"],"additionalProperties":false,"description":"Charged once per username a visitor's session checks, when Telegram answered."},"aiAutocomplete":{"type":"object","properties":{"eachUsd":{"type":"number"}},"required":["eachUsd"],"additionalProperties":false,"description":"Charged once for each visitor session that is shown suggestions."}},"required":["phoneVerification","emailValidation","telegramCheck","aiAutocomplete"],"additionalProperties":false},"canTopUp":{"type":"boolean","description":"True when the caller is the owner and billing and top-ups are set up on this deployment. Always false for a free workspace."},"metered":{"type":"boolean","description":"False when staff made the workspace free with metering off. Its checks are then never charged and never skipped, whatever the credits."},"keySources":{"type":"object","properties":{"phone":{"type":"string","enum":["ayeform","own","house"],"description":"Whose provider keys the check runs on. `ayeform` uses Ayeform's keys and is charged in credits. `own` uses the workspace's own keys and `house` uses keys Ayeform set up for it, and neither is ever charged."},"email":{"type":"string","enum":["ayeform","own","house"],"description":"Whose provider keys the check runs on. `ayeform` uses Ayeform's keys and is charged in credits. `own` uses the workspace's own keys and `house` uses keys Ayeform set up for it, and neither is ever charged."},"telegramFree":{"type":"boolean","description":"Telegram checks are never charged."}},"required":["phone","email","telegramFree"],"additionalProperties":false},"keyProblems":{"type":"object","properties":{"phoneVerification":{"description":"Set only for a check that runs on the workspace's own keys, and left out when all is well. `missing` means no key is saved, and `rejected` means the provider refused the saved key. In both cases the check is skipped until the owner saves a working key with `PUT /v1/workspaces/{workspaceId}/provider-keys/{provider}`. `error` means the provider's last answer was another error about the key's account or service, and checks are skipped while it lasts. The provider keys' `failure` says what went wrong.","type":"string","enum":["missing","rejected","error"]},"emailValidation":{"description":"Set only for a check that runs on the workspace's own keys, and left out when all is well. `missing` means no key is saved, and `rejected` means the provider refused the saved key. In both cases the check is skipped until the owner saves a working key with `PUT /v1/workspaces/{workspaceId}/provider-keys/{provider}`. `error` means the provider's last answer was another error about the key's account or service, and checks are skipped while it lasts. The provider keys' `failure` says what went wrong.","type":"string","enum":["missing","rejected","error"]}},"additionalProperties":false}},"required":["month","includedUsd","allowanceLeftUsd","balanceUsd","availableUsd","prices","canTopUp","metered","keySources","keyProblems"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/workspaces/{workspaceId}/credits/history":{"get":{"operationId":"credits.history","summary":"List credit history","description":"Every change to the workspace's credits, newest first, a page at a time. Any member or API key can read it.","tags":["credits"],"parameters":[{"name":"workspaceId","in":"path","required":true,"description":"The workspace's id. An API key can only use its own workspace.","schema":{"type":"string","description":"The workspace's id. An API key can only use its own workspace."}},{"name":"cursor","in":"query","required":false,"schema":{"description":"The `cursor` from the previous page. Leave it out for the first page.","type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"description":"How many entries per page, from 1 to 100. Defaults to 50.","type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"entries":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string","enum":["use","topUp","refund","grant","settle"],"description":"What changed the credits: `use`, `topUp`, `refund`, `grant` or `settle`. `settle` is last month's overage, repaid from this month's free credits."},"meter":{"description":"The check that was charged. Set for `use`.","type":"string","enum":["phoneVerification","emailValidation","telegramCheck","aiAutocomplete"]},"country":{"description":"The ISO country code of a phone check.","type":"string"},"formId":{"description":"The form that ran the check. Set when you can see its brand.","type":"string"},"amountUsd":{"type":"number"},"fromAllowanceUsd":{"type":"number","description":"How much came from the month's free credits. Set for `use` and `settle`."},"balanceChangeUsd":{"type":"number","description":"How much was added to the topped-up balance, or taken from it when negative."},"note":{"type":"string"},"at":{"type":"number","description":"When it happened, in milliseconds since the epoch."}},"required":["kind","amountUsd","fromAllowanceUsd","balanceChangeUsd","at"],"additionalProperties":false}},"cursor":{"description":"Pass it back as `cursor` to get the next page. Null on the last page. See [Pagination](/docs/api/pagination).","type":["string","null"]}},"required":["entries","cursor"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/workspaces/{workspaceId}/credits/top-up":{"post":{"operationId":"credits.topUp","summary":"Top up credits","description":"Buy credits through the provider's hosted checkout. Paid credits never expire and arrive once the provider confirms the payment (`GET /v1/workspaces/{workspaceId}/credits`). It fails with `409 billing_off` when billing isn't configured, `409 top_ups_unconfigured` while top-ups aren't set up on this deployment, and `409 free_workspace` when staff made the workspace free. Only the workspace owner can do this, and only from the app. See [Owner-only and app-only](/docs/api/owner-and-session).","tags":["credits"],"parameters":[{"name":"workspaceId","in":"path","required":true,"description":"The workspace's id. An API key can only use its own workspace.","schema":{"type":"string","description":"The workspace's id. An API key can only use its own workspace."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","description":"The provider's hosted checkout."}},"required":["url"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"amountUsd":{"type":"integer","minimum":10,"maximum":1000,"description":"How many dollars of credits to buy, in whole dollars from 10 to 1000."}},"required":["amountUsd"]}}}}}},"/v1/workspaces/{workspaceId}/provider-keys":{"get":{"operationId":"providerKeys.get","summary":"Get provider keys","description":"Whose keys the workspace's phone codes and email checks run on, and its own saved keys. Secrets are never returned: a key shows only its last 4 characters. Any member or API key can read it.","tags":["providerKeys"],"parameters":[{"name":"workspaceId","in":"path","required":true,"description":"The workspace's id. An API key can only use its own workspace.","schema":{"type":"string","description":"The workspace's id. An API key can only use its own workspace."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"phone":{"type":"object","properties":{"source":{"type":"string","enum":["ayeform","own","house"],"description":"Whose keys the check runs on. `ayeform` uses Ayeform's keys and is charged in credits. `own` uses the workspace's own keys and `house` uses keys Ayeform set up for it, and neither is charged. With `own` and no key saved, or a key the provider refuses, the check is skipped and the response comes in not verified or not checked. It never falls back to Ayeform's keys."},"key":{"anyOf":[{"type":"object","properties":{"last4":{"type":"string","description":"The key's last 4 characters."},"accountSid":{"description":"Twilio only.","type":"string"},"serviceSid":{"description":"Twilio only.","type":"string"},"addedAt":{"type":"number","description":"When the key was saved, in milliseconds since the epoch."},"failure":{"anyOf":[{"type":"object","properties":{"at":{"type":"number"},"kind":{"type":"string","enum":["rejected","error"],"description":"`rejected` means the provider refused the key, and checks are skipped until it's replaced. `error` means another error about the key's account or service, and checks are skipped while it lasts."},"message":{"type":"string","description":"The provider's words."}},"required":["at","kind","message"],"additionalProperties":false},{"type":"null"}],"description":"The provider's last problem with the key. It clears once the key works."}},"required":["last4","addedAt","failure"],"additionalProperties":false},{"type":"null"}],"description":"The workspace's own saved key, shown whatever the source so the owner can still remove it after staff change the source. Null when none is saved. Keys Ayeform set up for the workspace are never shown."}},"required":["source","key"],"additionalProperties":false,"description":"Phone codes (Twilio)."},"email":{"type":"object","properties":{"source":{"type":"string","enum":["ayeform","own","house"],"description":"Whose keys the check runs on. `ayeform` uses Ayeform's keys and is charged in credits. `own` uses the workspace's own keys and `house` uses keys Ayeform set up for it, and neither is charged. With `own` and no key saved, or a key the provider refuses, the check is skipped and the response comes in not verified or not checked. It never falls back to Ayeform's keys."},"key":{"anyOf":[{"type":"object","properties":{"last4":{"type":"string","description":"The key's last 4 characters."},"accountSid":{"description":"Twilio only.","type":"string"},"serviceSid":{"description":"Twilio only.","type":"string"},"addedAt":{"type":"number","description":"When the key was saved, in milliseconds since the epoch."},"failure":{"anyOf":[{"type":"object","properties":{"at":{"type":"number"},"kind":{"type":"string","enum":["rejected","error"],"description":"`rejected` means the provider refused the key, and checks are skipped until it's replaced. `error` means another error about the key's account or service, and checks are skipped while it lasts."},"message":{"type":"string","description":"The provider's words."}},"required":["at","kind","message"],"additionalProperties":false},{"type":"null"}],"description":"The provider's last problem with the key. It clears once the key works."}},"required":["last4","addedAt","failure"],"additionalProperties":false},{"type":"null"}],"description":"The workspace's own saved key, shown whatever the source so the owner can still remove it after staff change the source. Null when none is saved. Keys Ayeform set up for the workspace are never shown."}},"required":["source","key"],"additionalProperties":false,"description":"Email checks (NeverBounce)."},"telegramFree":{"type":"boolean","description":"Telegram checks are never charged."}},"required":["phone","email","telegramFree"],"additionalProperties":false,"description":"Secrets are never returned: a key shows only its last 4 characters."}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}},"/v1/workspaces/{workspaceId}/provider-keys/{provider}":{"put":{"operationId":"providerKeys.set","summary":"Save a provider key","description":"Save the workspace's own key for a provider, replacing any saved one. The key is tried with its provider first. If the provider refuses it, the call answers `400 key_rejected` with the provider's words, such as \"Twilio said: …\", and the saved key stays. Saved keys are encrypted, and a new one clears the old one's failure. It fails with `409 keys_not_enabled` unless staff set the provider's check to run on the workspace's own keys (phone for `twilio`, email for `neverbounce`), and with `409 keys_unconfigured` when `PROVIDER_KEYS_SECRET` isn't set on this deployment. Only the workspace owner can do this, and only from the app. See [Owner-only and app-only](/docs/api/owner-and-session).","tags":["providerKeys"],"parameters":[{"name":"workspaceId","in":"path","required":true,"description":"The workspace's id. An API key can only use its own workspace.","schema":{"type":"string","description":"The workspace's id. An API key can only use its own workspace."}},{"name":"provider","in":"path","required":true,"description":"The provider. `twilio` is Twilio Verify, which sends phone codes. `neverbounce` is NeverBounce, which checks email addresses.","schema":{"type":"string","description":"The provider. `twilio` is Twilio Verify, which sends phone codes. `neverbounce` is NeverBounce, which checks email addresses."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"phone":{"type":"object","properties":{"source":{"type":"string","enum":["ayeform","own","house"],"description":"Whose keys the check runs on. `ayeform` uses Ayeform's keys and is charged in credits. `own` uses the workspace's own keys and `house` uses keys Ayeform set up for it, and neither is charged. With `own` and no key saved, or a key the provider refuses, the check is skipped and the response comes in not verified or not checked. It never falls back to Ayeform's keys."},"key":{"anyOf":[{"type":"object","properties":{"last4":{"type":"string","description":"The key's last 4 characters."},"accountSid":{"description":"Twilio only.","type":"string"},"serviceSid":{"description":"Twilio only.","type":"string"},"addedAt":{"type":"number","description":"When the key was saved, in milliseconds since the epoch."},"failure":{"anyOf":[{"type":"object","properties":{"at":{"type":"number"},"kind":{"type":"string","enum":["rejected","error"],"description":"`rejected` means the provider refused the key, and checks are skipped until it's replaced. `error` means another error about the key's account or service, and checks are skipped while it lasts."},"message":{"type":"string","description":"The provider's words."}},"required":["at","kind","message"],"additionalProperties":false},{"type":"null"}],"description":"The provider's last problem with the key. It clears once the key works."}},"required":["last4","addedAt","failure"],"additionalProperties":false},{"type":"null"}],"description":"The workspace's own saved key, shown whatever the source so the owner can still remove it after staff change the source. Null when none is saved. Keys Ayeform set up for the workspace are never shown."}},"required":["source","key"],"additionalProperties":false,"description":"Phone codes (Twilio)."},"email":{"type":"object","properties":{"source":{"type":"string","enum":["ayeform","own","house"],"description":"Whose keys the check runs on. `ayeform` uses Ayeform's keys and is charged in credits. `own` uses the workspace's own keys and `house` uses keys Ayeform set up for it, and neither is charged. With `own` and no key saved, or a key the provider refuses, the check is skipped and the response comes in not verified or not checked. It never falls back to Ayeform's keys."},"key":{"anyOf":[{"type":"object","properties":{"last4":{"type":"string","description":"The key's last 4 characters."},"accountSid":{"description":"Twilio only.","type":"string"},"serviceSid":{"description":"Twilio only.","type":"string"},"addedAt":{"type":"number","description":"When the key was saved, in milliseconds since the epoch."},"failure":{"anyOf":[{"type":"object","properties":{"at":{"type":"number"},"kind":{"type":"string","enum":["rejected","error"],"description":"`rejected` means the provider refused the key, and checks are skipped until it's replaced. `error` means another error about the key's account or service, and checks are skipped while it lasts."},"message":{"type":"string","description":"The provider's words."}},"required":["at","kind","message"],"additionalProperties":false},{"type":"null"}],"description":"The provider's last problem with the key. It clears once the key works."}},"required":["last4","addedAt","failure"],"additionalProperties":false},{"type":"null"}],"description":"The workspace's own saved key, shown whatever the source so the owner can still remove it after staff change the source. Null when none is saved. Keys Ayeform set up for the workspace are never shown."}},"required":["source","key"],"additionalProperties":false,"description":"Email checks (NeverBounce)."},"telegramFree":{"type":"boolean","description":"Telegram checks are never charged."}},"required":["phone","email","telegramFree"],"additionalProperties":false,"description":"Secrets are never returned: a key shows only its last 4 characters."}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"twilio":{"description":"A Twilio Verify service's keys. Required when the provider is `twilio`, which otherwise answers `400 invalid`.","type":"object","properties":{"accountSid":{"type":"string","minLength":1,"maxLength":100},"authToken":{"type":"string","minLength":1,"maxLength":200},"serviceSid":{"type":"string","minLength":1,"maxLength":100,"description":"The Twilio Verify service's SID."}},"required":["accountSid","authToken","serviceSid"],"additionalProperties":false},"neverbounce":{"description":"The NeverBounce key. Required when the provider is `neverbounce`, which otherwise answers `400 invalid`.","type":"object","properties":{"apiKey":{"type":"string","minLength":1,"maxLength":200}},"required":["apiKey"],"additionalProperties":false}}}}}}},"delete":{"operationId":"providerKeys.remove","summary":"Remove a provider key","description":"Delete the workspace's own saved key for a provider, whatever its checks run on. If the source is `own`, that check is skipped until a key is saved again. Removing a key that isn't there is fine. Only the workspace owner can do this, and only from the app. See [Owner-only and app-only](/docs/api/owner-and-session).","tags":["providerKeys"],"parameters":[{"name":"workspaceId","in":"path","required":true,"description":"The workspace's id. An API key can only use its own workspace.","schema":{"type":"string","description":"The workspace's id. An API key can only use its own workspace."}},{"name":"provider","in":"path","required":true,"description":"The provider. `twilio` is Twilio Verify, which sends phone codes. `neverbounce` is NeverBounce, which checks email addresses.","schema":{"type":"string","description":"The provider. `twilio` is Twilio Verify, which sends phone codes. `neverbounce` is NeverBounce, which checks email addresses."}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"phone":{"type":"object","properties":{"source":{"type":"string","enum":["ayeform","own","house"],"description":"Whose keys the check runs on. `ayeform` uses Ayeform's keys and is charged in credits. `own` uses the workspace's own keys and `house` uses keys Ayeform set up for it, and neither is charged. With `own` and no key saved, or a key the provider refuses, the check is skipped and the response comes in not verified or not checked. It never falls back to Ayeform's keys."},"key":{"anyOf":[{"type":"object","properties":{"last4":{"type":"string","description":"The key's last 4 characters."},"accountSid":{"description":"Twilio only.","type":"string"},"serviceSid":{"description":"Twilio only.","type":"string"},"addedAt":{"type":"number","description":"When the key was saved, in milliseconds since the epoch."},"failure":{"anyOf":[{"type":"object","properties":{"at":{"type":"number"},"kind":{"type":"string","enum":["rejected","error"],"description":"`rejected` means the provider refused the key, and checks are skipped until it's replaced. `error` means another error about the key's account or service, and checks are skipped while it lasts."},"message":{"type":"string","description":"The provider's words."}},"required":["at","kind","message"],"additionalProperties":false},{"type":"null"}],"description":"The provider's last problem with the key. It clears once the key works."}},"required":["last4","addedAt","failure"],"additionalProperties":false},{"type":"null"}],"description":"The workspace's own saved key, shown whatever the source so the owner can still remove it after staff change the source. Null when none is saved. Keys Ayeform set up for the workspace are never shown."}},"required":["source","key"],"additionalProperties":false,"description":"Phone codes (Twilio)."},"email":{"type":"object","properties":{"source":{"type":"string","enum":["ayeform","own","house"],"description":"Whose keys the check runs on. `ayeform` uses Ayeform's keys and is charged in credits. `own` uses the workspace's own keys and `house` uses keys Ayeform set up for it, and neither is charged. With `own` and no key saved, or a key the provider refuses, the check is skipped and the response comes in not verified or not checked. It never falls back to Ayeform's keys."},"key":{"anyOf":[{"type":"object","properties":{"last4":{"type":"string","description":"The key's last 4 characters."},"accountSid":{"description":"Twilio only.","type":"string"},"serviceSid":{"description":"Twilio only.","type":"string"},"addedAt":{"type":"number","description":"When the key was saved, in milliseconds since the epoch."},"failure":{"anyOf":[{"type":"object","properties":{"at":{"type":"number"},"kind":{"type":"string","enum":["rejected","error"],"description":"`rejected` means the provider refused the key, and checks are skipped until it's replaced. `error` means another error about the key's account or service, and checks are skipped while it lasts."},"message":{"type":"string","description":"The provider's words."}},"required":["at","kind","message"],"additionalProperties":false},{"type":"null"}],"description":"The provider's last problem with the key. It clears once the key works."}},"required":["last4","addedAt","failure"],"additionalProperties":false},{"type":"null"}],"description":"The workspace's own saved key, shown whatever the source so the owner can still remove it after staff change the source. Null when none is saved. Keys Ayeform set up for the workspace are never shown."}},"required":["source","key"],"additionalProperties":false,"description":"Email checks (NeverBounce)."},"telegramFree":{"type":"boolean","description":"Telegram checks are never charged."}},"required":["phone","email","telegramFree"],"additionalProperties":false,"description":"Secrets are never returned: a key shows only its last 4 characters."}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"}}}}}}