Skip to main content
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#.
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.

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:
Program.cs

Configuration

Every section Veriqa:Channels:{channelType} with an Http subsection becomes one HTTP channel under the name of the section:
appsettings.json
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:
What to know about the keys (the full list is in the configuration reference):
  • 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).
  • 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, 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 and Phone number). Left unstated, the channel gives no number and is not offered to a sign-in that requires one.
appsettings.json
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 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).
appsettings.json
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.
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.

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). The fields of every payload, result and event are in the machine-readable JSON Schema of the wire — 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).
  • 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 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 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:
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. 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:
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 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 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: 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.