Skip to content

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.

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 curl are indistinguishable, and Origin and Referer are 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 @AuraEnabled ExperienceEngine controller over the platform’s session-bound, CSRF-protected transport.

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.

  1. Create a dedicated integration user on the Minimum Access - Salesforce profile, never a person.
  2. Assign the packaged Sliick Forms Integration permission 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 no Form_Submission__c access at all.
  3. 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.
  4. 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.
FlowWho it is forWhat you supply
Client CredentialsYour own backend calling your own orgConsumer key and secret, plus a Run As user set on the same Policies screen. That user is the identity every call runs as
JWT BearerSliick Edge, when you use hosted public formsNo 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.

All are deployed under the sliick namespace and all require an Authorization: Bearer <token> header.

MethodRoutePurpose
GET or POST/services/apexrest/sliick/experience/*Fetch a form’s manifest bundle
POST/services/apexrest/sliick/submitStage a submission
POST/services/apexrest/sliick/uploadSecure file intake
POST/services/apexrest/sliick/resumeMint 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>"}.

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.reasonMeaning
FORM_NOT_FOUNDNo such form, or not visible to you
FORM_NOT_OPENPaused, outside its schedule, or at its cap
NOTHING_TO_PREFILLNo element maps to a field you can read on that record
NO_DELIVERY_ADDRESSThe org has no public address for this form. Nothing was written
INVALID_RECORD_IDNot a valid Id
INSUFFICIENT_ACCESSAssign Sliick_Forms_Operator or Sliick_Forms_Admin
UNKNOWN_FIELDA valueOverrides key names no element on this form, or one that cannot take a value
MATCH_FIELD_LOCKEDA valueOverrides key names the form’s matching element, which cannot be overridden
INVALID_REQUESTvalueOverrides could not be read, or the form’s external id is too long to key a link
WRITE_FAILEDThe 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 valueOverrides rather than refusing it, because the envelope tolerates unknown keys by design, and listOperations cannot tell you which version you have. Check the installed package version in Setup, Installed Packages, or with sf package installed list.

/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.

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.

  • Upload budget. A form accepts up to 250 MB per integration in any 24 hours over /upload. Past that you get 429 with Upload_Budget_Exceeded and a Retry-After header. See File uploads.
  • Requests turned away are recorded and shown on the dashboard. See The Admin Dashboard.
  • Repeated link ids. A /submit under 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 Accepted with no submissionId.

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.

CodeMeaning
200Manifest served
201 ReceivedSubmission staged synchronously. The body carries submissionId
202 AcceptedSubmission published to the async path. No submissionId: track it by your own correlationId
202 Stored_For_ReviewSent with keepIfInvalid: true and failed validation. Stored, not processed until an admin accepts it. errors lists the messages. No submissionId
400Invalid_Request or Validation_Failed. The errors array names the failing fields
401Signature_Invalid, or a required signature was missing
429Upload_Budget_Exceeded: the form’s daily upload budget is spent. Wait for Retry-After
403Form_Closed, or, if the whole route 403s, the caller does not hold that handler class
404Form_Not_Found: no visible definition with that external ID
500Server 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.

A 403 means something different at each stage. Work down this list:

  1. 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.
  2. 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 Integration set, not a clone of it.
  3. A 403 carrying {"status": "Form_Closed"}. Authentication worked. The form is paused, outside its schedule window, or at its response cap.
  4. A 404 on /experience for a form you can see in the builder. You are calling as the org’s designated edge integration user, and that identity gets 404 for any form without Allow on your public Sliick Edge address ticked. Use a different user, or tick the box.