Custom formats
Accept the JSON another system already sends, such as a CRM or survey webhook, by mapping its fields onto the Standard body with a custom format.
A custom format is an incoming URL for a system that cannot send the Standard body. It takes the JSON that system already sends, for example a CRM, helpdesk, or survey webhook. It maps the payload's fields onto the Standard body and stores the response.
Each custom format has its own URL, field mapping, and Store payloads setting. A form can have up to 20 custom formats.
After mapping, the request goes through the same checks, idempotency, and error codes as Standard. A few extra codes cover mapping problems.
Create a format
Open the form and go to Distribution → API → Custom API, then click New format. Make sure the API switch is on and saved.
Enter a Name (up to 120 characters) and a Slug. The slug is part of the URL and cannot change later. It must be unique on the form, up to 100 characters, and use lowercase letters and numbers separated by single hyphens or underscores, for example crm-nps-webhook. standard and validate are reserved.
Choose Store payloads (Off, Failures only, or All), and optionally add a Description (up to 500 characters).
Paste a Sample payload: one real request exactly as your system sends it, up to 32 KB. The editor offers paths from it, and the sample is saved with the format.
Build the Field mapping (below), check it with Preview with the sample, and click Create format.
The format card shows its URL, how many fields are mapped, and its Store payloads setting. From the card menu you can Edit details, Duplicate, Copy URL, or Delete the format. Use the pencil icon to edit the mapping.
Field mapping
Each mapping row fills one field of the Standard body. Add field lists every field you can fill:
| Group | Targets |
|---|---|
| Answers | Every question on the live version, shown with the value shape it takes |
| Other text | The text for the Other option of choice questions that have one |
| Time spent | Milliseconds spent on each question (timeSpentMs) |
| Response | status, externalId, submittedAt, language, user.userName, ipAddress, completionTimeInSeconds, aiEnrichment |
| Device | timezone, deviceOs, deviceOsVersion, app, appVersion, deviceLanguage, userLanguage, urlOrScreenName, deviceType, deviceSize, preferredTheme, browser, browserVersion, countryCode |
| Context variables | The form's context variables (add them in Form design) |
| Source tracking | The form's source tracking parameters (turn on source tracking first) |
A format has 1 to 100 rows, and each target can be mapped only once. Payload fields that are not mapped are ignored. status is completed unless you map it.
Row settings
Open a row's settings (the sliders icon) to choose where its value comes from and how it is converted.
From payload reads a JSONPath from each request, for example $.survey.score. Paths start with $, and the picker suggests paths found in the sample. Fixed value sends the same value on every request, for example "partial" for status.
How the payload value is turned into the field's type. See Conversions.
Used when the path matches nothing in the request. Leave it empty to leave the field out. Defaults apply only to From payload rows.
Single choice, multiple choice, picture choice, and ranking answers only. Each entry maps an incoming value to one of the question's options, with up to 200 entries, and each incoming value can be mapped once. When the map is empty, incoming values are sent as option values as they are. When the map has entries, a value with no entry returns option_not_mapped.
When a path matches several values, they are collected into an array for multiple choice answers and lists. For any other field, several matches return path_matched_many.
Conversions
| Convert | What it does | Extra setting |
|---|---|---|
| As is | Sends the payload value unchanged | — |
| Text | Converts to a string | — |
| Number | Converts to a number, for example "9" → 9 | — |
| True / false | Converts to a boolean | — |
| Date | Parses a date and time | Reads: ISO 8601, Unix seconds, Unix milliseconds, or RFC 2822 |
| List | Splits text into an array, for example "Delivery;Checkout" → ["Delivery", "Checkout"] | Split text on: the separator, , by default |
| Duration | Time spent fields only. Reads numbers in the chosen unit, and text such as 1m 30s, 00:01:30, or PT1M30S | Numbers are in: milliseconds, seconds, or minutes |
A value that cannot be converted returns conversion_failed.
Check a mapping
The editor has three panes under the sample. None of them stores a response.
- Preview: Preview with the sample runs the draft mapping in the API and shows whether the result is valid, or lists each problem with its code.
- Test: paste a body of your own and check it against the mapping in the editor, saved or not, with the same engine the API uses.
- Requests: ready-to-run
curlcommands for this format's submit and validate URLs, with the sample as the body. They use the saved mapping, not unsaved edits.
Endpoints
| Endpoint | What it does |
|---|---|
POST /v2/admin/forms/{formId}/formats/{slug}/responses | Maps the payload, validates it, and stores the response |
POST /v2/admin/forms/{formId}/formats/{slug}/responses/validate | Maps and validates the payload; nothing is stored |
Both take any JSON body up to 100 KB, and both authenticate with an admin API key:
- Send the key in the
X-Api-Keyheader when the sender lets you add headers. - Otherwise, append
?X-Api-Key=YOUR_ADMIN_API_KEYto the URL. See Authentication for the trade-offs. - Send an
Idempotency-Keyheader, or mapexternalIdfrom the payload's event id, so retries from the sender are de-duplicated.
The submit URL answers the same way as Submit Response, with 202 and a responseId. The validate URL answers the same way as Validate Response.
Example: a CRM NPS webhook
Say a CRM posts this payload after each NPS survey, and you cannot change it:
curl -X POST 'https://api.encatch.com/engage-product/encatch/api/v2/admin/forms/3f8a2c1e-7b4d-4e9a-9c61-2d5f8e0b7a14/formats/crm-nps-webhook/responses' \
-H 'X-Api-Key: YOUR_ADMIN_API_KEY' \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"event_id": "evt_1001",
"occurred_at": 1792393591,
"contact": { "email": "jane.doe@example.com" },
"survey": {
"score": "9",
"liked": "Delivery;Checkout",
"comment": "Tracking updates could be more frequent."
}
}'Create a format with the slug crm-nps-webhook and these rows:
| Target | Value | Convert | Option map |
|---|---|---|---|
externalId | $.event_id | As is | — |
submittedAt | $.occurred_at | Date, reads Unix seconds | — |
user.userName | $.contact.email | As is | — |
how-likely-are-you-to-recommend-us | $.survey.score | Number | — |
what-did-you-like-most | $.survey.liked | List, split on ; | Delivery → fast-delivery, Checkout → easy-checkout |
anything-we-could-improve | $.survey.comment | Text | — |
The API turns the payload into this Standard body, then validates and stores it:
{
"status": "completed",
"externalId": "evt_1001",
"submittedAt": "2026-10-19T07:06:31Z",
"user": { "userName": "jane.doe@example.com" },
"answers": {
"how-likely-are-you-to-recommend-us": 9,
"what-did-you-like-most": ["fast-delivery", "easy-checkout"],
"anything-we-could-improve": "Tracking updates could be more frequent."
}
}Because externalId comes from event_id, a webhook retried within 1 hour with the same payload returns the same responseId instead of storing a second response. user.userName must belong to a user created with Identify User. Otherwise, leave that row out to store anonymous responses.
Errors
Custom formats return the same codes as Standard, plus these:
| Code | Status | Meaning |
|---|---|---|
format_not_found | 404 | No custom format with this slug on the form |
path_matched_many | 422 | A JSONPath rule matched several values where the field takes one |
conversion_failed | 422 | The payload value could not be converted to the field's type |
option_not_mapped | 422 | The payload value has no entry in the rule's option map |
question_not_in_form | 422 | A rule targets a question the live version does not have |
In invalidParams, name points at the rule's target field. The most common cause of question_not_in_form is publishing a new form version that removed or replaced a question. Re-open the format after publishing, and remap or remove the affected rows.
Related
Was this page helpful?
