Skip to content

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.

PropertyRequiredDescription
keyYesA unique lowercase slug matching ^[a-z0-9_]{2,40}$. Reserved values (email, web_form, web) are rejected.
labelYesHuman-readable channel name shown in the host panel.
iconNohttps:// URL to a square icon (SVG or PNG, at least 48 x 48 px).
capsYesCapabilities object (see below).

channels[].caps

PropertyRequiredTypeDescription
outboundWindowHoursYesintegerNumber of hours after the last inbound message during which the host can send a free-form reply. Use 0 for no window (template-only).
richTextNobooleanWhether the channel supports bold, links, and other rich text. Defaults to false.
quickRepliesNobooleanWhether the channel can render quick-reply buttons. Defaults to false.
templateRequiredOutsideWindowNobooleanWhether an approved message template is required when replying outside the outbound window. Defaults to false.

Validation rules

  • channels requires the provide_inbox_channel scope to be listed in scopes.
  • channels requires endpoints.message_send to be set.
  • Each channel key must match ^[a-z0-9_]{2,40}$. Duplicate keys within one manifest are rejected.
  • Reserved keys (email, web_form, web) are rejected.
  • caps.outboundWindowHours must 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

HeaderValue
Content-Typeapplication/json
X-Stayblox-TeamThe installing team's slug.
X-Stayblox-TimestampUnix timestamp (seconds) of the request.
X-Stayblox-Signaturesha256=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"
}
FieldTypeDescription
message_idstring (UUID)Stayblox message id. Use for idempotency.
conversation_idstring (UUID)Stayblox conversation id.
channelstringThe channel key that matched this install.
recipient.external_thread_idstringProvider thread id (e.g. a WhatsApp PSID or thread UID) identifying where to deliver the message.
bodystringMessage text.
body_formatstringtext or a richer format the channel supports. Degrade to plain text when unsupported.
attachmentsarrayList of { url, type, name } objects. May be empty.
settingsobjectThe per-install settings values the host entered during installation.
api_base_urlstringRoot 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..."
}
FieldRequiredValuesDescription
statusYes"sent" or "failed"Whether the message was delivered to the provider.
provider_message_idNostringProvider's message id, used for delivery receipt correlation.
errorNostringHuman-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

FieldRequiredDescription
channelYesThe channel key declared in your manifest.
externalThreadIdYesProvider thread id. Used to match or create a conversation.
senderIdentifierYesProvider user id. Used for contact matching.
bodyNoMessage text.
bodyFormatNotext (default) or another format the channel supports.
attachmentsNoList of { url, type, name } objects.
externalMessageIdNoProvider message id. Used for idempotency — duplicate ids are ignored.
sentAtNoISO-8601 timestamp. Defaults to the time of the API call.
contactHintsNoName/email/phone hints for contact matching (see below).

ContactHintsInput

FieldDescription
firstNameGuest's first name.
lastNameGuest's last name.
displayNameDisplay name (used when first/last are absent).
emailEmail address for contact matching.
phoneE.164 phone number for contact matching.
avatarUrlProfile 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

FieldRequiredDescription
externalMessageIdYesThe provider_message_id you returned in the message_send response.
statusYes"delivered", "read", or "failed".

Checklist

  • [ ] Manifest declares provide_inbox_channel in scopes and valid entries in channels.
  • [ ] endpoints.message_send is set to a publicly reachable https:// URL.
  • [ ] message_send handler verifies the HMAC signature and rejects stale requests.
  • [ ] message_send responds { "status": "sent" | "failed" } within the timeout.
  • [ ] Inbound guest messages are forwarded via inboundMessageCreate.
  • [ ] Delivery receipts are reported via messageStatusUpdate.
  • [ ] userErrors is checked on every mutation.

© Stayblox — Developer Platform