Developers

Platform Bridges

Use signed, permission-gated bridge endpoints when plugins need deterministic platform actions.

What is a platform bridge?

A platform bridge is a narrow HTTP endpoint that lets an installed plugin request one platform-owned action.

It is not a general admin API. It exists for actions that should be deterministic, audited, and permission-gated, such as:

  • Creating a payment request.
  • Creating a durable commerce order.
  • Checking payment or order status.
  • Scheduling a message.
  • Creating an escalation.
  • Requesting reminders for obligations owned by the plugin.

The bridge keeps the AI out of the persistence path. The AI can guide the conversation, but the bridge owns the state transition.

Agent-facing tools versus dependency-only capabilities

Some installed capabilities are meant for the AI agent to call directly. Others are installed so plugins can call them through a bridge.

ModeVisible to the AI agent?Who uses it?Example
Agent-facing pluginYesThe WhatsApp agentGas OS exposes list_products, quote_order, and create_b2c_order
Dependency-only capabilityNoA plugin or internal toolkit through bridgesE-Commerce persists the order; Payments or M-Pesa requests the payment

For plugin-owned commerce flows, keep the domain plugin agent-facing and keep platform E-Commerce/Payments as dependency-only capabilities. The mother app stays business-agnostic: it does not know what gas, legal consultations, or salon appointments mean. It only knows generic platform effects such as catalog sync, order creation, checkout, payment, receipts, schedules, and escalations.

When a plugin is granted E-Commerce bridge permissions for a WhatsApp number instance, raw platform commerce_* tools are hidden from the AI for that instance. This prevents the agent from bypassing the plugin's domain flow. The plugin still uses the E-Commerce bridge to create durable platform orders.

Common request envelope

Every bridge request is signed and includes the plugin, organization, WhatsApp number instance, and idempotency key.

In this context, instanceId means the specific connected WhatsApp number that should own the action. For example, if Gas OS is granted to the "Sales WhatsApp" number, bridge calls for sales orders should use that WhatsApp number instance ID.

json
{
  "organizationId": "org_123",
  "instanceId": "inst_456",
  "serviceName": "GAS_OS",
  "idempotencyKey": "gas-order-GAS-2041-payment-v1"
}

Send the signature in either header:

text
x-plugin-signature: <base64url hmac sha256 of raw request body>
x-kasilabs-signature: <same value, legacy-compatible>

The signing secret is the plugin secret issued during sandbox or live installation.

Enforcement

Before doing anything, the bridge checks:

  1. The request body is valid JSON.
  2. The signature matches the installed plugin secret.
  3. The plugin is installed for the organization.
  4. The plugin is granted to the target WhatsApp number instance.
  5. The required bridge permission is granted.
  6. The recipient is allowed.
  7. Side-effect requests use an idempotency key.

Each side-effect request is recorded with a status, request hash, response snapshot, and error if one occurs.

Current bridge endpoints

EndpointPermission shapePurpose
POST /plugins/bridges/payments/initiateplugin:payments:initiate:<recipient_scope>Create a customer payment request
POST /plugins/bridges/payments/statusplugin:payments:status:own or plugin:payments:status:anyVerify latest payment status
POST /plugins/bridges/payments/refundplugin:payments:refund:execute:own or plugin:payments:refund:execute:anyRequest a refund
POST /plugins/bridges/ecommerce/orders/createplugin:ecommerce:orders:create:<recipient_scope>Create a durable order from synced catalog items
POST /plugins/bridges/ecommerce/orders/getplugin:ecommerce:orders:read:anyFetch an order
POST /plugins/bridges/ecommerce/after-sales/createplugin:ecommerce:after_sales:<type>:createCreate refund, cancellation, return, replacement, or support request
POST /plugins/bridges/messages/sendplugin:messages:send:<recipient_scope>Send a text message
POST /plugins/bridges/messages/scheduleplugin:messages:schedule:<recipient_scope>Schedule a text message
POST /plugins/bridges/escalations/createplugin:messages:escalate:<recipient_scope>Create or assign a human escalation
POST /plugins/obligations/requestplugin:obligations:requestSubmit an external obligation for reminder/escalation processing

<recipient_scope> is one of current_chat, known_contact, or external_recipient. Prefer current_chat for customer-driven flows because it uses a short-lived token instead of exposing the raw WhatsApp JID to the plugin.

When KasiLabs calls a plugin tool during a customer conversation, the tool request includes:

json
{
  "context": {
    "currentChat": {
      "token": "<short-lived bridge token>",
      "expiresAt": 1778250300000
    },
    "user": {
      "id": "usr_masked_...",
      "hashVersion": 1
    }
  }
}

