# Integrate on any stack

> One kit of best practices per stack (Next.js + Vercel, Node, Python, Laravel, edge), the contract every integration meets, and how to port it to any other stack.

`npx talo-hub skills install` installs the core skills plus the kit for the project's stack, detected from the project root. `--stack <id>` picks kits by hand (comma-separated, or `all`), and `npx talo-hub skills list` shows them. Every kit is a SKILL.md you can also read directly:

| Kit | Stack | Detected by |
|---|---|---|
| [nextjs-vercel](https://talopartnerhub.vercel.app/skills/talo-stack-nextjs-vercel/SKILL.md) | Next.js + Vercel | package.json: next |
| [node-express](https://talopartnerhub.vercel.app/skills/talo-stack-node-express/SKILL.md) | Node.js (Express, Fastify, NestJS, Koa) | package.json: express, fastify, @nestjs/core, koa |
| [python](https://talopartnerhub.vercel.app/skills/talo-stack-python/SKILL.md) | Python (FastAPI, Django, Flask) | requirements / pyproject: fastapi, django, flask; files: manage.py |
| [laravel](https://talopartnerhub.vercel.app/skills/talo-stack-laravel/SKILL.md) | PHP + Laravel | composer.json: laravel/framework; files: artisan |
| [edge](https://talopartnerhub.vercel.app/skills/talo-stack-edge/SKILL.md) | Edge (Cloudflare Workers, Supabase Edge Functions, Deno) | package.json: wrangler, hono; files: wrangler.toml, wrangler.jsonc, wrangler.json, supabase/functions, deno.json |

No kit for your stack? Every kit implements the contract below with its framework's tools; do the same with yours.

## The contract

1. **Server only.** The key (`TALO_HUB_API_KEY`) and the webhook secret (`TALO_HUB_WEBHOOK_SECRET`) live in server-side environment variables: sandbox (`tph_test_`) and production (`tph_live_`) in different environments, never in client code, the repo or logs. The browser only gets the payment's cvu, alias, amount, status and expires_at, or, for the widget, its id and read-only `checkout_token`.
2. **Create at checkout, idempotently.** `POST /api/v1/payments { amount, external_id: <order id>, motive }` from the server, and store `payment.id` on the order. Retrying with the same external_id returns the same payment, so retries never charge twice. After EXPIRED, use a new external_id.
3. **Verify every webhook on the raw body.** Read the body bytes before any JSON parser, then check `talo-hub-signature`: `t` within 300 seconds of now, `v1` exactly 64 lowercase hex characters, equal to HMAC-SHA256(secret, `<t>.<raw body>`) compared in constant time. Answer 400 when it fails. Your verifier must pass the [test vector](https://talopartnerhub.vercel.app/docs/webhooks.md). Unsigned events relayed by `npx talo-hub events listen --forward-to` (header `talo-hub-forwarded: 1`) are accepted only in local development: `NODE_ENV === "development"` under `next dev`, or elsewhere an explicit `TALO_HUB_DEV_FORWARD=1` (`true` in Laravel) together with a `tph_test_` key, set only in the local env file.
4. **Keep the route reachable.** Nothing in front of the webhook path may redirect, challenge or ask for a login: no CSRF check, session middleware, bot protection or deployment protection. Register the final https URL, the one that answers without redirecting.
5. **Answer 2xx within 10 seconds,** and 2xx again for an event you already handled. Record what you need, answer, then do the slow work (emails, invoices).
6. **Handle each event once, in durable storage.** Record event ids under a unique constraint in your database or Redis. Not in memory: serverless functions and multiple instances don't share it.
7. **Re-read before acting.** `GET /api/v1/payments/{id}` and decide from that status, not the event's snapshot: SUCCESS or OVERPAID → fulfil, exactly once, with a conditional update (`UPDATE orders SET fulfilled_at = now() WHERE payment_id = $1 AND fulfilled_at IS NULL`); UNDERPAID → ask for the rest; EXPIRED → offer a new payment.
8. **Reconcile.** A scheduled job re-reads the orders whose payment is still PENDING or UNDERPAID and applies rule 7, in case a delivery never arrived.
9. **Poll only for the buyer's screen.** Every 3 to 10 seconds while the order page is open, stopping at SUCCESS, OVERPAID or EXPIRED, or let the [`<talo-checkout>` widget](https://talopartnerhub.vercel.app/docs/checkout.md) do it with the payment's checkout_token. Fulfilment happens in rules 7 and 8, never in the browser.
10. **Test it in sandbox** with the [sandbox plan](https://talopartnerhub.vercel.app/docs/sandbox.md): exact, short and extra transfers, the same event twice, a ping (`npx talo-hub webhooks test <id>`), then `npx talo-hub doctor`.

## Porting it

| You need | Look in your framework for |
|---|---|
| The raw body | the request bytes before body parsing; a raw parser on the webhook route only |
| HMAC-SHA256 and a constant-time compare | the standard library: `crypto.createHmac` + `timingSafeEqual`, `hmac` + `compare_digest`, `hash_hmac` + `hash_equals`, `crypto.subtle`, `OpenSSL::HMAC` + `secure_compare`, `javax.crypto.Mac` + `MessageDigest.isEqual` |
| A route outside CSRF and auth | the middleware's exclusion list, matcher or decorator |
| Exactly-once handling | a unique index (`INSERT … ON CONFLICT DO NOTHING`) or Redis `SET key NX EX` |
| Work after answering | the framework's queue or after-response hook |
| Reconciliation | cron, or the platform's scheduled jobs |
| Calling the hub | an HTTP client with timeouts: about 25 s to create a payment (Talo can take 15 to 20 s), under 8 s for reads in the webhook; retry network errors and `502 talo_unavailable` with backoff and the same external_id |

The verifier, in any language:

```text
parts  = the header split on "," into key=value pairs
t, v1  = parts["t"], parts["v1"]
reject if t is not an integer, |now - t| > 300, or v1 is not 64 lowercase hex characters
expected = lowercase hex of HMAC-SHA256(key = secret, message = "<t>." + raw body bytes)
accept only if constant_time_equal(expected, v1)
```

---
Human page: https://talopartnerhub.vercel.app/docs/stacks · All pages: https://talopartnerhub.vercel.app/docs/index.md
