# Web forms

> How to create tickets from a form on your website with the web connector, map form fields to attributes, restrict domains, add reCAPTCHA and read the response.

Canonical HTML page: https://conclude.io/doc/apps/integrations/web-form/
Last updated: 2026-10-27

---

A web connector lets a form on your website create tickets in an installed app. People without a chat account can then submit requests. This page is for the builder who owns both the app and the form. The key list is in [Connectors](https://conclude.io/doc/reference/connectors/#web-connector), and the request contract in [Endpoints](https://conclude.io/doc/reference/endpoints/#form).

## Add a web connector

The connector is a top-level `webConnector` key in the app YAML, and you edit it on the [Expert tab](https://conclude.io/doc/apps/editor/expert/). Leave `id` empty. Conclude generates an ID when it is missing, or when it belongs to another channel.

```yaml
webConnector:
  id:
```

`disabled: true` stops submissions and returns `disabled_error` without removing the configuration.

## Submit the form

Send a `POST` to `/v1/form/<cid>`, where `<cid>` is the connector ID. The older `/v1/activity.form/<cid>` path reaches the same handler.

The body is either `application/json` or `application/x-www-form-urlencoded`. JSON must be a flat object of string values. In a form-encoded body, Conclude reads the first value of each field. Any other content type is rejected with HTTP 400, and so is malformed JSON. Cross-origin requests are allowed.

## Map fields to attributes

A form field named like an attribute needs no mapping. An `attributeMap` entry renames a form field (`external`) to an attribute (`internal`). Its `defaultValue` applies when the field is empty or absent. A default may use macros that read other form fields.

```yaml
webConnector:
  id: <generated>
  attributeMap:
    - internal: title
      defaultValue: Message from ${name} ${email}
    - external: message
      internal: body
      defaultValue: <no message>
```

Values are cut at 3000 characters. A mandatory string or text attribute that is still empty is set to `-`. Every value then passes input validation. If one fails, the request returns `validation_error`.

**Two spellings the product does not read.** The reference's [attribute map syntax](https://conclude.io/doc/reference/connectors/#attribute-map-syntax) shows `from` and `to`, but the product reads `external` and `internal`. The bundled `contact-form.yaml` spells the key `attribute_map`, which the product ignores. As a result, `message` never reaches `body` and `title` becomes `-`. Rename the key to `attributeMap` when you adapt that app.

## Restrict domains and rate

`domain` is a comma-separated list of hostnames. When you set it, the request's `Origin` header must match one of them, or a subdomain of one. The match ignores case. Any other origin fails with `domain_error`. A script can send any `Origin`, so this is a filter, not security.

`settings.rateLimit` caps ticket creation for the whole app, and you write it in `N/min` form. The default is 10 per minute, and saving clamps the value to between 1 and 120. See [Settings](https://conclude.io/doc/reference/yaml/#settings). Over the limit, the endpoint returns `rate_limit_error`. A submission identical to one received in the last five minutes returns `duplicate_request_error`. A general per-client limit also applies.

## Block bots with reCAPTCHA v3

Run `/c app secrets` in the app's channel and store your Google reCAPTCHA v3 private key. The dialog works in Slack only, because Microsoft Teams has no secrets dialog. Your page must obtain a token with the matching site key and post it as a field named `recaptcha`.

Once a key is stored, every submission must carry a token. A missing token returns `recaptcha_error` and sends a system alert to the app. A token that Google rejects or scores below 0.5 fails the same way, and so does a token sent when no key is stored.

## Log accepted and rejected submissions

`logMessage` and `logJunk` are optional chat messages. Each one has a `to` field, which holds a role expression or a channel, and a `body` field, where form values are macros.

`logMessage` fires with the mapped values for each submission that passes reCAPTCHA and validation. It fires before the duplicate and rate checks. `logJunk` fires with the raw fields when reCAPTCHA or validation fails.

## Read the response

A created ticket returns:

```json
{"status": "ok", "xid": "<external ticket id>"}
```

`xid` is the new ticket's external ID. Posting `xid` back to the form endpoint does not update the ticket.

Otherwise `status` is one of `disabled_error`, `validation_error`, `recaptcha_error`, `rate_limit_error`, `duplicate_request_error`, `not_found_error` or `internal_error`. These still arrive with HTTP 200, so check the body. An unknown connector ID (`config_error`) and `domain_error` return HTTP 400 instead.

## Related pages

- [Macros and roles](https://conclude.io/doc/reference/macros-and-roles/) for the macro syntax in default values and log messages

