The extension API

Every route an installed extension is allowed to call — plus the admin-only management surface used to install, configure and reload extensions. For the architecture behind this API, see how extensions work.

In developmentThis reference describes the designed shape of the API. Endpoints, fields and error codes are still subject to change before the SDK ships.

Overview

The API is versioned in the URL and organised into two surfaces: the extension surface (/v1/ext/…), scoped to a single installed extension, and the management surface (/v1/extensions/…), gated to the admin:extensions:manage privilege.

Base URL      https://api.tabibu.health
Extension API /v1/ext/...
Management API /v1/extensions/...
Sandbox       https://sandbox.tabibu.health

Breaking changes ship as a new major version with a twelve-month overlap. Additive changes (new fields, new endpoints) do not bump the version.

Authentication

Every request carries a bearer token scoped to one extension and one facility. Sandbox keys are prefixed sk_sandbox_, live keys sk_live_. Keys are issued once, at install time, and can be rotated without downtime.

curl https://api.tabibu.health/v1/ext/billing/invoices/inv_4Kp2 \
  -H "Authorization: Bearer sk_live_51Hk..." \
  -H "Tabibu-Version: 2026-06-01"

Errors

Errors return a 4xx or 5xx status with a JSON body under an error key.

{
  "error": {
    "type": "validation_error",
    "message": "amount must be greater than 0",
    "param": "amount"
  }
}
StatusTypeMeaning
400invalid_requestMalformed JSON or a required field is missing.
401unauthenticatedThe API key is missing, expired or revoked.
403insufficient_scopeThe key's privileges don't cover this route.
404not_foundThe resource doesn't exist, or isn't visible to this key.
409idempotency_conflictThe same Idempotency-Key was sent with a different body.
422validation_errorThe request parsed, but a value failed validation.
429rate_limitedToo many requests — see Retry-After.
500internal_errorSomething failed on our side. Safe to retry.

Idempotency

RabbitMQ delivery is at-least-once, so a write may be retried after a redeliver. Send an Idempotency-Key header on every POST — Tabibu returns the original response for a repeated key instead of processing it twice. Keys are held for 24 hours.

Idempotency-Key: mpesa_ws_CO_070926183451_4821

Rate limits

120 requests per minute per extension key, refilled continuously. A throttled request returns 429 rate_limited with a Retry-After header, in seconds.

Pagination

List endpoints are cursor-paginated. Pass limit (default 20, max 100) and starting_after with the last ID from the previous page.

GET /v1/extensions?limit=20&starting_after=ext_mpesa_payments

{
  "data": [ ... ],
  "has_more": false
}

Extension management

GET/v1/extensionsadmin:extensions:view

List installed extensions with status, version and last-updated time.

FieldTypeDescription
categorystringFilter by category, e.g. payment, insurance.
enabledbooleanFilter to enabled or disabled extensions only.

Request

curl https://api.tabibu.health/v1/extensions?category=payment \
  -H "Authorization: Bearer sk_admin_..."

Response

{
  "data": [
    {
      "name": "mpesa-payments",
      "version": "1.2.0",
      "category": "payment",
      "is_enabled": true,
      "status": "running"
    }
  ],
  "has_more": false
}
POST/v1/extensionsadmin:extensions:manage

Install an extension from a registry manifest URL, or a local manifest for development.

FieldTypeDescription
manifest_urlstring, requiredRegistry URL to the manifest.toml for the version to install.
configobjectValues for the fields declared under [extension.config].

Request

curl -X POST https://api.tabibu.health/v1/extensions \
  -H "Authorization: Bearer sk_admin_..." \
  -d manifest_url="https://registry.tabibu.dev/mpesa-payments/1.2.0" \
  -d config[MPESA_SHORTCODE]="174379"

Response

{
  "name": "mpesa-payments",
  "version": "1.2.0",
  "status": "installing",
  "api_key_id": "key_9Fn2..."
}
PUT/v1/extensions/:name/configadmin:extensions:manage

Update configuration values. Secret fields can be replaced but are never read back — the response only confirms is_configured.

Request

curl -X PUT https://api.tabibu.health/v1/extensions/mpesa-payments/config \
  -H "Authorization: Bearer sk_admin_..." \
  -d MPESA_ENV="production"

Response

{
  "MPESA_SHORTCODE": { "value": "174379" },
  "MPESA_PASSKEY": { "is_configured": true },
  "MPESA_ENV": { "value": "production" }
}
POST/v1/extensions/:name/reloadadmin:extensions:manage

Zero-downtime hot reload. A new container starts and begins consuming before the old one drains and exits — no message is lost.

Request

curl -X POST https://api.tabibu.health/v1/extensions/mpesa-payments/reload \
  -H "Authorization: Bearer sk_admin_..."

Response

{ "name": "mpesa-payments", "status": "reloading" }
GET/v1/extensions/:name/ui-tokenrequired_privilege

Issue a short-lived, scoped token for an extension's WebView. Rejected with 403 if the requesting user lacks the manifest's required_privilege.

Request

curl https://api.tabibu.health/v1/extensions/mpesa-payments/ui-token \
  -H "Authorization: Bearer <user_session_token>"

Response

{ "token": "wvt_eyJhbGciOi...", "expires_in": 900 }

Billing extension API

GET/v1/ext/billing/invoices/:idext:billing:read

Retrieve an invoice to process a payment against — amount, currency, patient reference and current status.

Request

curl https://api.tabibu.health/v1/ext/billing/invoices/inv_4Kp2 \
  -H "Authorization: Bearer sk_live_51Hk..."

Response

{
  "id": "inv_4Kp2",
  "patient_id": "pat_9K2mXw",
  "amount": 1500.0,
  "currency": "KES",
  "status": "pending_payment"
}
POST/v1/ext/billing/payments/:id/recordext:billing:write

Record a completed payment against an invoice. Tabibu marks the invoice paid and publishes billing.payment_completed.

FieldTypeDescription
referencestring, requiredThe provider's transaction reference, e.g. an M-Pesa receipt number.
amountnumber, requiredAmount actually received, in the invoice's currency.
methodstringHow it was paid — mpesa, card, cash.

Request

curl -X POST https://api.tabibu.health/v1/ext/billing/payments/inv_4Kp2/record \
  -H "Authorization: Bearer sk_live_51Hk..." \
  -H "Idempotency-Key: mpesa_ws_CO_070926183451_4821" \
  -d reference="QGR7XXYZ1" \
  -d amount=1500.0 \
  -d method="mpesa"

Response

{ "id": "inv_4Kp2", "status": "paid", "reference": "QGR7XXYZ1" }
POST/v1/ext/billing/payments/:id/failext:billing:write

Record a failed or cancelled payment attempt, so the invoice returns to pending_payment with a reason on file.

FieldTypeDescription
reasonstring, requiredShort machine-readable reason, e.g. insufficient_funds, cancelled.

Request

curl -X POST https://api.tabibu.health/v1/ext/billing/payments/inv_4Kp2/fail \
  -H "Authorization: Bearer sk_live_51Hk..." \
  -d reason="cancelled"

Response

{ "id": "inv_4Kp2", "status": "pending_payment", "last_failure": "cancelled" }

Control

POST/v1/ext/control/drain-completeext:control:write

Called by the SDK when an extension has finished draining in-flight work after SIGTERM. Tells Tabibu it's safe to stop the container immediately, instead of waiting out the grace period.

Request

curl -X POST https://api.tabibu.health/v1/ext/control/drain-complete \
  -H "Authorization: Bearer sk_live_51Hk..."

Response

{ "acknowledged": true }