This guide is for developers building new StoreTalk Apps. An app is a standalone web service that receives signed webhooks from StoreTalk and returns results for flows to act on. You can build one in any language — we publish a TypeScript SDK that handles the protocol for you.Documentation Index
Fetch the complete documentation index at: https://help.storetalk.app/llms.txt
Use this file to discover all available pages before exploring further.
Architecture overview
- Customer triggers a flow → reaches an App node
- StoreTalk calls your app’s webhook with
session.create - Your app returns a session URL (your web UI)
- StoreTalk sends the URL as a WhatsApp button to the customer
- Customer completes the action in your web UI
- Your app calls StoreTalk’s callback URL with
session.complete - Flow resumes with the variables you return
Two-phase lifecycle
StoreTalk apps run in two distinct phases:Session phase
The customer is actively engaged. The flow is paused at the App node. Status pushes move the session forward. A terminal status (like
order_confirmed) completes the session and resumes the flow.Fulfillment phase
The flow has already finished. Your app continues working in the background (packing an order, tracking delivery). Each status push sends a WhatsApp message directly to the customer — no flow involvement.
order_confirmed. All the delivery updates happen in fulfillment without the flow needing to wait.
Start with the SDK
Install the official SDK:app.complete():
{{result}} available as a variable.
The manifest
Every app must exposeGET /api/manifest. StoreTalk reads it when registering the app to learn what actions and events it supports.
StoreTalkApp config. Just mount it:
Fulfillment updates
After a session completes, you can keep sending updates:URL shortener
When your app sends a customer-facing URL via WhatsApp — a booking link, a meeting join link, an order tracking page — the URL usually carries a JWT or signed access token in the query string and balloons to ~300 characters. WhatsApp doesn’t auto-shorten, so the customer’s message renders as a wall of URL with the actual content (provider name, time, location) buried below. The platform shortener (s.storetalk.app) collapses these to ~28 characters. Apps call it via the SDK; the response is a redirect URL ready to embed in a message.
expiresAt is required
There is no default. Caller must scope expiresAt to the underlying token or entity validity:
- Booking flows:
endTime + 24h grace - Order tracking: delivery deadline + a few days
- Cart recovery: the cart’s TTL
- JWT-bearing URLs: prefer
shortenJwtUrl()— extracts theexpclaim automatically (no signature verification; that happens at the destination)
Revoking on cancellation
Tag every URL minted for a logical entity with the samerefId, then revoke them all in one call when the entity reaches a terminal state (booking cancelled, order refunded, cart abandoned past TTL):
revoked: 0.
Best-effort fallback
Ifs.storetalk.app is unreachable, your app should log and fall back to the long URL rather than block the WhatsApp message. The customer still gets a working link, just unbranded:
Where to wire it in
Audit every code path where your app builds a customer-facing URL that goes into a WhatsApp message — typically:- Confirmation messages (
app.complete()after a booking/order is finalised) - Reminders / scheduled notifications (
app.scheduleNotify()— shorten before the message body is rendered, since the rendered text is stored in the scheduler payload) - Lifecycle pushes (
app.pushFulfillment(),app.completeFulfillment()) - Direct notifications (
app.notify())
src/lib/shortener.ts) so all paths agree on the refId convention and expiresAt policy. The booking-pro app’s lib/booking-shortener.ts is the canonical example.
Service contracts
Some capabilities are too universal to bake into every app — taking a payment, hosting a video meeting, listing on a marketplace. StoreTalk exposes them as service contracts. Your app declares either that it provides one (you’re a payment gateway, a video host, etc.) or that it consumes one (you need payments, you need meetings) — and Core wires the two sides together at runtime. Two contracts ship today:| Contract slug | Purpose |
|---|---|
payment | Collect online payments — UPI, cards, net banking |
meet | Create and cancel video-meeting rooms, return a join URL |
Declaring a contract in your manifest
A provider lists what it offers underserviceContracts:
serviceDependencies:
Calling a service (consumer side)
requestService(contractSlug, payload) is a single HMAC-signed call to Core. Core resolves the provider (see below), forwards a service.request webhook to that provider, waits for its synchronous response (10s timeout), and returns the body to you as result.data.
For long-running operations, the provider may also send a follow-up service.response webhook. Subscribe to it from the consumer side:
Handling a service request (provider side)
return becomes the consumer’s result.data. To respond asynchronously after some background work, store the event.delivery_id and call respondService(deliveryId, status, data) later.
How Core picks a provider
When more than one app provides the same contract on a tenant (e.g. both StoreTalk Meet and StoreTalk Zoom are installed):- The tenant’s explicit default for that contract wins, if set
- Otherwise, if exactly one provider is installed, it’s used automatically
- Otherwise the request fails with a clear error
TenantServiceDefault. Consumer apps don’t pick a provider; Core does.
Graceful degradation
If no provider is installed or all are disabled,requestService() rejects with a descriptive InvalidOperationException. Catch it and degrade gracefully rather than failing the whole flow — the user-facing docs promise this, and well-built apps deliver it:
payment is unavailable, and skips meeting-link generation when meet is unavailable.
Deploy ordering — non-negotiable when extending a contract
Core does not enforce a deploy order at install time, but versioning a contract is fragile. When you add a new action, field, or behaviour to an existing contract, ship the provider first, then the consumer. A real example from PR #99 (booking-pro + StoreTalk Meet, 2026-04-28): we extended themeet contract with action: 'cancel'. The provider apps (StoreTalk Meet, StoreTalk Zoom) shipped first to start accepting the new action. Only then did booking-pro start emitting it. Had it shipped the other way, the consumer’s cancel request would have hit an old provider that didn’t recognise the action — and a Zoom handler that defaults to “create on unknown action” would have spawned a junk room every time a customer cancelled.
Apply the same rule for any breaking shape change to the request or response payload. New optional fields are safe in either direction; new required fields and new action verbs are not.
Security
Every webhook from StoreTalk is signed with HMAC-SHA256. The SDK verifies signatures automatically. For manual verification:Multi-tenancy
A single app instance serves all tenants that install it. Every webhook carriestenant_id and install_id fields. Your app is responsible for isolating data by install_id.
Use install_id as the tenant key throughout your database. Never trust cross-tenant data.
Debug inspector
The SDK ships with a built-in debug UI. Set theDEBUG_TOKEN env var (or let StoreTalk sync one via admin handshake) and mount it:
Full contract reference
This guide covers the basics. For the complete contract — every event, every header, every error response — see:- SDK package on GitHub Packages — TypeScript source and type definitions
- App webhook contract spec — canonical protocol reference
- App lifecycle pipeline spec — why the two-phase model, billing implications, variable declaration
Getting listed
Once your app is working:- Deploy your webhook endpoint at a stable public URL with HTTPS
- Ensure
/api/manifestreturns correctly - Contact the StoreTalk team with your manifest URL to begin the review process
- Approved apps appear in the tenant marketplace
Apps overview
How apps fit into StoreTalk from a tenant perspective.
Managing apps
What the tenant-facing management experience looks like.