Appearance
Inbox channel-provider apps
A channel-provider app lets you bring a new messaging channel into Stayblox — for example, WhatsApp, Telegram, or a custom SMS provider. When a host installs your app and connects a property to it, Stayblox:
- delivers outbound host replies to your endpoint (
message_send) — platform to app - accepts inbound guest messages you forward via GraphQL mutations — app to platform
Like payment apps, channel-provider apps authenticate with a per-install bearer token and the same HMAC signing scheme. See Signing & security for verification details.
1. Declare your app (manifest)
Add the provide_inbox_channel scope to your manifest's scopes array and declare your channels under the top-level channels key. Also set endpoints.message_send to the HTTPS URL Stayblox will POST outbound messages to.
jsonc
{
"name": "WhatsApp Connect",
"scopes": ["provide_inbox_channel", "read_conversations"],
"channels": [
{
"key": "whatsapp",
"label": "WhatsApp",
"icon": "https://cdn.example.com/whatsapp.svg",
"caps": {
"richText": false,
"quickReplies": false,
"outboundWindowHours": 24,
"templateRequiredOutsideWindow": true
}
}
],
"endpoints": {
"message_send": "https://app.example.com/stayblox/message-send"
},
"settings_schema": [
{ "key": "phone_number_id", "type": "string", "label": "Phone Number ID", "required": true },
{ "key": "access_token", "type": "string", "label": "Access Token", "required": true }
]
}channels array
Each entry declares one messaging channel your app provides.
| Property | Required | Description |
|---|---|---|
key | Yes | A unique lowercase slug matching ^[a-z0-9_]{2,40}$. Reserved values (email, web_form, web) are rejected. |
label | Yes | Human-readable channel name shown in the host panel. |
icon | No | https:// URL to a square icon (SVG or PNG, at least 48 x 48 px). |
caps | Yes | Capabilities object (see below). |
channels[].caps
| Property | Required | Type | Description |
|---|---|---|---|
outboundWindowHours | Yes | integer | Number of hours after the last inbound message during which the host can send a free-form reply. Use 0 for no window (template-only). |
richText | No | boolean | Whether the channel supports bold, links, and other rich text. Defaults to false. |
quickReplies | No | boolean | Whether the channel can render quick-reply buttons. Defaults to false. |
templateRequiredOutsideWindow | No | boolean | Whether an approved message template is required when replying outside the outbound window. Defaults to false. |
Validation rules
channelsrequires theprovide_inbox_channelscope to be listed inscopes.channelsrequiresendpoints.message_sendto be set.- Each channel
keymust match^[a-z0-9_]{2,40}$. Duplicate keys within one manifest are rejected. - Reserved keys (
email,web_form,web) are rejected. caps.outboundWindowHoursmust be present.
2. Receive outbound messages (platform to app)
When a host sends a reply on one of your channels, Stayblox POSTs a signed delivery command to your endpoints.message_send URL. Verify the signature before processing (same algorithm used for webhooks — see Signing & security).
Request headers
| Header | Value |
|---|---|
Content-Type | application/json |
X-Stayblox-Team | The installing team's slug. |
X-Stayblox-Timestamp | Unix timestamp (seconds) of the request. |
X-Stayblox-Signature | sha256=HMAC_SHA256("{timestamp}.{raw_body}", webhook_secret) |
Request body
jsonc
{
"message_id": "9b2c1f0e-...",
"conversation_id": "7d3a0e1b-...",
"channel": "whatsapp",
"recipient": {
"external_thread_id": "1234567890"
},
"body": "Hello! Your check-in code is 4821.",
"body_format": "text",
"attachments": [],
"settings": {
"phone_number_id": "...",
"access_token": "..."
},
"api_base_url": "https://app.stayblox.com/developer/api/2026-01"
}| Field | Type | Description |
|---|---|---|
message_id | string (UUID) | Stayblox message id. Use for idempotency. |
conversation_id | string (UUID) | Stayblox conversation id. |
channel | string | The channel key that matched this install. |
recipient.external_thread_id | string | Provider thread id (e.g. a WhatsApp PSID or thread UID) identifying where to deliver the message. |
body | string | Message text. |
body_format | string | text or a richer format the channel supports. Degrade to plain text when unsupported. |
attachments | array | List of { url, type, name } objects. May be empty. |
settings | object | The per-install settings values the host entered during installation. |
api_base_url | string | Root URL for the Developer GraphQL API. Use this to construct the endpoint when calling back. |
Response
Respond with HTTP 200 and a JSON body:
jsonc
{
"status": "sent",
"provider_message_id": "wamid.ABC123..."
}| Field | Required | Values | Description |
|---|---|---|---|
status | Yes | "sent" or "failed" | Whether the message was delivered to the provider. |
provider_message_id | No | string | Provider's message id, used for delivery receipt correlation. |
error | No | string | Human-readable error description when status is "failed". |
Any non-200 response or network timeout is treated as a failure.
3. Forward inbound messages (app to platform)
When a guest sends a message on your channel, forward it to Stayblox with the inboundMessageCreate mutation. Use your install's bearer token.
graphql
mutation InjectMessage($input: InboundMessageInput!) {
inboundMessageCreate(input: $input) {
conversationId
messageId
userErrors { field message }
}
}InboundMessageInput
| Field | Required | Description |
|---|---|---|
channel | Yes | The channel key declared in your manifest. |
externalThreadId | Yes | Provider thread id. Used to match or create a conversation. |
senderIdentifier | Yes | Provider user id. Used for contact matching. |
body | No | Message text. |
bodyFormat | No | text (default) or another format the channel supports. |
attachments | No | List of { url, type, name } objects. |
externalMessageId | No | Provider message id. Used for idempotency — duplicate ids are ignored. |
sentAt | No | ISO-8601 timestamp. Defaults to the time of the API call. |
contactHints | No | Name/email/phone hints for contact matching (see below). |
ContactHintsInput
| Field | Description |
|---|---|
firstName | Guest's first name. |
lastName | Guest's last name. |
displayName | Display name (used when first/last are absent). |
email | Email address for contact matching. |
phone | E.164 phone number for contact matching. |
avatarUrl | Profile photo URL. |
Result
jsonc
{
"data": {
"inboundMessageCreate": {
"conversationId": "7d3a0e1b-...",
"messageId": "9b2c1f0e-...",
"userErrors": []
}
}
}userErrors is empty on success. On failure it describes the problem (unknown channel, missing required field, etc.).
4. Report delivery receipts (app to platform)
After delivering a host reply, update its delivery status with messageStatusUpdate. Use the externalMessageId you returned in step 2's provider_message_id to correlate.
graphql
mutation StatusUpdate($input: MessageStatusInput!) {
messageStatusUpdate(input: $input) {
ok
userErrors { field message }
}
}MessageStatusInput
| Field | Required | Description |
|---|---|---|
externalMessageId | Yes | The provider_message_id you returned in the message_send response. |
status | Yes | "delivered", "read", or "failed". |
Checklist
- [ ] Manifest declares
provide_inbox_channelinscopesand valid entries inchannels. - [ ]
endpoints.message_sendis set to a publicly reachablehttps://URL. - [ ]
message_sendhandler verifies the HMAC signature and rejects stale requests. - [ ]
message_sendresponds{ "status": "sent" | "failed" }within the timeout. - [ ] Inbound guest messages are forwarded via
inboundMessageCreate. - [ ] Delivery receipts are reported via
messageStatusUpdate. - [ ]
userErrorsis checked on every mutation.