Revvly
Docs
API reference

Forms API.

Build your own front end for a Revvly form. Read the published form, upload files and submit answers from the browser with no key, or submit from your server with an API key. Every submission runs the form’s steps exactly as one from the hosted page would.

#Which to use

From the browser
The three REST routes below. No key; they accept requests from any website, without cookies. Each is addressed by the form’s public id, the one in its hosted link.
From your server
The submitForm GraphQL mutation, with an API key that has the “Can submit forms” permission. Adds retry-safe idempotency keys and a higher daily limit.
iThe form’s Share tab has a prompt for a coding agent that already contains your form’s fields, answer formats and these endpoints. Start there; this page is the reference behind it. Setup and sharing are covered in the Forms guide.

#Read a published form

GEThttps://api.revvly.ai/api/v1/forms/:publicId200 OK

The live version of the form: its questions in order, and the settings for the button, thank-you message and redirect. Edits in Revvly appear here only once they are published, so build from this, not from the builder. A form that is not published, archived or unknown is a 404.

{
  "publicId": "Xk2mQ8vT1nR4pLw7Za",
  "name": "Book a demo",
  "description": null,
  "version": 3,
  "settings": {
    "submitLabel": "Request a demo",
    "successMessage": "Thanks, we will be in touch within a day.",
    "redirectUrl": null
  },
  "fields": [
    {
      "id": "fld_1", "key": "email", "type": "EMAIL", "label": "Work email",
      "description": null, "placeholder": "[email protected]", "required": true,
      "options": [], "maxLength": null, "urlParam": null,
      "maxFiles": null, "maxFileSizeMb": null, "accept": null
    },
    {
      "id": "fld_2", "key": "team_size", "type": "DROPDOWN", "label": "Team size",
      "description": null, "placeholder": null, "required": false,
      "options": [{ "id": "o1", "label": "1-10" }, { "id": "o2", "label": "11-50" }],
      "maxLength": null, "urlParam": null,
      "maxFiles": null, "maxFileSizeMb": null, "accept": null
    },
    {
      "id": "fld_3", "key": "utm_source", "type": "HIDDEN", "label": "Source",
      "description": null, "placeholder": null, "required": false,
      "options": [], "maxLength": null, "urlParam": "utm_source",
      "maxFiles": null, "maxFileSizeMb": null, "accept": null
    }
  ]
}

#Answer formats

answers is an object keyed by each field’s key. Leave out questions the person did not answer. Unknown keys are ignored.

SHORT_TEXT, LONG_TEXT
A string, within maxLength when set.
RICH_TEXT
A string of Markdown or simple HTML. Scripts and styles are removed.
EMAIL
A valid email address.
PHONE
5 to 40 characters: digits, spaces, dots, slashes, hyphens and parentheses, with an optional + at the start only.
URL
A web address. https:// is added when missing.
NUMBER
A number, or a string holding one.
DATE
A string, YYYY-MM-DD.
SINGLE_CHOICE, DROPDOWN
One option label, exactly as listed in options.
MULTIPLE_CHOICE
An array of option labels.
CONSENT
true when ticked. Leave it out when not.
HIDDEN
A string, usually read from the page URL parameter named in urlParam.
FILE_UPLOAD
An array of fileIds from the uploads endpoint, at most maxFiles.
HEADING, PARAGRAPH
Layout only. Send nothing.

#Submit answers

POSThttps://api.revvly.ai/api/v1/forms/:publicId/submissions201 Created
answers
Required. See Answer formats.
sourceoptional
Leave it out. Submissions to this route are recorded as coming from Custom, a form you built into your own site, so you can tell them from the share link and the embed in Submissions and in the CSV. (Revvly’s own pages send HOSTED or EMBED; submissions from your server through submitForm are API.)
websitespam trap
Render a text input named website that people cannot see or reach (off-screen, tabindex="-1", aria-hidden="true", not display: none) and send its value as it is. A filled one gets the normal success response and is silently discarded.
pageUrl, referreroptional
window.location.href and document.referrer, shown with the submission.
Request
POST https://api.revvly.ai/api/v1/forms/Xk2mQ8vT1nR4pLw7Za/submissions
Content-Type: application/json

