Developer Reference

This page is for developers who connect OmniForm to their own systems. You may need to build two endpoints:

  • A choices endpoint that returns the choices for a dropdown, if your admin loads choices from your API.

  • A webhook that receives each form submission.

How admins set up forms is described in Build a Form.

Choices endpoint

The Nested API Dropdown, and Dropdown and Multi-select with From API as their choices source, load their choices from an endpoint you provide.

Response format

Return a JSON array of objects with label and value:

[
{ "label": "Option 1", "value": "option_1" },
{ "label": "Option 2", "value": "option_2" },
{ "label": "Option 3", "value": "option_3" }
]
  • label is the text the agent sees.

  • value is what your webhook receives for that choice, for example an ID from your system.

  • Return the array itself, not an object that wraps it (such as { "data": [...] }). Other shapes are not displayed correctly.

  • For the Nested API Dropdown, if the request fails or the response is not valid JSON, the agent sees Failed to get options.

For Dropdown and Multi-select with From API as their choices source:

  • label and value may be text or numbers. value is always sent to your webhook as text.

  • Items with the same value are shown once. Items with an empty value are skipped.

  • If the request fails, or the response is not an array of label/value objects, the agent sees Couldn't load choices. Close and reopen this list to try again. They can still remove choices they already selected and submit the form.

  • If the response is an empty array, the list shows that there is no data.

  • A choice the agent selected earlier that your endpoint no longer returns stays in the field, marked Not in list. It is still sent to your webhook unless the agent removes it, and it counts toward the Choice Limit.

How the request is sent

  • Method: GET, with the headers and query parameters configured on the field.

  • Added automatically: the query parameters room_id (the room the agent is working in) and app_id (your Omnichannel App ID). Test API in the builder does not send room_id, because no room is open.

  • From the agent's browser: the request is sent directly from the browser of the agent who opens the form, not from Qiscus servers. Your endpoint must be reachable from the agent's network and must allow cross-origin requests (CORS) from the Omnichannel agent dashboard, including the headers configured on the field.

  • Headers are visible: because the request runs in the browser, the configured header values (for example an API key) can be seen with the browser's developer tools by anyone who opens the form. They are also included in the webhook body, in the field's payload. Use a key that can only read this list.

  • When it is called: when the field is shown to the agent. Searching filters the choices already loaded and does not call your endpoint again. For Dropdown and Multi-select, if the call fails, the agent can close and reopen the list to retry.

  • Nested levels: each level of a Nested API Dropdown calls its own endpoint: the first level when the field is shown, and each next level when the choice in the level before it changes. The admin passes that choice as a query parameter using a placeholder such as {[Category]}, which is replaced with the selected value. Until the previous level has a choice, the agent sees Please complete the previous field first.

Changing the source of a saved field

An admin can switch a saved Dropdown or Multi-select field between Manual and From API at any time. The builder does not ask for confirmation. After the form is saved, new submissions change as follows:

  • Dropdown: value changes from the choice label to the value from your API, for example from "Shipping" to "CAT-14". If your system stores or matches the label, read selected_option.label instead.

  • Multi-select: value keeps the same shape, but each value inside it changes from the label to the value from your API, for example from { "label": "Price inquiry", "value": "Price inquiry" } to { "label": "Price inquiry", "value": "TAG-104" }.

  • Payload keys: source changes to "api", api is added, and dropdowns is no longer sent.

  • Switching back to Manual reverses all of the above.

Submissions that were already sent are not changed. Agents who have the form open while the change is saved see This form was just updated. Check your answers, then submit again. when they submit; their answers to unchanged fields are kept.

Webhook

Each submission is sent to the Webhook URL configured on the form's Submit Button.

Request

POST {{WEBHOOK_URL}}
{
"Content-Type": "application/json",
"User-Agent": "custom-form-addon",
"{{Custom-Header-Key}}": "{{Custom-Header-Value}}"
}

The form can define up to 5 custom headers. Use them to verify that a request comes from OmniForm.

Body

attachments, conversation_file, additional_info, customer_properties, and room_info are included only when the matching option is turned on in the form. The sample is shortened: each field's payload also carries that field's settings, such as its choice list or endpoint configuration.

{…}

Key

Description

name, description

The form's name and short description

author

The agent who submitted the form: id, name, email, role, and division, as provided by Omnichannel

fields

One object per field, in form order. See Field values by type

attachments

Files the agent attached

conversation_file

Link to the conversation history of the current session

additional_info

The room's additional information. null when the form appends it to a Multi-line Text field instead

customer_properties

The selected customer properties

room_info

The selected room details. Only the items ticked in the form are included

Each field's answer is in fields[].payload.value:

Field type

type

payload.value

Single-line Text

single_line

Text

Multi-line Text

multi_line

Text

Phone Number

phone_number

Text with digits only, country code first, no + ("628123456789")

Checkbox

checkbox

A JSON-encoded string of the selected labels ("[\"Email\",\"WhatsApp\"]"). Parse it as JSON to get the list

