StoreOSv0.15.0
Plugins

Webhooks & callbacks

The four public endpoints StoreOS exposes per tenant for courier and payment providers, and how each one is authenticated.

Why these exist

Couriers and payment gateways report back asynchronously — a parcel is picked up, a card payment settles. StoreOS exposes one public endpoint per provider per tenant, so each merchant pastes their own URL into their own provider dashboard.

These endpoints live on the storefront API, not core-api, because providers call them from the public internet.


Get the URLs

Never hand-assemble them. Ask the API, so the base URL stays correct across environments:

query WebhookUrls {
  plugin__webhookUrls {
    provider
    kind
    url
  }
}
{
  "data": {
    "plugin__webhookUrls": [
      { "provider": "sslcommerz", "kind": "webhook",  "url": "https://storefront-api.storeos.dev/payment/webhook/sslcommerz/acme" },
      { "provider": "bkash",      "kind": "callback", "url": "https://storefront-api.storeos.dev/payment/callback/bkash/acme" },
      { "provider": "pathao",     "kind": "webhook",  "url": "https://storefront-api.storeos.dev/courier/webhook/pathao/acme" },
      { "provider": "steadfast",  "kind": "webhook",  "url": "https://storefront-api.storeos.dev/courier/webhook/steadfast/acme" }
    ]
  }
}

All four are returned whether or not the plugin is configured — they are addresses, not status. The trailing segment is the tenant UID.

webhook vs callback

kindWho calls itDirection
webhookThe provider's server, unattendedServer → server, POST
callbackThe customer's browser, mid-checkoutRedirect, GET, ends in a redirect back to your storefront

bKash is the only callback — the shopper is bounced through it and then redirected onward, so it must stay reachable from the public web, not just from a provider's IP range.


Routes and authentication

ProviderMethodRouteAuthenticated by
PathaoPOST/courier/webhook/pathao/:tenantX-Pathao-Signature header vs webhook_secret
SteadfastPOST/courier/webhook/steadfast/:tenantAuthorization: Bearer … vs webhook_token
SSLCOMMERZPOST/payment/webhook/sslcommerz/:tenantGateway IPN payload validation
bKashGET/payment/callback/bkash/:tenantSigned query params from bKash

Courier secrets are compared in constant time, so a wrong token fails identically no matter how many leading characters happen to match.

A webhook for a tenant that has not configured that courier returns 401, not 404 — an unconfigured plugin is treated as "not authorized", never as an open endpoint.


Pathao

Pathao's integration check is strict, and StoreOS implements it exactly:

RequirementBehavior
HeaderX-Pathao-Signature must equal plugins.courier.pathao.webhook_secret
StatusResponds 202 Accepted
Response headerEchoes X-Pathao-Merchant-Webhook-Integration-Secret
Test payload{ "event": "webhook_integration" } is acknowledged without touching any order

So the first thing Pathao sends is a handshake, not an order event. If the merchant's dashboard shows the integration as verified, the secret matched.

Order events are dispatched by event name and update the matching shipment.


Steadfast

RequirementBehavior
HeaderAuthorization: Bearer {webhook_token}
StatusResponds 200 OK
Body{ "status": "success", "message": "Webhook received successfully." }

Steadfast sends two payload shapes — delivery status updates and tracking updates — both on the same URL.


Payments

SSLCOMMERZ posts an IPN to its webhook URL. StoreOS validates it against the tenant's active credential set and moves the payment attempt forward.

bKash redirects the shopper's browser to the callback with paymentID, status, signature, version, and product in the query string. StoreOS verifies, records the result, and issues a 302 back into your storefront — your app only ever sees the final landing page.

Which credentials are used depends on activeMode. A store in sandbox mode is validated against sandbox keys, so test payments on a live URL still work.


Setup order

The sequence matters, because a provider will test the URL the moment it is saved:

  1. Configure the plugin first — plugin__configure with the courier's webhook_secret / webhook_token.
  2. Read the URL — plugin__webhookUrls.
  3. Paste it into the provider dashboard and let their integration test run.
  4. Validate — plugin__validateCourier confirms the API credentials independently of the webhook.

Pasting the URL before step 1 makes the provider's test fail with 401, and some dashboards then refuse to re-save until the URL is changed and changed back.


Local development

The routes are built from STOREFRONT_API_PUBLIC_URL. On a laptop that value points at localhost, which no provider can reach, so plugin__webhookUrls returns URLs nobody will ever call.

Either point that variable at a tunnel (so the returned URLs are genuinely reachable), or test the handler directly:

curl -X POST http://localhost:4000/courier/webhook/steadfast/acme \
  -H "Authorization: Bearer $STEADFAST_WEBHOOK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"consignment_id":"…","status":"delivered"}'
curl -i -X POST http://localhost:4000/courier/webhook/pathao/acme \
  -H "X-Pathao-Signature: $PATHAO_WEBHOOK_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"event":"webhook_integration"}'

The second one should come back 202 with the echoed integration header — the same check Pathao itself runs.

On this page