Live in production

Scheduled messaging, durable by default.

A BullMQ + Redis job queue that powers every reminder, campaign, abandoned-cart nudge, and drip sequence on the StoreTalk platform. Multi-tenant, observable, SDK-driven — available to first-party and third-party apps alike.

app.ts
await app.scheduleNotify({
installId: tenantId,
customerId: 'cust_8f...',
message: 'Reminder: appt at 11 AM',
fireAt: new Date('2026-04-29T05:00:00Z'),
refId: 'booking_b7d2'
});

// → { jobId: "j_3k9...", status: "queued" }

Pending jobs

142 total

Booking reminder

booking_b7d2 · in 4h 23m

Abandoned cart

cart_a12 · in 1h 47m

Renewal reminder

sub_99k · in 2 days

For

⚙️StoreTalk app developers
🏢Enterprise admins
📊Marketing operations teams
🔌Third-party app builders (SDK)
What it does

Infrastructure that every conversational app needs

Conversational commerce runs on time-delayed messages. Every app would otherwise build the same Redis-backed scheduler again. We built it once.

Schedule a message for any future moment

Booking reminder 24 hours before appointment. Abandoned-cart nudge 2 hours after browsing. Renewal alert 7 days before subscription expiry. The Scheduler holds delayed jobs durably in Redis and fires them at the precise moment, even if the app server restarts in between.

  • Schedule by absolute timestamp or relative delay
  • Multi-tenant isolation — each tenant's jobs in a separate queue
  • Sub-minute precision for time-critical alerts
  • Time-zone aware — fires in tenant's local TZ, not UTC
  • Up to 90-day delay supported out of the box

Cron patterns for recurring sends

Weekly subscription renewal reminder, daily fitness class summary, monthly invoice for B2B customers, quarterly health-check nudge. Standard cron syntax, with named templates for common patterns ("every Monday 9 AM IST", "first of every month").

  • Standard cron syntax — minute, hour, day, month, weekday
  • Named patterns for non-developer users
  • Per-recurring-job customer-list update
  • Pause and resume without losing the schedule
  • Bounded by tenant subscription limits

Cancel by reference ID — clean state

A customer pays before the abandoned-cart reminder fires. Their booking is rescheduled before the reminder goes out. Each scheduled job has a reference ID (typically the booking ID, order ID, or campaign ID); cancel pending jobs by reference and the system cleans up gracefully.

  • Cancel a single job by reference ID
  • Bulk cancel — all jobs matching a tenant + ref pattern
  • Cancel-and-replace pattern for stateful workflows
  • Idempotent cancellation — safe to call repeatedly
  • Audit log of every cancellation for support traceability

HMAC-signed callbacks for every fire

When a scheduled job fires, the Scheduler doesn't send the WhatsApp message itself — it calls back to StoreTalk Core with an HMAC-signed payload. Core then resolves the customer's active channel (WhatsApp, web, voice) and dispatches the message. Same security model as inbound webhooks.

  • HMAC-SHA256 signature on every callback request
  • Replay protection — timestamps with skew tolerance
  • Retry with exponential backoff on callback failure
  • Dead-letter queue for jobs that fail repeatedly
  • Configurable per-job callback URL (advanced use cases)

Powers every StoreTalk app — and yours

E-commerce uses it for abandoned-cart reminders. BookingPro for appointment reminders. Campaign Manager for batch sends. Loyalty programmes for renewal nudges. Third-party apps installed via the StoreTalk SDK get the same scheduler with zero extra setup — just call app.scheduleNotify().

  • Single-line SDK call — no infra to provision
  • Per-app job namespace — your jobs, your IDs
  • Cross-app job inspection forbidden — strict isolation
  • Documented in the public SDK reference
  • Free for tenant-routed jobs; metered for high-volume apps

Observable — see every pending job

Operations transparency matters. The Scheduler dashboard shows pending jobs per tenant, fire-time histograms, success/failure rates per app, and a live activity feed. Debug a "why didn't this reminder fire?" question without opening Redis.

  • Per-tenant pending jobs view
  • Fire-time histogram — see scheduling load by hour
  • Per-app success / failure / retry stats
  • Live activity feed for the last 1,000 fires
  • Prometheus metrics endpoint for your monitoring stack
How it works

SDK call → durable queue → callback to Core

1

Your app calls scheduleNotify() via the SDK

One line: app.scheduleNotify({ installId, customerId, message, fireAt, refId }). The SDK signs the request, posts to scheduler.storetalk.app, and gets back a job ID.

2

Scheduler enqueues the job in Redis durably

The job lands in BullMQ's Redis-backed queue. Survives Redis restart (RDB + AOF persistence), survives Scheduler restart. Held until fireAt.

3

At fireAt, Scheduler hits Core, Core dispatches

The Scheduler doesn't send WhatsApp directly — it calls Core's /api/app-events/scheduled-fire endpoint. Core looks up the customer's active channel and sends the message there.

What it powers

Used by every StoreTalk app

Booking reminders

24h + 1h before appointment

Abandoned-cart recovery

2h after cart abandonment, with discount

Subscription renewal

7d before renewal, again 1d before

Loyalty programme nudges

Monthly redemption reminders

Campaign batch sends

Schedule a marketing blast for 9 AM tomorrow

Drip sequences

Day-1, Day-3, Day-7 onboarding messages

Drop-off recovery

Reminder if a flow stalled mid-conversation

Compliance reports

Monthly auto-export to enterprise tenants

FAQ

Frequently asked

Is the Scheduler something tenants interact with directly?
Mostly no. The Scheduler is infrastructure that powers other StoreTalk apps. Tenants benefit from it indirectly — every "reminder" or "scheduled message" feature in the platform routes through it. Developers building third-party apps on the StoreTalk SDK call it directly via app.scheduleNotify().
Why a separate service instead of in-process queues?
Three reasons: (1) Durability — Scheduler runs on a dedicated server with persistence, surviving app restarts. (2) Multi-app sharing — every app gets the same scheduling primitive without each app re-implementing it. (3) Operational visibility — one dashboard for every tenant's pending jobs.
Where does it run?
Dedicated Hetzner CX23 instance in EU-Central with Redis 7 persistence. Latency to Core (Mumbai) is well under callback timeouts. Migration to AWS ap-south-1 is on the roadmap as part of the broader Hetzner → AWS plan.
What happens if the callback fails?
Exponential backoff retry: 30s, 2m, 10m, 1h, 6h. After all retries fail, the job goes to a dead-letter queue with the failure reason. Dead-letter jobs are inspected by the engineering team and replayed manually if appropriate.
Can I see what's scheduled for my tenant?
Yes. The Scheduler dashboard (accessible from your StoreTalk admin panel) shows pending jobs filtered by your tenant, including which app scheduled them, what reference ID they're tied to, and when they'll fire.
How does it scale?
BullMQ on Redis scales horizontally — the Scheduler fans out workers as queue depth grows. The bottleneck is Core's callback ingestion, which is sized for current platform throughput with headroom. Plan for 1,000+ concurrent jobs/sec at the 10K-tenant target.
Is there a public API or only the SDK?
SDK is the recommended path — it handles auth, signing, and SDK-versioned payload formatting. Direct REST API exists for internal tooling but is not stable for external consumers; use the SDK.
What does it cost a tenant?
Free for normal tenant usage (booking reminders, abandoned-cart, scheduled campaigns within plan limits). Third-party apps that schedule heavy volumes are metered after a generous free tier. See the developer guide for current pricing.

Build apps on the same scheduler we use.

Read the SDK docs to start scheduling delayed and recurring messages from your own StoreTalk app.