> ## Documentation Index
> Fetch the complete documentation index at: https://veriqa.app/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a channel adapter in any language

> Plug in a channel Veriqa does not ship by running it as a separate service in any language: Veriqa passes it the work over HTTP, and you add the channel in configuration.

You can plug in a channel Veriqa does not ship by running it as a separate service in any language. The **HTTP
channel** is an adapter shipped with Veriqa that stands in for yours inside the host and forwards the
work over HTTP to an **external adapter** — a service you write yourself and run next to Veriqa or
inside your own perimeter. You need no .NET and no host code: you add the channel in configuration.

An adapter that runs inside a .NET host is covered in
[Build a channel adapter in C#](/docs/guides/custom-channel-adapter).

<Note>
  The wire contract between Veriqa and an external adapter is version `1.0` and ships with the status
  **`preview`**. Until the contract is declared stable, it may still change.
</Note>

## How the traffic flows

* **The platform talks to your adapter, not to Veriqa.** Your adapter receives the platform's webhooks
  or polls it, verifies them and answers the platform's challenge handshake itself. Veriqa maps no GET
  route for an HTTP channel.
* **Your adapter posts events to Veriqa** on the routes every channel has,
  `/api/channels/{channelType}/webhook` or `/api/channels/{channelType}/webhook/{tenant}`. An event of
  a transaction goes to the route of **that transaction's tenant** — the one the `tenant` field of the
  `deep-link` and `send-prompt` calls of that transaction carried; `null` means the route without a
  tenant segment. Veriqa does not tell you the tenant again: keep the mapping "transaction → tenant"
  yourself.
* **Your public entry is yours to protect.** Veriqa's rate limit guards its own routes; the
  entry your adapter opens to the platform needs protection against junk traffic of its own.
* **Veriqa calls your adapter at one address**, `Http:BaseUrl`, for everything it needs from the
  channel.
* **The platform credentials stay with your adapter.** Veriqa never asks for them and never stores
  them.
* **One registration is one external adapter**: one address, one transport secret, one set of channel
  facts. Without `Http:Tenant` it serves every tenant of the installation. A tenant that brings an
  implementation of its own gets a registration of its own, under its own name (for example
  `shop-a-zalo`), with `Http:Tenant` naming that tenant.
* **The standard Veriqa image always registers HTTP channels**: a channel is added with environment
  variables and a restart of the container, without rebuilding the image. An embedded host references
  the `Veriqa.Core.ChannelAdapter.Http` package and adds one call:

```csharp Program.cs theme={null}
authServer.AddChannelAdapters(adapters => adapters.AddHttpChannels());
```

## Configuration

Every section `Veriqa:Channels:{channelType}` with an `Http` subsection becomes one HTTP channel under
the name of the section:

```json appsettings.json theme={null}
{
  "Veriqa": {
    "Channels": {
      "acme-chat": {
        "InboundVerification": "signature",
        "Http": {
          "BaseUrl": "https://acme-adapter.internal/",
          "DisplayName": "Acme Chat",
          "Capabilities": {
            "SupportsInChannelConfirmation": true,
            "DeliversOutcomeNotice": true
          }
        }
      }
    }
  }
}
```

The same channel in environment variables, with the transport secret — keep it there or in a secret
store, not in `appsettings.json`. A channel type with a dash cannot be exported from a shell, but the
`environment:` of a compose file and `docker run -e` take it:

```bash theme={null}
Veriqa__Channels__acme-chat__InboundVerification=signature
Veriqa__Channels__acme-chat__Http__BaseUrl=https://acme-adapter.internal/
Veriqa__Channels__acme-chat__Http__TransportSecret=whsec_<base64 of 24–64 random bytes>
Veriqa__Channels__acme-chat__Http__Capabilities__SupportsInChannelConfirmation=true
```

What to know about the keys (the full list is in the
[configuration reference](/docs/reference/configuration#keys-in-the-declaration-catalog)):

* **The channel facts and the button are stated by the operator**, with `Http:Capabilities:*` (one key
  per property of `ChannelCapabilities`), `Http:DisplayName` and `Http:IconSvgPath`. There is no
  network manifest: Veriqa does not ask your adapter what it can do. A fact left unstated takes the
  safe default of `ChannelCapabilities` — every boolean `false`, `plain_text` as the only content kind,
  no emoji rule of the channel's own (`RendersEmoji` unset), no phone number
  ([The phone number](#the-phone-number)).
* **`Http:Tenant`** names the tenant that owns the registration. It serves that tenant alone; for every
  other tenant the channel does not exist — not on the inbound route, not for events, not in outbound
  calls. Unset, the registration serves every tenant of the installation.
* **`Http:AdapterSettings`** is an opaque string, up to 8192 characters, that Veriqa forwards verbatim
  with every call and never reads. It has a core level and a tenant level; the value of a tenant
  replaces the core value entirely.
* **`Http:AllowInsecureHttp`** admits an `http` base URL. Use it only for an isolated internal network
  (a neighboring container); Veriqa writes a warning at startup.
* **Every `Http:*` value is fixed at startup**, except `Http:AdapterSettings`, which is resolved per
  call. Changing the others takes a restart.
* **The name of the channel** follows the format of [Name your channel](/docs/guides/custom-channel-adapter#3-name-your-channel), and the
  names of the channels shipped with Veriqa are reserved even when the shipped channel is not wired
  into the host: `telegram`, `whatsapp`, `max`, `email`, `instagram`, `messenger`, `wechat`, `viber`.
  The reserved list grows as new built-in channels ship.
* **A configuration error found at startup stops the host**, with a message naming the key or the
  channel section — for example, a missing or relative base URL, an `http` base URL without
  `Http:AllowInsecureHttp`, a missing `Http:TransportSecret`, a secret not in the `whsec_` form, a
  secret shared by two registrations, a forbidden deep-link scheme, a timeout not in the `hh:mm:ss`
  form, `Http:AdapterSettings` longer than 8192 characters, a reserved name or one another channel of
  the host already has. An empty `Http:Tenant` is an error too: to serve every tenant, remove the key.

Unlike an `AddChannel` channel (the note in §2), `InboundVerification` **is** checked at startup
for an HTTP channel: the registration declares the channel to the core, so an enabled HTTP channel
without a value stops the host.

### The phone number

A channel that can give the user's phone number says so in `Http:Capabilities:PhoneNumber` — the same
fact a built-in channel declares in code (see [Phone number on request](/docs/guides/custom-channel-adapter#phone-number-on-request) and
[Phone number](/docs/guides/phone-number)). Left unstated, the channel gives no number and is not offered to
a sign-in that requires one.

| Key (under `Http:Capabilities:PhoneNumber`) | Values | Unstated |
| - | - | - |
| `Acquisition:{n}` | a list: `Automatic` — the number comes with the identity of your events, nothing is asked; `OnRequest` — Veriqa asks for it with a `send-message` of the kind `phone_number_request`. Both may be listed | none — the channel gives no number |
| `Proof` | `Proven` — a number from this channel is proven to belong to the user; `NotProven` | `NotProven` |

```json appsettings.json theme={null}
"Capabilities": {
  "SupportedMessageKinds": [ "plain_text", "phone_number_request" ],
  "PhoneNumber": {
    "Acquisition": [ "OnRequest" ],
    "Proof": "Proven"
  }
}
```

```bash theme={null}
Veriqa__Channels__acme-chat__Http__Capabilities__SupportedMessageKinds__0=plain_text
Veriqa__Channels__acme-chat__Http__Capabilities__SupportedMessageKinds__1=phone_number_request
Veriqa__Channels__acme-chat__Http__Capabilities__PhoneNumber__Acquisition__0=OnRequest
Veriqa__Channels__acme-chat__Http__Capabilities__PhoneNumber__Proof=Proven
```

The names are matched without regard to case. Five mistakes stop the host from starting: `PhoneNumber`
written as a single value instead of a section, a value outside the names above, `Acquisition`
written as a single value instead of a list, `Proof: Proven` with no `Acquisition`, and `OnRequest`
without `phone_number_request` in `SupportedMessageKinds` — the request would never reach your
adapter.

`Proof` is the **ceiling** of what your events may state. `identity.phone_number_verified: true` of an
event is taken only from a registration that declares `Proven`; from any other, Veriqa lowers it to
`false` and writes a warning. `false` and an absent field are never raised: declaring `Proven` does
not make a number proven that your adapter said nothing about.

### The start by a code

A channel whose link cannot carry the transaction, or whose platform does not always deliver it,
declares the [start by a code](/docs/guides/custom-channel-adapter#start-by-a-code-—-when-the-link-cannot-be-relied-on) in
`Http:Capabilities:CodeStart` — the same two members a built-in channel sets in code. The core shows
the code on its pages, the user sends it to your bot, and your adapter posts it with the event
`auth_start_by_code` (see [The wire contract](#the-wire-contract)).

| Key (under `Http:Capabilities:CodeStart`) | Values | Unstated |
| - | - | - |
| `LinkCarriesTransaction` | `false` — your `deep-link` returns a link that only opens the chat, and the code is the only way to start; `true` — the link carries the transaction, and the code is a fallback beside it. **Stating this key declares the start by a code** | no start by a code |
| `MessageTemplate` | the message the user sends, `{code}` standing for the code exactly once | the user sends the code alone; an empty value counts as unstated |

```json appsettings.json theme={null}
"Capabilities": {
  "CodeStart": {
    "LinkCarriesTransaction": false,
    "MessageTemplate": "/veriqa {code}"
  }
}
```

```bash theme={null}
Veriqa__Channels__acme-chat__Http__Capabilities__CodeStart__LinkCarriesTransaction=false
Veriqa__Channels__acme-chat__Http__Capabilities__CodeStart__MessageTemplate="/veriqa {code}"
Veriqa__Channels__acme-chat__Http__UnmatchedCodeReplyText=<Natural Key of your reply>
```

`Http:UnmatchedCodeReplyText` is the reply to a code that names no live sign-in; unset,
`Http:StaleLinkReplyText` answers instead. The template is judged where the channel is shown, as for
a built-in channel, not at startup. Three mistakes stop the host from starting: `CodeStart` written as a single value
instead of a section, `LinkCarriesTransaction` that is not `true` or `false`, and `MessageTemplate`
without `LinkCarriesTransaction`. With `LinkCarriesTransaction: true` the spare code block and its
setting `SpareStartCodeEnabled` work for the HTTP channel as for any channel. An `auth_start_by_code`
from a registration that does not declare the start is not processed.

<Note>
  For now the start by a code is open only to a registration without an owner. A registration that
  declares it and sets `Http:Tenant` stops the host at startup, with a message naming both keys.
</Note>

## The wire contract

Every call from Veriqa is a signed `POST` of a JSON envelope to `Http:BaseUrl`: `version`, `operation`,
`channel_type` (one entry may serve several registrations), `tenant` (or `null`), `adapter_settings`
(or `null`) and `payload`. The answer is `200` with an envelope of `version` and exactly one of
`result` or `error` (`code` and `message`).

| Operation | What your adapter does |
| - | - |
| `send-prompt` | Sends the confirmation question with two buttons. Veriqa passes the text it rendered, the button captions and the structured context, so you may render on your own |
| `send-message` | Sends a message to the user. Its `kind` is one the registration declares. The kind `phone_number_request` comes with the member `phone_number_request` — the captions of the share button and of the second button, the secondary action (`skip` or `cancel_sign_in`) and the transaction; how you show the two is your choice. For every other kind the member is `null` |
| `report-outcome` | Shows the user where a transaction stands: `outcome` is its final state or `awaiting_claims` — the sign-in is confirmed and waits for the details the application still asks for. A final state may carry `phone_request_message`, the phone number request to take down. `expired` is reported for every transaction started from the channel — with `prompt_message` set to `null` when the question was not asked in the chat or `send-prompt` returned no reference. `declined` also comes with nothing pressed — when the user cancels the sign-in on the sign-in window while its question waits in the chat — with `prompt_message` set to `null` when `send-prompt` returned no reference. Called only when the channel declares `DeliversOutcomeNotice`; for a press on a sign-in that is already waiting, `awaiting_claims` is sent only when the channel also declares `SupportsMessageUpdate` |
| `deep-link` | Returns the `url` that takes the user into your channel for a transaction |
| `health` | Reports whether the adapter is healthy, its version and the highest wire version it speaks |

| Event | When your adapter posts it |
| - | - |
| `auth_start` | The user reached your channel through the deep link of a transaction |
| `auth_start_by_code` | The user sent your bot the code of the sign-in page — only for a registration that declares [the start by a code](#the-start-by-a-code). `code` is what the user sent, with the wrapping of your `MessageTemplate` stripped and nothing else done to it: Veriqa drops spaces and dashes and checks the length. The event names no transaction |
| `auth_confirm` | The user pressed the confirm button |
| `auth_decline` | The user pressed the decline button |
| `phone_shared` | The user shared their phone number. `phone_number` is in E.164 with the leading plus — `+` and 1 to 15 digits, the first not zero, no separators or spaces. Veriqa does not repair a number in another spelling: the event is not processed, the answer is still `200`, and a warning goes to the log |
| `phone_step_action` | The user chose the second action of a phone number request — through a control that carries a payload. `action` is `skip` or `cancel_sign_in`, as the request named it; `transaction_id` is the one of the request |
| `unaddressed` | A direct message from a known sender that names no transaction. The optional `text` carries the text of the message: a button that carries no payload arrives as the text of its caption, and that is how such a platform answers the second action of a phone number request |
| `unrelated` | Anything Veriqa has no business answering |

The fields of every payload, result and event are in the machine-readable
[JSON Schema of the wire](#conformance-suite) — this guide does not repeat them. Rules the schema does
not express:

* **Error codes.** The codes the wire names are in the schema, and any other code is accepted too. The
  code of an `error` is written to the log and decides nothing else, with one exception:
  `channel_cannot_continue` in the answer to `send-prompt` ends the transaction in failure right away.
  Any other `error` acts by its operation, and the call is not repeated after it:
  * `send-prompt` — the transaction stays open and waits for a new `auth_start` from the user or the
    end of its lifetime;
  * `deep-link` — the user gets no way into your channel: the sign-in page is rendered without it;
  * `send-message`, `report-outcome` — the refusal is logged and counted in the metrics, nothing more.
* **`deep-link` is idempotent**: a repeated call for the same transaction returns the same `url`
  and creates no new state.
* **Answers are bounded.** An answer is read up to 1 MB and refused past it, redirects are not
  followed, and a deep link is accepted only as an absolute URI with the `https` scheme or a scheme
  listed in `Http:DeepLinkSchemes`, no longer than 2048 characters.
* **Timeouts and retries.** One attempt waits `Http:OperationTimeout` (10 s by default); `deep-link`
  waits `Http:DeepLinkTimeout` (2 s) because it stands on the path of the sign-in page. `send-prompt`,
  `send-message` and `report-outcome` are retried on a transport failure — no connection, a timeout,
  a `5xx`, a `429` — up to three times, all attempts within `Http:RetryBudget` (20 s). Nothing else is
  retried: a `200` with `error`, a `3xx`, any other `4xx`, an oversized or malformed answer.
  `deep-link` and `health` are attempted once.
* **The second action of a phone number request comes back in either of two ways**, whichever your
  platform allows: the `phone_step_action` event, or an `unaddressed` event whose `text` equals the
  caption of the second button. An `action` Veriqa does not know makes the event `unrelated`. The share
  button itself is answered with `phone_shared`.
* **The phone number of an identity.** `identity.phone_number` of any event is taken as the user's
  number — the automatic way — and `identity.phone_number_verified: true` only up to the `Proof` the
  registration declares (see [The phone number](#the-phone-number)).
* **`auth_start_by_code` goes on the route your bot's other events use.** It names no transaction, so
  the rule of the transaction's tenant does not apply to it: Veriqa looks the code up across the whole
  installation, whichever route the event came on, and takes the tenant from the transaction it finds.
* **Refuse an event envelope on your call entry.** A body with `event` instead of `operation` that
  arrives at `Http:BaseUrl` is an event reflected back at you; answer it with anything but a `200`
  with `result`.
* **Serve calls while your own request is open.** On `auth_start` Veriqa sends `send-prompt` to your
  adapter **before** it answers your `POST` of the event. An adapter that cannot take calls
  concurrently with its own requests to Veriqa times the prompt out, and the transaction waits for its
  lifetime.

## The signature

Both directions are signed with the transport secret of the registration by the
[Standard Webhooks](https://www.standardwebhooks.com/) scheme `v1` (HMAC-SHA256) — every language has
a library for it. The headers are `webhook-id`, `webhook-timestamp` and `webhook-signature`, and a
timestamp is accepted within 5 minutes either way of the receiver's clock.

* **Rotation without downtime.** Put the new secret in `Http:TransportSecret` and the old one in
  `Http:PreviousTransportSecret`: the sender then signs with both, and the receiver accepts either.
  Veriqa writes a warning at startup until the previous secret is removed.
* **Answers are not signed**: their authenticity rests on the connection Veriqa opened to the
  configured address — which is why `https` is the default.
* **A wrong signature is `403`**, without an envelope and without any effect, in both directions: Veriqa
  answers your unsigned or mis-signed event that way, and your adapter must answer such a call the
  same way.
* **Every registration has a secret of its own.** Two registrations sharing one would let one pass as
  the other, so the host refuses to start.

## Idempotency

* **The `webhook-id` of `send-prompt`, `send-message` and `report-outcome` is the idempotency key.**
  A repeat with a key you have already served successfully sends nothing new and returns the result of
  the first call — the same message reference. `deep-link` and `health` carry a unique `webhook-id`
  that is not a key.
* **The `webhook-id` of your event is yours to choose**: unique per event and unchanged when you
  deliver the same event again. Veriqa derives the keys of the `send-prompt` and `report-outcome` an
  event causes from it, so a redelivered event does not ask the question twice.
* **A failed attempt does not bind the key**: the next attempt with it is served anew.
* **Retention is the operator's choice**, within two floors: no shorter than the retry budget of
  Veriqa (`Http:RetryBudget`), and, for a key derived from an event, no shorter than the window in which
  your adapter itself may deliver that event again.

## Wire version and the `preview` status

Every envelope, in both directions, carries `version` — the wire version of its sender, `major.minor`;
the first is `1.0`, with the status `preview`. Within one major version changes are additive only — a
new optional field, operation, enumeration value or event type — and each comes with the behavior of
a receiver that does not know it: an unknown field is ignored, an unknown operation is answered with
`unsupported_operation`, an unknown question variant is shown as a question without details, an
unknown event is taken by Veriqa as `unrelated`. Removing or renaming a field or changing its meaning
takes a new major version. Answer a call of another major version with `unsupported_wire_version`;
Veriqa, in turn, does not process an event of another major version and takes such an answer as
outside the contract. Every event also carries `adapter_version` — the semver version of your adapter.

## Conformance suite

Veriqa ships an executable conformance suite that checks your adapter against the wire, whatever it
is written in. It checks three things:

* **calls through the real wrapper** — every operation, a repeat under the same key, a wrong secret
  and a timestamp outside the window. These calls are made by the very HTTP channel code Veriqa runs,
  so the suite cannot drift from Veriqa;
* **malformed calls** Veriqa never makes — unsigned, with an unknown field, variant or operation, of a
  foreign major version, an event envelope reflected at your call entry — sent by a separate sender;
* **the receiver of events** — it plays Veriqa for your events: checks the signature and the schema
  of each, calls `send-prompt` before it answers `auth_start`, and checks that every event came on the
  route of its transaction's tenant.

Two scenarios check the phone number, and each joins a run only when the registration declares its
way: `send-message.phone-number-request` for `OnRequest`, `identity.phone-automatic` for `Automatic`. A
scenario missing from the report means its way is not declared. An event that states
`phone_number_verified: true` without a declared `Proof: Proven` fails `events.all-valid`.

A registration that declares [the start by a code](#the-start-by-a-code) gets the scenario
`flow.code-start`: the suite makes up a code, has it sent to the bot of a deep link — by the platform
stub with `send-text`, or by you on an `ACTION` line — and expects `auth_start_by_code` with those
digits from the user of the run. With `LinkCarriesTransaction: false` the scenarios that start a
transaction by its link — `flow.confirm`, `flow.decline`, `identity.phone-automatic` — are left out of
the run: a link that only opens the chat starts nothing. The suite then does not play the confirmation
after a start by a code — its made-up code has no transaction behind it — so check that path with a
live run.

The suite is a container image, built from the root of the repository:

```bash theme={null}
docker build -f src/core/Veriqa.Core.ChannelAdapter.Http.Conformance/Dockerfile -t veriqa-http-channel-conformance .
```

Run it next to your adapter with the configuration of your channel in the same
`Veriqa__Channels__<type>__*` keys Veriqa reads, plus keys of its own under `Conformance__*`:
`ChannelType` and `ChannelUserId` are required; `Mode`, `Tenants` and `ReportPath` are optional.

| `Conformance__Mode` | What runs | For |
| - | - | - |
| `calls` (default) | the calls alone | the CI gate of your adapter |
| `interactive` | the calls and the receiver; you open the deep link on the platform and press the buttons yourself | a manual check of the event half |
| `automatic` | the calls, the receiver and a stub of the platform that plays the user; `Conformance__PlatformStubUrl` is required | adapters that come with such a stub; its contract is in the suite's README |

In `interactive` and `automatic` the suite stands in for Veriqa: it listens on port `8080` of the image
on the same paths, `/api/channels/{type}/webhook` and `/api/channels/{type}/webhook/{tenant}`, so your
adapter changes only the base address of Veriqa it posts to. How long it waits for an event is
`Conformance__EventTimeoutSeconds`.

The suite prints one line per scenario — `PASS <id>`, `FAIL <id>: <reason>` or `N/A <id>: <reason>` —
writes a JSON report to `Conformance__ReportPath`, and exits with `0` (no scenario failed), `1` (at
least one failed) or `2` (the run could not be carried out; the message names what to fix). The full
list of scenarios is in the README of `src/core/Veriqa.Core.ChannelAdapter.Http.Conformance`.

The machine-readable schema of the wire (JSON Schema) ships with the suite: at
`/schema/http-channel-wire-v1.schema.json` in the image and in
`src/core/Veriqa.Core.ChannelAdapter.Http.Conformance/Schema/` in the repository.

## Reference adapters

Three external adapters in the repository show the whole contract on a real platform:

* `samples/python/custom-channel-telegram/` — Telegram, in Python;
* `samples/node/custom-channel-whatsapp-twilio/` — WhatsApp through Twilio, in Node;
* `samples/node/custom-channel-whatsapp-baileys/` — WhatsApp through Baileys, in Node — a working adapter for debugging, small teams and your own use; WhatsApp may ban the number.

Each runs next to Veriqa from its own folder:

```bash theme={null}
cp .env.example .env    # fill in the transport secret and the platform credentials
docker compose up
```

Between them they show every operation and every event of their channel, the signature in both
directions, idempotency (a repeat sends no second message), credentials per tenant,
`adapter_settings` read as a setting of the adapter's own, and the protection of the adapter's own
public entry. The Twilio adapter also answers `channel_cannot_continue` when the platform cannot
deliver the question; the Telegram adapter splits the updates between Veriqa and a bot of your own.
Two of them show the two ways of obtaining the phone number: the Telegram adapter **on request** — it
shows the request as a keyboard with a contact button and returns the second button as the text of an
`unaddressed` event; the Twilio adapter **automatically** — the number of the sender travels in the
identity of every event.
The Telegram adapter also shows [the start by a code](#the-start-by-a-code) behind a switch,
`CODE_START`: `off` by default, `fallback` for a registration with `LinkCarriesTransaction: true`,
`only` for one with `false`; its CI runs the suite a second time in the `only` mode.
The Baileys adapter is a working one, meant for debugging WhatsApp scenarios, for small teams and for your
own use: Baileys is an unofficial WhatsApp Web client on a number of your own, and WhatsApp may ban that
number. It asks the question as a
poll and fetches the messages over its own outbound session.
Each README documents its configuration, its `InboundVerification` value and a manual live run.

They are samples to learn from and copy, not a catalog of community adapters.

## `InboundVerification` of an external adapter

The rule of [Declare the inbound verification level of your channel — and document it](/docs/guides/custom-channel-adapter#declare-the-inbound-verification-level-of-your-channel-—-and-document-it)
holds for an external adapter unchanged: the platform's traffic is verified by your adapter, so
**you name the value for every inbound mode of your channel** in your documentation, and the operator
declares it in the section of the channel. There is no default. The signature of the transport between
your adapter and Veriqa is always checked and is not what this value describes.

The reference adapters name theirs:

| Adapter | Inbound mode | `InboundVerification` |
| - | - | - |
| Telegram | `webhook` — a secret token in a header of every update | `shared_secret` |
| Telegram | `polling` — the adapter fetches the updates | `outbound_fetch` |
| WhatsApp through Twilio | the webhook signed by Twilio | `signature` |
| WhatsApp through Baileys | the adapter's own outbound WhatsApp Web session | `outbound_fetch` |

In the same documentation list the top-level keys of your `raw_metadata` that carry personal data,
the native user id of the platform included: the integrator decides from that list what they may keep.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.