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",
      "name": "WhatsApp",
      "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.
nameNoDisplay name shown to hosts, for example "Sample Chat". 2 to 60 characters. When omitted, the channel key is prettified ("sample_chat" renders as "Sample chat").
capsYesCapabilities object (see below).

The name (or the prettified key when it is omitted) is what hosts see wherever your channel is listed in their inbox UI: the send-autonomy matrix, channel filters, and contact identity selects.

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.
  • name, when present, must be a string of 2 to 60 characters.
  • 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