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.
Webhooks
Section titled “Webhooks”| 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_idis unique because Stripe redeliverspayment_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.
Uploads
Section titled “Uploads”| 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.
Adding an endpoint
Section titled “Adding an endpoint”- Handler in
packages/core/src/http/. - Mount it in the
mountsarray inapps/api/src/index.ts. - Only if the web origin must serve it too, add the path to
PROXIED_PREFIXESorPROXIED_EXACTinapps/web/src/proxy.ts.
Nothing with business logic goes in apps/web/src/app/**/route.ts.