Dropdown

dropdown

Text: the selected choice. With a From API source, the value returned by your endpoint

Nested API Dropdown

nested_api_dropdown

Array with one object per level: the level's identifier and label, and the selected value

Date and Time

date

ISO 8601 timestamp in UTC ("2026-10-13T03:00:00.000Z")

Number and Decimal

number / decimal

Number. The type is decimal when the field allows decimals

Multi-select

multi_select

Array of the selected choices as {label, value} objects, in list order. [] when an optional field is left empty

  • For Checkbox and Dropdown, payload.choices / payload.dropdowns contain only the selected choices.

  • identifier is a stable key for each field. Map fields by identifier rather than label, because a label can be renamed.

Dropdown and Multi-select payload keys

Key

Fields

Description

source

dropdown, multi_select

"manual" or "api". Dropdown fields created before Choices source existed have no source; treat them as "manual"

api

dropdown, multi_select with source: "api"

Endpoint configuration: url, method ("GET"), headers, query_params

min_selections, max_selections

multi_select

The Choice Limit. Present only when set

selected_option

dropdown

{ "label", "value" } of the selected choice, for both sources. For Manual, value equals label. Not present when an optional dropdown is left empty

dropdowns

dropdown, multi_select with source: "manual"

The selected choices, in the same format as before

Multi-select sends the same {label, value} shape for both sources. For a Manual source, each value equals its label.

{
"type": "multi_select",
"label": "Conversation Tags",
"is_required": true,
"identifier": "conversation_tags-k3x9",
"payload": {
"source": "api",
"api": {
"url": "https://api.example.com/omniform/tags",
"method": "GET",
"headers": { "X-Api-Key": "{{YOUR_READ_ONLY_KEY}}" },
"query_params": {}
},
"min_selections": 1,
"max_selections": 4,
"value": [
{ "label": "Price inquiry", "value": "TAG-104" },
{ "label": "Account locked", "value": "TAG-221" }
]
}
}
{
"type": "multi_select",
"label": "Contact Reason",
"is_required": true,
"identifier": "contact_reason-p5q6",
"payload": {
"source": "manual",
"max_selections": 4,
"value": [
{ "label": "Price", "value": "Price" },
{ "label": "Account", "value": "Account" }
],
"dropdowns": [
{ "label": "Price", "order": 1, "selected": true },
{ "label": "Account", "order": 2, "selected": true }
]
}
}

Dropdown also sends payload.selected_option with both the label and the value of the selected choice. payload.value stays a plain string, so existing integrations keep working.

{
"type": "dropdown",
"label": "Category",
"is_required": false,
"identifier": "category-r7s8",
"payload": {
"source": "api",
"api": {
"url": "https://api.example.com/omniform/categories",
"method": "GET",
"headers": {},
"query_params": {}
},
"value": "CAT-14",
"selected_option": { "label": "Shipping", "value": "CAT-14" }
}
}

Responding to OmniForm

Respond with HTTP status 200 once you have accepted the submission. Any other status, including 201 or 204, is treated as a failure, and the agent sees that the submission failed.

To show your own error message to the agent, return one of these structures. If more than one is present, OmniForm uses the first match in this order: error.data.message, error.message, message.

Preferred:

{
"message": "Your error message here",
"details": [
{ "field": "field_name", "message": "Field-specific error message" }
]
}

Alternative 1:

{
"error": {
"message": "Your error message here",
"details": [
{ "field": "field_name", "message": "Field-specific error message" }
]
}
}

Alternative 2:

{
"error": {
"data": {
"message": "Your error message here",
"details": [
{ "field": "field_name", "message": "Field-specific error message" }
]
}
}
}
  • message is shown to the agent.

  • details is optional and is not shown to the agent. Use it for debugging.

If none of these structures is returned, the agent sees:

unexpected error when sending data to webhook url <your-url> status code: <status-code>

When a submission fails

  • OmniForm sends each submission once and does not retry it automatically.

  • The agent sees the error message, and their answers stay in the form, so they can correct them and submit again. Each attempt is a new request to your webhook.

  • The agent waits for your response before seeing the result, so respond quickly.

If your service has already processed a submission but responds with a status other than 200, the agent will likely submit it again. Return 200 as soon as you accept the data, and make sure a repeated submission does not create a duplicate record.

Troubleshooting

The agent sees "unexpected error when sending data to webhook url …"

Your webhook responded with a status other than 200, or with a body OmniForm does not recognize. Check the status code in the message, and see Responding to OmniForm.

My webhook returns 201 but the agent sees an error

Only 200 counts as success. Change the status your webhook returns.

Choices from my API don't load

Check that the endpoint allows cross-origin requests (CORS) from the Omnichannel agent dashboard, and that it returns a plain JSON array of {label, value} objects. Use Test API in the builder to see the response.

The Checkbox value is a string, not an array

That is the Checkbox format. Parse payload.value as JSON to get the list of selected labels.

Dates are a few hours off

Date and Time values are sent in UTC. Convert them to your timezone.