Use context.currentChat.token in bridge calls that target the customer currently chatting with the WhatsApp number instance.

Payment initiate

Use this when the plugin needs the platform to request payment from a customer.

json
{
  "organizationId": "org_123",
  "instanceId": "inst_456",
  "serviceName": "GAS_OS",
  "idempotencyKey": "gas-order-GAS-2041-payment-v1",
  "recipient": {
    "type": "current_chat",
    "token": "<context.currentChat.token>"
  },
  "customer": {
    "name": "Amina Wanjiru",
    "email": "amina@example.com"
  },
  "payment": {
    "amount": 3350,
    "description": "13kg cooking gas refill and delivery",
    "method": "mobile_money_mpesa",
    "externalReference": "GAS-2041"
  },
  "eventTool": "record_payment_event",
  "sendMessage": true
}

Required permissions:

  • plugin:payments:initiate:current_chat
  • plugin:messages:send:current_chat if sendMessage is not false

When payment succeeds, the platform can call the plugin tool named in eventTool with a deterministic payment event. This lets the plugin update its own order state without asking the AI to infer that payment happened.

The payment event includes the masked customer identity, not the raw WhatsApp JID:

json
{
  "eventType": "payment.success",
  "reference": "pay_...",
  "externalReference": "GAS-2041",
  "customer": {
    "id": "usr_masked_...",
    "hashVersion": 1
  },
  "amount": 3350,
  "currency": "KES"
}

Commerce order create

Use this when the plugin wants an order persisted in the platform commerce system.

json
{
  "organizationId": "org_123",
  "instanceId": "inst_456",
  "serviceName": "GAS_OS",
  "idempotencyKey": "gas-order-GAS-2041-create-v1",
  "recipient": {
    "type": "current_chat",
    "token": "<context.currentChat.token>"
  },
  "customer": {
    "name": "Amina Wanjiru",
    "deliveryAddress": "Beth House, Kasarani, House 16"
  },
  "items": [
    {
      "sourcePlugin": "GAS_OS",
      "externalId": "KGAS-13KG",
      "quantity": 1
    }
  ],
  "deliveryFee": 200,
  "notes": "Customer wants delivery before 6 PM",
  "externalReference": "GAS-2041",
  "callbacks": {
    "orderEventTool": "record_order_event",
    "paymentEventTool": "record_payment_event"
  },
  "checkout": {
    "method": "mobile_money_mpesa"
  }
}

Required permissions:

  • plugin:ecommerce:orders:create:current_chat
  • plugin:ecommerce:checkout:initiate if checkout is included
  • plugin:payments:initiate:current_chat if checkout is included
  • plugin:messages:send:current_chat if checkout is included and sendMessage is not false

When callbacks.orderEventTool is included, the platform stores the callback metadata on the order and calls that plugin tool for deterministic lifecycle events such as order.created, order.status_updated, order.fulfillment_updated, and order.payment_recorded. The payload includes the platform order id, order number, store id, payment and fulfillment statuses, total, currency, line items, actor metadata, and externalReference when supplied.

Treat these callbacks as state synchronization events. Do not ask the AI to infer whether an order was created, paid, dispatched, or delivered.

For current_chat flows, do not ask the customer for a payer phone number and do not send a typed phone number to the bridge. Use the context.currentChat.token; the platform derives the payer from the active WhatsApp chat.

Preserve delivery addresses exactly as the customer provides them. If the customer says Beth House, Kasarani, House 16, send that complete value as customer.deliveryAddress. Do not reduce it to Kasarani, because house, building, estate, road, gate, room, and landmark details are fulfillment-critical.

The order is created from platform-owned catalog records. Products keep their images from the catalog. Services do not require images and are represented by description, duration, price, and service state.

Before a plugin can send externalId items, the merchant grants plugin:ecommerce:catalog:sync and syncs the plugin catalog in the dashboard. The platform stores durable E-Commerce catalog rows keyed by sourcePlugin and sourceExternalId. During order creation, the bridge resolves the plugin SKU or service ID to the platform-owned catalog item. This avoids hard-coding platform catalog IDs inside the plugin.

If the plugin already knows a platform catalog item ID, it may still send:

json
{
  "items": [
    {
      "itemId": "catalog_13kg_refill",
      "quantity": 1
    }
  ]
}

Message and scheduling bridges

Immediate text message:

json
{
  "organizationId": "org_123",
  "instanceId": "inst_456",
  "serviceName": "GAS_OS",
  "idempotencyKey": "gas-order-GAS-2041-reminder-now",
  "recipient": {
    "type": "current_chat",
    "token": "<context.currentChat.token>"
  },
  "text": "Your gas delivery has been assigned and is now being prepared."
}

