The Sliick Forms REST API
This page is a developer reference. It names Apex classes, permission sets and HTTP contracts, and it is for someone building a server-to-server integration against Sliick Forms. Admins configuring a form or a public site do not need it: see Choosing how to host a form instead.
What the API is for
Section titled “What the API is for”Sliick Forms exposes four Apex REST routes for authenticated integrations: fetch a form’s manifest, stage a submission, upload a file, and mint a save-and-continue token. They are the surface for a server-to-server client with an identity of its own.
They are not a guest surface, and no packaged permission set grants them to a guest. Apex REST is anonymous HTTP: a request from a browser and one from
curlare indistinguishable, andOriginandRefererare set by the caller. Granting these classes to a site’s shared guest identity would publish submit, upload and resume to the open internet. A browser on an Experience Cloud page uses the Sliick Experience Form component instead, which calls the@AuraEnabledExperienceEnginecontroller over the platform’s session-bound, CSRF-protected transport.
Becoming an authenticated caller
Section titled “Becoming an authenticated caller”The package ships an External Client App called Sliick Forms Integration, already registered by the install: there is no consumer key, secret or callback URL for you to create. It does not work until an admin edits its Policies screen, and that is the reason a first attempt fails.
- Create a dedicated integration user on the Minimum Access - Salesforce profile, never a person.
- Assign the packaged
Sliick Forms Integrationpermission set to that user, and do not clone it: an upgrade updates a packaged set in place and can never add a grant to your copy. The set carries API Enabled, Apex class access to the four handlers plus the inbound provisioning callback, read-only access to form definitions and elements, and Create on both staging platform events. It grants noForm_Submission__caccess at all. - Setup, External Client App Manager, Sliick Forms Integration, Policies, Edit. Set Permitted Users to Admin approved users are pre-authorized and pre-authorise the integration user’s own profile or a permission set that user holds.
- Enable the grant flow you intend to use, on the same screen. Enabling a flow and pre-authorising a user are separate settings and you need both.
| Flow | Who it is for | What you supply |
|---|---|---|
| Client Credentials | Your own backend calling your own org | Consumer key and secret, plus a Run As user set on the same Policies screen. That user is the identity every call runs as |
| JWT Bearer | Sliick Edge, when you use hosted public forms | No key material: Sliick holds the private key. You still enable the flow and name the integration user as its subject |
JWT Bearer against the packaged app is Sliick’s transport, not yours. The certificate an assertion is verified against lives in Dev-Hub-owned settings you cannot edit, so you cannot sign an assertion the packaged app will accept. To use JWT Bearer for your own backend, create your own External Client App in your org and point it at the same routes: the handlers are global and do not care which app the token came from.
Revoking access is one step: unassign the permission set from the user, or deactivate the user. Both take effect on the next call.
The four routes
Section titled “The four routes”All are deployed under the sliick namespace and all require an Authorization: Bearer <token> header.
| Method | Route | Purpose |
|---|---|---|
GET or POST | /services/apexrest/sliick/experience/* | Fetch a form’s manifest bundle |
POST | /services/apexrest/sliick/submit | Stage a submission |
POST | /services/apexrest/sliick/upload | Secure file intake |
POST | /services/apexrest/sliick/resume | Mint a save-and-continue token |
formExternalId may be supplied in the path, as a query parameter, or in a POST body.
A fifth route, POST /services/apexrest/sliick/callback, exists only so a hosted-edge connection can deliver credentials to the org. It refuses anything that is not a correctly signed delivery for that org, and you never call it.
/upload takes formExternalId, fileName and base64, and answers 201 with {"success": true, "uploadRef": "…", "fileName": "…", "errors": []}. Put the uploadRef in the submission payload for that field.
/resume takes formExternalId and state, where state is {"values": {…}, "rows": {…}, "page": 0}, and answers 201 with {"success": true, "resumeRef": "<token>"}.
Record links from Apex and Flow
Section titled “Record links from Apex and Flow”Minting a record link is not one of the REST routes. It is an in-org Apex call that runs as the calling user, so it seeds only what that user can already read on the record:
String response = sliick.FormsApi.invoke('createRecordLink',
'{"params":{"formExternalId":"intake","recordId":"a0X...",'
+ '"valueOverrides":{"email":"a@b.com","seats":2}}}');
On success:
{ "url": "https://forms.example.com/intake?sliickPrefill=<token>",
"token": "<token>", "expiresAt": "2026-09-19T09:00:00.000Z",
"formExternalId": "intake", "recordId": "a0X..." }
recordId takes 15 or 18 characters; both key the same link. valueOverrides is optional: a JSON object of element API name to value, where each value is a string, a number, true/false, or a list of strings. A value replaces what the record would supply, or adds an element the record does not map. Omit it for the fully automatic record-derived map.
Design around the three rules the link itself carries: a 7-day expiry that is not configurable, spent on submit, and one live link per form per record for the whole org, so calling this again rotates the nonce on the same row and the previous link stops working immediately, along with any answers saved behind it.
Refusals come back as success: false with an envelope code and a finer result.reason. Branch on the reason, never on message text:
result.reason | Meaning |
|---|---|
FORM_NOT_FOUND | No such form, or not visible to you |
FORM_NOT_OPEN | Paused, outside its schedule, or at its cap |
NOTHING_TO_PREFILL | No element maps to a field you can read on that record |
NO_DELIVERY_ADDRESS | The org has no public address for this form. Nothing was written |
INVALID_RECORD_ID | Not a valid Id |
INSUFFICIENT_ACCESS | Assign Sliick_Forms_Operator or Sliick_Forms_Admin |
UNKNOWN_FIELD | A valueOverrides key names no element on this form, or one that cannot take a value |
MATCH_FIELD_LOCKED | A valueOverrides key names the form’s matching element, which cannot be overridden |
INVALID_REQUEST | valueOverrides could not be read, or the form’s external id is too long to key a link |
WRITE_FAILED | The org refused the write: a validation rule or a trigger |
From Flow, the same capability is the invocable Create Sliick Form Record Link, under the Sliick Forms category. Inputs are formExternalId, recordId and the optional valueOverridesJson, built in a Plain text Text Template because rich text encodes the quotes. Outputs are success, url, expiresAt, errorCode and errorMessage, positionally aligned with the inputs. It never throws, so one refused record costs the rest of the collection nothing, and a collection past the transaction’s query budget comes back as BATCH_TOO_LARGE rather than a governor exception. Ten to twenty per interview is comfortable.
An older package ignores
valueOverridesrather than refusing it, because the envelope tolerates unknown keys by design, andlistOperationscannot tell you which version you have. Check the installed package version in Setup, Installed Packages, or withsf package installed list.
Signed requests for edge-served forms
Section titled “Signed requests for edge-served forms”/submit, /upload and /resume refuse an unsigned request with 401 and Missing X-Sliick-Signature header when the form they name is served on your Sliick Edge address, meaning Allow on your public Sliick Edge address is ticked on that form.
- The rule is per form, not per org. A form that is not on your edge address is unaffected, even in an org that uses the edge for other forms.
- You cannot produce the signature yourself. It is computed over an org-held secret in a protected custom setting, invisible to subscribers by design. Send that traffic through Sliick Edge, which signs on your behalf and is also where rate limiting and bot filtering run.
- Unaffected: forms not on your edge address, orgs with no edge connection, Experience Cloud forms, and
/experience, which has never verified signatures. - A signature you do send is always checked, whatever the form’s setting.
Keeping a response that fails validation
Section titled “Keeping a response that fails validation”Add "keepIfInvalid": true to a /submit request and a response that fails the form’s validation is stored instead of refused. You get 202 with the status Stored_For_Review and the messages in errors, and no submissionId. The response carries a Failed validation badge in Sliick Responses, is not processed and its webhook is held until an admin accepts it. See The responses console. Without the key, a failing response is refused with 400 Validation_Failed as before.
Limits and records
Section titled “Limits and records”- Upload budget. A form accepts up to 250 MB per integration in any 24 hours over
/upload. Past that you get429withUpload_Budget_Exceededand aRetry-Afterheader. See File uploads. - Requests turned away are recorded and shown on the dashboard. See The Admin Dashboard.
- Repeated link ids. A
/submitunder a link id already used on that form, with different answers, is stored and flagged Suspected duplicate. An exact repeat is recorded once. - Identities you granted Create on Form Submission also need Read on the Suspected Duplicate and Answers Digest fields, and Read on Failed Validation and Validation Errors if they process responses. Without the first pair, a submission is still recorded, but the answer is
202 Acceptedwith nosubmissionId.
Flow actions over REST. Sliick Forms Admin and Sliick Forms Operator can call the record-link and process Flow actions through the REST API. Salesforce’s Run Flows permission is still yours to grant. Trigger Secure Upload Link never did anything and is labelled deprecated.
Status codes
Section titled “Status codes”| Code | Meaning |
|---|---|
200 | Manifest served |
201 Received | Submission staged synchronously. The body carries submissionId |
202 Accepted | Submission published to the async path. No submissionId: track it by your own correlationId |
202 Stored_For_Review | Sent with keepIfInvalid: true and failed validation. Stored, not processed until an admin accepts it. errors lists the messages. No submissionId |
400 | Invalid_Request or Validation_Failed. The errors array names the failing fields |
401 | Signature_Invalid, or a required signature was missing |
429 | Upload_Budget_Exceeded: the form’s daily upload budget is spent. Wait for Retry-After |
403 | Form_Closed, or, if the whole route 403s, the caller does not hold that handler class |
404 | Form_Not_Found: no visible definition with that external ID |
500 | Server fault, or Validation_Unavailable where the server could not load the form’s elements to validate against. Retry with backoff rather than treating it as a rejection of the payload |
Which path you get is decided by your identity, not by a parameter. A caller with Create on Form_Submission__c takes the synchronous path and gets 201 with an ID; a caller without it, which includes every packaged permission set, takes the asynchronous path and gets 202. Treat 202 as the normal success case and do not depend on receiving a submissionId.
Reading a 403
Section titled “Reading a 403”A 403 means something different at each stage. Work down this list:
- The token request is refused. Nothing of ours was called yet. Almost always the Permitted Users setting, or a user whose profile or permission set was never pre-authorised. Check the user you actually authenticate as: for Client Credentials that is the Run As user.
- The whole route 403s, with no JSON body of ours. The identity behind the token does not hold that handler’s Apex class. Assign the packaged
Sliick Forms Integrationset, not a clone of it. - A 403 carrying
{"status": "Form_Closed"}. Authentication worked. The form is paused, outside its schedule window, or at its response cap. - A
404on/experiencefor a form you can see in the builder. You are calling as the org’s designated edge integration user, and that identity gets404for any form without Allow on your public Sliick Edge address ticked. Use a different user, or tick the box.