Channels
Channels represent the communication pathways through which oHallo sends and receives messages. Each channel has a type (email, chat, WhatsApp, or voice), a set of configuration parameters, and a provisioning status that tracks whether the channel is fully operational.
Required scope: channels:read for GET endpoints, channels:write for write endpoints.
People signed in to the dashboard need the owner or admin role to create, change or delete a channel. A key restricted to a brand or workspace works with the channels of the workspaces its restriction covers; a channel or workspace outside it answers 404.
Create channel
Section titled “Create channel”Create a channel in a workspace. config takes the shape for the channel’s type, described under Channel configuration. This email channel receives its mail on a subdomain given to oHallo.
Required scope: channels:write
curl -X POST "https://api.ohallo.eu/api/workspaces/a1b2c3d4-e5f6-7890-abcd-ef1234567890/channels" \ -H "Authorization: Bearer sf_live_v1_a3Bx9kLmP2qR7wYz4nDfGhJkQpStUvWx" \ -H "Content-Type: application/json" \ -d '{ "channelType": "email", "displayName": "Support Email", "config": { "inbound_mode": "subdomain", "from_address": "support@help.example.com", "inbound_address": "support@help.example.com" } }'Request body:
| Field | Type | Required | Description |
|---|---|---|---|
channelType | string | Yes | email, chat, whatsapp or voice_phone |
displayName | string | Yes | Human-readable name (minimum 1 character) |
config | object | No | The configuration for the channel’s type. An email channel needs one; the others accept an empty object, the default |
enabled | boolean | No | Whether the channel is enabled. Default true |
phoneNumberId | UUID | For voice_phone | The provisioned phone number the voice channel answers. Only a voice channel takes one |
welcomeGreetingAttested | boolean | No | The EU AI Act Article 50 confirmation for a voice channel’s welcome_greeting |
Response: 201 with the channel. An email channel is provisioned after the call returns, and its record carries what its mode needs set up outside oHallo: the DNS records to publish in config.dns_records, or the address oHallo generated in config.inbound_address. Its status shows how far the setup has come. A chat channel is active at once, and its publicKey is the data-key of the chat’s embed code. A voice channel is active at once when its phone number is, and pending_activation while the number is in review with the carrier. Each call creates a new channel.
Get channel
Section titled “Get channel”Retrieve full details for a single channel.
curl "https://api.ohallo.eu/api/channels/d4e5f6a7-b8c9-0123-4567-890abcdef012" \ -H "Authorization: Bearer sf_live_v1_a3Bx9kLmP2qR7wYz4nDfGhJkQpStUvWx"Response (trimmed to the main fields):
{ "id": "d4e5f6a7-b8c9-0123-4567-890abcdef012", "workspaceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "channelType": "email", "displayName": "Support Email", "status": "active", "enabled": true, "config": { "inbound_mode": "subdomain", "from_address": "support@help.example.com", "inbound_address": "support@help.example.com", "domain": "help.example.com" }, "createdAt": "2026-01-10T08:00:00.000Z"}Response fields:
| Field | Type | Description |
|---|---|---|
id | UUID | Channel identifier |
workspaceId | UUID | Workspace the channel belongs to |
channelType | string | Channel type: email, chat, whatsapp or voice_phone |
displayName | string | Human-readable name |
status | string | Where the channel stands: draft, pending_dns, pending_verification, pending_setup, pending_inbound, pending_activation, active, error, inactive or deprovisioned. See Channel statuses |
enabled | boolean | Whether the channel is enabled |
config | object | The configuration for the channel’s type (settings only; credentials are never returned). See Channel configuration |
publicKey | string | null | A chat channel’s public key, the data-key of its embed code |
phoneNumberId | UUID | null | The phone number a voice channel answers |
brandId | UUID | null | On a read by id: the brand the channel answers for, which is the brand of its workspace |
createdAt | string | ISO 8601 timestamp |
updatedAt | string | ISO 8601 timestamp |
Update channel
Section titled “Update channel”Update channel settings. Only the fields you include in the request body are changed. This example sets where a chat channel runs and how it appears.
Required scope: channels:write
curl -X PATCH "https://api.ohallo.eu/api/channels/f6a7b8c9-d0e1-4234-8678-90abcdef1234" \ -H "Authorization: Bearer sf_live_v1_a3Bx9kLmP2qR7wYz4nDfGhJkQpStUvWx" \ -H "Content-Type: application/json" \ -d '{ "displayName": "Website Chat", "config": { "allowed_origins": ["https://www.example.com"], "theme": { "mode": "floating", "position": "bottom-left" } }, "enabled": true }'Request body:
| Field | Type | Required | Description |
|---|---|---|---|
displayName | string | No | Human-readable name (minimum 1 character) |
config | object | No | The configuration keys to change, in the shape for the channel’s type |
enabled | boolean | No | Whether the channel is active |
workspaceId | UUID | No | Moves the channel to another workspace, and with it under that workspace’s brand. The caller must be allowed to change both workspaces. A voice channel stays in the workspace its phone number is registered to, and a move answers 422 |
phoneNumberId | UUID | No | On a voice channel, the phone number it answers. A voice channel keeps its number: the number it already has is accepted, and any other answers 409. Another channel type answers 400 |
welcomeGreetingAttested | boolean | No | The EU AI Act Article 50 confirmation for a voice channel’s welcome_greeting |
A connected mailbox keeps its address, provider and inbound mode: a change of from_address, inbound_address, mail_provider or inbound_mode on a channel whose mailbox is connected answers 409 with { "error": "mailbox_connected", "fields": [...] }. To move the channel to another mailbox, disconnect it in the dashboard, update the channel, then connect the new mailbox. Setting enabled to true on a connected mailbox first renews its subscription at the mail provider; if that fails, the request answers 502 with { "error": "mailbox_reenable_failed" } and the channel stays off.
On an email, chat or voice channel, the config object is merged into the stored configuration key by key: a key you send replaces its stored value whole, so send theme with all of its fields; keys you leave out keep their value; and a key sent as null is removed. The merged configuration is then validated against the shape for the channel’s type, and an invalid one answers 422. Keys the platform maintains are ignored when sent: domain, dns_records, ses_config_set, active_at, subscription_id, subscription_expires_at, gmail_history_id, graph_catchup_link, graph_catchup_address, graph_catchup_completed_at, subscription_renewal_failed, subscription_renewal_failed_at, subscription_renewal_failed_reason, mailbox_reconnect_notice, oauth_authenticated_at, oauth_pending, error_message and error_message_kind.
Response: Returns the full updated channel object.
Channel configuration
Section titled “Channel configuration”config holds a channel’s settings, in a shape that depends on its channelType.
inbound_mode decides how mail reaches oHallo, and which fields the channel needs:
inbound_mode | How mail reaches oHallo | Fields to send |
|---|---|---|
subdomain | A subdomain given to oHallo, such as help.example.com, receives the mail itself. Publish the records in dns_records on it | from_address, an address on the subdomain, and inbound_address, the same address |
forwarding | The mailbox stays where it is and forwards its mail to the inbound_address oHallo generates. No DNS records. When Gmail sends its forwarding confirmation to that address, oHallo stores it on the channel as config.forwarding_confirmation (provider, code, link, forwarding_from, received_at; code or link is null when Gmail’s message carries none, and one of them is always present), shown in the dashboard; it is written by oHallo and ignored in a create or update | from_address; mail_provider: office365, google or other |
inbox_api | oHallo reads and sends through a Microsoft 365 mailbox, connected in the dashboard | from_address, the mailbox, which may be empty until it is entered in the dashboard; inbound_address, the same; mail_provider: microsoft365 |
mail_routing_rule | The mailbox stays where it is and a mail-flow rule in its provider redirects its mail to the inbound_address oHallo generates. Replies come from from_address itself, signed by the three DKIM records in dns_records, published on its domain; the MX record stays as it is. The channel turns active when the first redirected message arrives after the records verify | from_address; mail_provider: office365 or google |
A config without inbound_mode is read as subdomain, with inbound_address taken from from_address. Another inbound_mode on create answers 422 with the message on inbound_mode; on update, a new inbound_mode other than subdomain, forwarding or inbox_api answers 422 the same way, since an update provisions nothing. A subdomain channel’s addresses are on a subdomain of a registrable domain, as the Public Suffix List draws it, since an MX record on the registrable domain itself would receive all of its mail: an address such as support@example.com or support@example.co.uk answers 422 on create, and on an update that gives the channel that address, with the message on the address. A domain receives and sends for one oHallo account: an address on a domain a channel of another account uses answers 422 with code email_domain_in_use and a sentence naming the domain, on create and on an update that gives the channel that domain, and an address on a domain oHallo sends its own mail from answers 422 with code email_domain_reserved. A subdomain channel’s from_address and inbound_address are on the same domain; addresses on two domains answer 422 with the message on inbound_address. A channel read back can also carry subdomain_with_redirect (with its routing_subdomain); an update that keeps that mode is accepted.
While it provisions the channel, oHallo adds keys of its own: the inbound_address it generates, dns_records with the records to publish (verification_txt, dkim_cnames, mx, spf and dmarc, each with type, name, value, and priority for MX), domain, the domain the records belong to, and error_message when provisioning fails. The verification record, the DKIM records and the MX record route and verify mail; SPF and DMARC serve the delivery of replies. The DMARC record names no report address: to receive DMARC reports, add rua=mailto: with an address of your own. A mail_routing_rule channel lists its DKIM records once provisioning has requested them.
An inbox_api channel is finished in the dashboard: open the channel under Settings, Channels and choose Connect, which signs in to the mailbox’s provider. The channel stays in draft until then.
Every field is optional: a chat channel with an empty config runs on any website as a floating launcher.
| Field | Type | Description |
|---|---|---|
allowed_origins | string[] | The website origins the chat runs on, each a scheme and host such as https://www.example.com. A page’s origin must match one exactly, so a domain and its www subdomain are two origins. An empty or absent list lets the chat run on any website |
theme.mode | string | floating (the default), a launcher in a corner of the page; sidebar, a full-height panel at the side; or inline, a section inside the page |
theme.primary_color | string | The chat’s accent colour as a hex value, such as #0A2540 |
theme.position | string | bottom-right (the default) or bottom-left: the corner the floating launcher sits in |
suggested_prompts | string[] | Questions shown in the empty chat in the inline mode; choosing one sends it as the visitor’s first message |
handover.email | boolean | Default true. When a visitor’s question needs the team and an email address for the visitor is known, the assistant passes the chat to the team by email: an email conversation with the visitor opens, or an open one continues, linked to the chat, in the email channel chosen for follow-ups or, when none is chosen, the workspace’s only email channel. The visitor receives an email in it, unless a team member holds that conversation or has paused the assistant on it. When handover.phone is true and the visitor leaves a number for a call, a callback request is recorded instead |
handover.phone | boolean | Default false. With true, a phone number the visitor leaves for a call from the team becomes a callback request in Callbacks. With false, none is recorded |
handover.guidance | string | In the account’s own words, which questions go to the team (up to 2,000 characters). The assistant reads it when it decides which questions to pass to the team |
A chat channel may also hold escalation and human_handover_fallback, which are accepted on create and update and stored with the channel. While a channel holds no handover, its guidance is escalation.guidance, handover.phone is true exactly when human_handover_fallback.strategy is callback, and handover.email is true. A channel that holds handover reads handover alone.
A voice channel (voice_phone) answers the phone number it was created with. Every field is optional.
| Field | Type | Description |
|---|---|---|
language | string | The line’s language, as a language code such as da or sv: one of the brand’s supportedLanguages, or the workspace’s operatingLanguage. It is stored as the canonical code (no is stored as nb). Without it, or with null, the line takes the workspace’s operating language. A language outside those answers 422 and nothing is saved |
welcome_greeting | string | The opening line callers hear on this number, in place of the workspace’s greeting (up to 2,000 characters). A new or changed line is saved only with "welcomeGreetingAttested": true in the same request, and its wording must mention the assistant in the line’s language; otherwise the request answers 422 with code greeting_attestation_required or greeting_must_mention_ai. A confirmation covers the line’s language: after the line moves to another language, the line is confirmed again in that language, and until then callers hear oHallo’s standard opening line |
responsiveness | string | sentence, the default, waits for a whole sentence before the voice speaks it, which sounds most natural; token starts on the first words and answers sooner |
excluded_policy_ids | UUID[] | Policies left out of calls on this number. A call draws on every approved policy of the workspace except these |
excluded_kb_ids | UUID[] | Knowledge base entries left out of calls on this number |
human_handover_fallback | object | What the assistant does when a person cannot take over; see below |
voice_queue | object | What a caller waiting for a person hears, such as the hold music, as set on the channel’s page in the dashboard |
When a person cannot take over
Section titled “When a person cannot take over”On a voice channel, human_handover_fallback sets what the assistant does when a person cannot take over: when the team is closed or no colleague is on duty. A caller placed in the queue waits for a colleague. strategy is resume_ai, where the assistant carries on with the customer itself, or callback, where it records a request for someone to call the customer back. A voice channel applies take_message as callback, and one without a strategy offers a callback.
{ "human_handover_fallback": { "strategy": "callback" }}Delete channel
Section titled “Delete channel”Permanently delete a channel. Active conversations on this channel will no longer receive or send messages.
For an email channel, oHallo then removes what the channel set up to receive mail, in the background: its mail routing, and once no other channel uses its domain, oHallo’s approval to send from that domain; or for a connected mailbox (inbound_mode: inbox_api) its subscription at the mail provider and the stored mailbox access. Deleting a channel that is already deleted answers { "ok": true }; for a connected mailbox it runs that cleanup again.
Required scope: channels:write
curl -X DELETE "https://api.ohallo.eu/api/channels/d4e5f6a7-b8c9-0123-4567-890abcdef012" \ -H "Authorization: Bearer sf_live_v1_a3Bx9kLmP2qR7wYz4nDfGhJkQpStUvWx"Response:
{ "ok": true}Store channel credentials
Section titled “Store channel credentials”Store or replace a WhatsApp channel’s access token. The token is written to a secure vault and never returned in subsequent API responses. The response includes a credentialsRef that oHallo uses to retrieve the token at runtime.
The route stores credentials for WhatsApp channels. Any other channel type answers 422 with { "error": "credentials_not_applicable" } and nothing is stored; an email channel’s connected mailbox is signed in from its page in the dashboard. A channel holding stored credentials that this route did not write answers 409 with { "error": "credentials_not_owned" }, and the channel keeps them.
Required scope: channels:write
curl -X POST "https://api.ohallo.eu/api/channels/d4e5f6a7-b8c9-0123-4567-890abcdef012/credentials" \ -H "Authorization: Bearer sf_live_v1_a3Bx9kLmP2qR7wYz4nDfGhJkQpStUvWx" \ -H "Content-Type: application/json" \ -d '{ "access_token": "example-whatsapp-access-token" }'Request body:
| Field | Type | Required | Description |
|---|---|---|---|
access_token | string | No | The channel’s permanent access token from Meta Business Manager. Left out, the stored token stays as it is. |
A key other than access_token answers 422, and nothing is stored.
Response:
{ "ok": true, "credentialsRef": "channels/a1b2c3d4-e5f6-7890-abcd-ef1234567890/d4e5f6a7-b8c9-0123-4567-890abcdef012"}Test channel connectivity
Section titled “Test channel connectivity”Test an email channel against what its setup needs to go live, and move its status forward from the answer. A subdomain channel passes when the domain of from_address is verified for sending, its DKIM records are verified and its MX record resolves to the inbound endpoint, and a pass sets it active. A mail_routing_rule channel passes when the DKIM records of its domain are verified; a pass moves a channel still waiting for its DKIM records (pending_dns) on to pending_setup and leaves any other status as it is, since the channel turns active when the first message its mail-flow rule redirects arrives. A failure sets an active channel to error; a channel still waiting for its records keeps its status, and missingRecords names the records it waits on: verification (the domain verified for sending), dkim or mx. An MX lookup that gets no answer decides nothing: unconfirmedRecords names it, the status stays as it was, and the test can run again a moment later. The test never sets a status earlier than the channel’s own, and a deleted channel keeps its status. A forwarding channel sends its replies from oHallo’s own address and an inbox_api channel sends through its mailbox provider, so for those two the test answers 422 with { "error": "test_not_applicable" } and the status stays as it was. On an account whose billing is suspended or closed, the test answers 409 with { "error": "billing_restricted" } and the status stays as it was.
Required scope: channels:write
curl -X POST "https://api.ohallo.eu/api/channels/d4e5f6a7-b8c9-0123-4567-890abcdef012/test" \ -H "Authorization: Bearer sf_live_v1_a3Bx9kLmP2qR7wYz4nDfGhJkQpStUvWx"Response: status is the channel’s status after the test.
{ "ok": true, "status": "active"}If the test fails, the response names the records the channel still waits on, and reason says what to do.
{ "ok": false, "status": "pending_dns", "missingRecords": ["mx"], "reason": "Records still to verify on help.example.com: MX. Publish them as listed under DNS records, then run the test again."}Check DNS records
Section titled “Check DNS records”Look up the DNS records an email channel’s inbound_mode asks you to publish (config.dns_records) and report each one’s state: verified when found as expected, mismatch when found with another value, not_found when not published, and pending when the lookup did not settle, so check again shortly. SPF is verified when it is a v=spf1 record that includes amazonses.com, one merged with your own senders included, and DMARC when it is v=DMARC1 with p=quarantine or p=reject, whatever report address it names. A mode with no records to publish answers an empty list with reason. Six checks a minute per channel; a seventh answers 429. A channel that is not an email channel answers 422.
Required scope: channels:read
curl -X POST "https://api.ohallo.eu/api/channels/f6a7b8c9-d0e1-4234-8678-90abcdef1234/dns/probe" \ -H "Authorization: Bearer sf_live_v1_a3Bx9kLmP2qR7wYz4nDfGhJkQpStUvWx"Response:
{ "records": [ { "type": "MX", "name": "help.example.com", "expected": "inbound.mail.example.net", "observed": "inbound.mail.example.net", "status": "verified" }, { "type": "TXT", "name": "_dmarc.help.example.com", "expected": "v=DMARC1; p=quarantine", "status": "not_found" } ]}Retry provisioning
Section titled “Retry provisioning”Retry the provisioning of an email channel that is in error status. This re-runs its provisioning from the beginning.
A channel that connects a mailbox (inbound_mode: inbox_api) recovers by connecting the mailbox again from its page in the dashboard, so for it this endpoint answers 409 with { "error": "mailbox_reconnect_required" }. A channel in another status answers 409, and a channel that is not an email channel answers 400.
Required scope: channels:write
curl -X POST "https://api.ohallo.eu/api/channels/d4e5f6a7-b8c9-0123-4567-890abcdef012/retry-provisioning" \ -H "Authorization: Bearer sf_live_v1_a3Bx9kLmP2qR7wYz4nDfGhJkQpStUvWx"Response:
{ "ok": true}Channel statuses
Section titled “Channel statuses”| Status | Meaning |
|---|---|
draft | Set up but not live. An inbox_api email channel stays in draft until its mailbox is connected in the dashboard |
pending_dns | A subdomain, subdomain_with_redirect or mail_routing_rule email channel waits for its DNS records to be published and to resolve. A mail_routing_rule channel waits here until its DKIM records are verified, for up to 72 hours, after which it moves to error |
pending_verification | The channel’s DNS records resolve, and oHallo is completing the verification of its domain |
pending_setup | A mail_routing_rule or subdomain_with_redirect email channel waits for the first message its mail-flow rule redirects, and turns active when it arrives |
pending_inbound | A forwarding email channel waits for the first forwarded message, and turns active when it arrives |
pending_activation | A voice channel waits while its phone number is in review with the carrier, and turns active when the number is approved |
active | Fully operational: the channel can send and receive messages. A successful connectivity test also sets it, on every channel but a mail_routing_rule one |
error | Provisioning failed, a connectivity test failed on an active channel, or a connected mailbox needs to be connected again. After a provisioning failure, config.error_message says why when the reason is known. The retry endpoint provisions an email channel again, and a connected mailbox is connected again in the dashboard |
inactive | Disconnected: a mailbox reads inactive after it is disconnected, and the channel receives and sends nothing until the mailbox is connected again |
deprovisioned | The channel was deleted. Channel lists leave it out, and a read by id returns it |