Scheduled text message:

json
{
  "organizationId": "org_123",
  "instanceId": "inst_456",
  "serviceName": "GAS_OS",
  "idempotencyKey": "gas-order-GAS-2041-reminder-later",
  "recipient": {
    "type": "current_chat",
    "token": "<context.currentChat.token>"
  },
  "name": "Gas delivery follow-up",
  "runAt": 1778172000000,
  "timezone": "Africa/Nairobi",
  "text": "Please confirm you received your gas delivery."
}

Gas OS scenario

This example shows how the AI can use fewer tools while the plugin and bridges handle state.

Merchant setup

The gas merchant installs the Gas OS plugin and grants it to the sales WhatsApp number instance.

The WhatsApp number instance has:

  • E-Commerce catalog item synced from Gas OS: 13kg cooking gas refill, price KES 3,150.
  • Delivery fee rule: KES 200 inside Nairobi.
  • Active payment provider enabled for mobile-money collection.
  • Gas OS plugin permissions:
    • gas:orders:create
    • gas:orders:read
    • plugin:ecommerce:orders:create:current_chat
    • plugin:ecommerce:checkout:initiate
    • plugin:payments:initiate:current_chat
    • plugin:payments:status:own
    • plugin:messages:send:current_chat

The agent only needs a small plugin tool surface:

  • gas_quote_order
  • gas_confirm_order
  • gas_check_delivery

It does not need raw payment, scheduling, order persistence, or receipt tools exposed directly. Those platform capabilities run as dependency-only bridge targets.

Customer conversation

text
Customer: Hi, I need 13kg gas delivered to Beth House, Kasarani, House 16.
AI: I can help. Do you need a refill or a new cylinder?
 
Customer: Refill.
AI calls Gas OS tool: gas_quote_order
Tool input: { "product": "13kg refill", "area": "Beth House, Kasarani, House 16" }
 
Gas OS returns:
{
  "quoteId": "GQ-2041",
  "summary": "13kg refill + Beth House, Kasarani, House 16 delivery",
  "total": 3350,
  "currency": "KES",
  "requiresConfirmation": true
}
 
AI: The total is KES 3,350 including delivery. Should I place the order and send payment instructions?
 
Customer: Yes.
AI calls Gas OS tool: gas_confirm_order
Tool input: { "quoteId": "GQ-2041", "customerConfirmed": true, "deliveryAddress": "Beth House, Kasarani, House 16" }

Internal bridge work

Gas OS now performs deterministic work server-to-server.

  1. Gas OS calls /plugins/bridges/ecommerce/orders/create.
  2. KasiLabs verifies the signature and permissions.
  3. KasiLabs creates the durable commerce order.
  4. Because checkout is included, KasiLabs creates the payment request.
  5. KasiLabs sends the customer payment instructions.
  6. The bridge stores the idempotency key and response.

The AI does not need to remember the order or invent a payment reference.

text
AI: I have created order GAS-2041. Please complete the payment instructions I sent. Once payment is confirmed, I will continue with delivery updates.

Payment callback

The customer pays.

  1. The payment network confirms payment to KasiLabs.
  2. KasiLabs marks the transaction successful.
  3. KasiLabs marks the commerce order paid.
  4. KasiLabs generates the standard receipt PDF and sends one WhatsApp document message with the receipt summary as the caption.
  5. KasiLabs calls Gas OS record_payment_event.
  6. Gas OS marks its own order paid and ready for assignment.
  7. The agent is resumed with a system event saying payment completed.
text
AI: Payment received for order GAS-2041. Your refill is now being prepared for delivery to Beth House, Kasarani, House 16.

Why this is deterministic

The AI did not decide whether payment happened. The payment callback did.

The AI did not persist the order from memory. The commerce bridge did.

The AI did not independently send a receipt. The payment workflow sent the single receipt document message.

The plugin did not bypass platform permissions. The bridge verified them before each action.

Failure examples

Missing payment permission:

json
{
  "ok": false,
  "error": "Plugin is missing permission: plugin:payments:initiate:current_chat"
}

Unknown external recipient without external-recipient access:

json
{
  "ok": false,
  "error": "Plugin is missing permission: plugin:messages:send:external_recipient"
}

In this error, "this instance" means the WhatsApp number instance named by instanceId in the bridge request.

Duplicate retry with same idempotency key:

json
{
  "ok": true,
  "duplicate": true,
  "bridge": "payments",
  "action": "initiate",
  "idempotencyKey": "gas-order-GAS-2041-payment-v1",
  "result": {
    "reference": "pay_..."
  }
}