Skip to content

Webhooks, cron and uploads

Handlers live in packages/core/src/http/ and are mounted in the mounts array in apps/api/src/index.ts. The web app serves a few of them too, through one-line re-exports, until every external caller points at the API host.

Endpoint Source
POST /api/webhooks/stripe Stripe
POST /api/webhooks/autosend Autosend (email delivery events)
POST /api/webhooks/whatsapp apps/whatsapp-gateway

Rules for the Stripe webhook, learned the hard way:

  • Idempotency is not optional. payments.stripe_payment_intent_id is unique because Stripe redelivers payment_intent.succeeded.
  • Never throw on a structural problem. Capture it, log it, return 200. A 500 makes Stripe retry for three days against something that will never resolve.
  • A succeeded PaymentIntent always leaves a row — a booking with a payment, or an orphan payment flagged for review.

The WhatsApp gateway spools webhooks it cannot deliver and retries, so a deploy window does not lose inbound messages. It posts through Traefik rather than a container name so a restarting replica cannot black-hole traffic.

Coolify scheduled tasks POST to these:

Endpoint Job
/api/cron/cleanup-expired Expire stale holds, send the abandonment email
/api/cron/send-reminders Booking reminders
/api/cron/auction-close Close auctions
/api/cron/auction-lifecycle Auction state transitions
/api/cron/auction-chat-retention Trim auction chat history
/api/cron/sync-emails Email sync
/api/cron/cdp-nightly Nightly CDP build

cleanup-expired is part of the payment path — changing it means an entry in docs/booking-payments/decisions.md.

Endpoint Purpose
POST /api/upload User upload
POST /api/admin/upload Admin upload
POST /api/admin/upload/presign Presigned R2 URL
POST /api/profile/avatar Avatar

Storage is Cloudflare R2. CLOUDFLARE_R2_BUCKET differs per environment (tcgkl / tcgkl-beta) and CLOUDFLARE_R2_PUBLIC_URL has no trailing slash — a double slash in an asset URL is almost always that.

  1. Handler in packages/core/src/http/.
  2. Mount it in the mounts array in apps/api/src/index.ts.
  3. Only if the web origin must serve it too, add the path to PROXIED_PREFIXES or PROXIED_EXACT in apps/web/src/proxy.ts.

Nothing with business logic goes in apps/web/src/app/**/route.ts.