{
  "answers": {
    "email": "[email protected]",
    "team_size": "11-50",
    "utm_source": "newsletter"
  },
  "website": "",
  "pageUrl": "https://example.com/demo?utm_source=newsletter",
  "referrer": "https://www.google.com/"
}
Success
201 Created
{
  "submissionId": "6705f2c1a4b3e2d1c0f9e8d7",
  "successMessage": "Thanks, we will be in touch within a day.",
  "redirectUrl": null
}

Replace the form with successMessage, or go to redirectUrl when it is set.

Invalid answers
400 Bad Request
{
  "message": "Some answers need attention.",
  "fieldErrors": {
    "email": "Enter a valid email address.",
    "team_size": "Pick one of the listed options."
  }
}
400
Show each fieldErrors message under its question, keep what the person typed, and move focus to the first one.
404
The form is not published right now.
429
Too many submissions from this visitor (30 a minute) or to this form today (1,000). Ask the person to try again later.

#Upload a file

POSThttps://api.revvly.ai/api/v1/forms/:publicId/uploads201 Created

For FILE_UPLOAD questions. Ask for an upload address per file, upload the bytes to it, then submit the returned fileIds as the answer. The type is decided by the file name’s extension; a type or size the question does not accept is a 400 here. When the form is submitted, each file is checked for its real type and scanned before it is kept, and a file that fails comes back as that question’s fieldErrors entry. Files never submitted are deleted after a day.

POST https://api.revvly.ai/api/v1/forms/Xk2mQ8vT1nR4pLw7Za/uploads
Content-Type: application/json

{ "fieldKey": "cv", "filename": "ada-lovelace.pdf", "size": 482113 }
201 Created
{
  "fileId": "6705f2c1a4b3e2d1c0f9e8a1",
  "uploadUrl": "https://…signed upload address…",
  "contentType": "application/pdf",
  "maxBytes": 31457280,
  "expiresAt": "2026-10-09T10:15:00.000Z"
}

// Then, from your server:
PUT <uploadUrl>
Content-Type: application/pdf      <- exactly the contentType returned
<the file's bytes>

// And submit the id as the answer:
"answers": { "cv": ["6705f2c1a4b3e2d1c0f9e8a1"] }
iThe visitor’s browser can send the file straight to the upload address from your site. If the browser reports a cross-origin (CORS) error on that request, do the upload step from your server instead: it receives the file, asks for the address and uploads it. Or keep file questions on the hosted or embedded form.

#submitForm (GraphQL, from your server)

MUTATIONhttps://api.revvly.ai/api/v1/graphqlFormSubmissionReceipt

Submit to one of your workspace’s published forms with an API key sent as Authorization: Bearer <key>. The key needs the “Can submit forms” permission (Settings, API keys); a key with only write access is refused, and a forms key can do nothing else. Never put this key in browser code.

mutation Submit($input: SubmitFormInput!) {
  submitForm(input: $input) {
    submissionId
    duplicate
    successMessage
    redirectUrl
  }
}
Variables
{
  "input": {
    "publicId": "Xk2mQ8vT1nR4pLw7Za",
    "answers": { "email": "[email protected]", "team_size": "11-50" },
    "idempotencyKey": "contact-form-7f3a9c",
    "pageUrl": "https://example.com/demo"
  }
}
publicId or formId
Exactly one. The form is only ever looked up in the key’s own workspace.
idempotencyKeyoptional
Up to 128 characters, unique per submission. Retrying with the same key returns the first submission with duplicate: true instead of storing another, so a timed-out request is safe to repeat.
Errors
BAD_REQUEST with extensions.fieldErrors for invalid answers, NOT_FOUND for a form that is not published, FORBIDDEN without the permission or without Forms, and RATE_LIMITED past 10,000 a day per form.
// server.js. The key lives here and nowhere else.
const res = await fetch('https://api.revvly.ai/api/v1/graphql', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.REVVLY_FORMS_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ query: SUBMIT_FORM, variables }),
});
const { data, errors } = await res.json();
const error = errors?.[0];
if (error?.extensions?.code === 'BAD_REQUEST') {
  const fieldErrors = error.extensions.fieldErrors;
  // Invalid answers carry { "<field key>": "<message>" }: show each under its question.
  // A BAD_REQUEST without it is a problem with the call itself (both formId and
  // publicId sent, an idempotencyKey over 128 characters): the message says which.
  return fieldErrors ? showErrors(fieldErrors) : showProblem(error.message);
}

Related