---
name: talo-payments
description: Add bank-transfer payments (Argentina, CVU/alias) with Talo Pay to a project through the Talo Partner Hub — create a payment at checkout, show the CVU (Talo's payment page, the <talo-checkout> widget, or a reusable payment link with a QR), confirm it via signed webhooks, refund, and test end to end in sandbox with simulated transfers. Use when the user wants to charge by transfer, integrate Talo or Talo Pay, accept payments in ARS by CVU, or share a payment link.
---

# Talo payments through the Talo Partner Hub

The hub (`https://talopartnerhub.vercel.app`) wraps Talo's API: one API key per account and
environment, payments attributed to the partner, and signed webhooks. Money moves only in
**ARS by bank transfer**: each payment gets a CVU and alias that the buyer transfers to.

Stack kits: if this project has a `talo-stack-*` skill (Next.js + Vercel, Node, Python, Laravel,
edge), follow it for the code; `npx talo-hub skills install` adds the one that matches. Any other
stack: apply the 10 rules at https://talopartnerhub.vercel.app/docs/stacks.md.

## 1. Get a key

- Sandbox first: `npx talo-hub login` (opens the browser to authorize the hub in Talo; it exits by
  itself when the person authorizes, so wait for it instead of asking them whether they did; see
  talo-operations for running it from an agent) and then
  `npx talo-hub keys create --name "<project>" --scope charge --dotenv .env.local` for the project's server
  (`--vercel production|preview` on Vercel: the key goes straight there, never on screen). Keys look
  like `tph_test_…` (sandbox) or `tph_live_…` (production, real money). `charge` creates payments
  but can't refund; that's all a checkout needs.
- Write it straight into the project's server-side env as `TALO_HUB_API_KEY`. **Never** in client
  code, git, logs or the conversation. Check `.env*` is gitignored.
- If the MCP server is connected (tools `create_payment`, `simulate_transfer`, `create_api_key`…),
  use the tools to explore and test; the project's code still calls the HTTP API.
- For the full test plan and the move to production, see the `talo-go-live` skill.

## 2. Create the payment on the server, at checkout

```http
POST https://talopartnerhub.vercel.app/api/v1/payments
Authorization: Bearer $TALO_HUB_API_KEY
Content-Type: application/json

{ "amount": 15000, "external_id": "<order id>", "motive": "Pedido #1001" }
```

The answer has `id`, `status` (`PENDING`), `cvu`, `alias`, `amount`, `expires_at`, `payment_url`
and `checkout_token`. Show the buyer the CVU and alias with the exact amount, and keep `id` with the
order. Reusing the order's `external_id` returns the same payment, so retries never charge twice.
The amount always comes from your server (the order), never from the browser.

## 3. Show the payment to the buyer

Pick one (details: https://talopartnerhub.vercel.app/docs/checkout.md):

| The project | Use |
|---|---|
| Just needs somewhere to send the buyer | Redirect to `payment_url`: Talo's own page with amount, alias and CVU (copy buttons), countdown, account holder and CUIT. No UI to build. |
| Wants the buyer to stay on its page | The widget. Pass the page only `id` and `checkout_token`, never the API key: `<script src="https://talopartnerhub.vercel.app/widget/v1/talo-checkout.js" defer></script>` and `<talo-checkout payment-id="…" token="…"></talo-checkout>`. React/Next.js: `npx shadcn@latest add https://talopartnerhub.vercel.app/r/talo-payment-status.json` (component) or `…/r/talo-checkout-nextjs.json` (Next.js block with hub client and webhook route). |
| Has no checkout at all (sells by chat, Instagram, a QR at the counter) | A payment link: `npx talo-hub links create --title "<what>" --amount <ARS>` (or `--min`/`--max` for an amount the buyer types), `POST /api/v1/payment-links`, or the MCP tool `create_payment_link`. Share its `url` or print its `qr_url`. Each buyer gets a new payment whose `external_id` is `link_<link id>_<random>` and whose `motive` is the title. |

- `checkout_token` is read-only and opens only that payment's public view
  (`GET /api/public/payments/{id}?token=…`) for 15 days: it may go in the page or a URL. It comes
  in the answers to create and `GET /api/v1/payments/{id}`, not in lists or webhook events.
- The widget polls by itself (every 4 s, backing off on errors, stopping when paid or expired) and
  fires `talo-status`, `talo-paid`, `talo-expired` and `talo-error`. Use them to update the screen
  only: **never fulfil on `talo-paid`, `onPaid` or the buyer coming back from Talo's page** —
  anyone can fire a browser event. Fulfil in the webhook (step 4).
- If the site sends a Content-Security-Policy, add `https://talopartnerhub.vercel.app` to
  `script-src` and `connect-src`. `style-src` needs no change: the widget uses a constructed
  stylesheet, not inline styles.
- Payment links allow up to 20 payments per hour from the same network (IP) per link, and a breaker
  of 200 per link per 10 minutes; a buyer who comes back to the link gets the payment they already
  started (also after a partial transfer), which doesn't count. Buyers on the same Wi-Fi count
  together. Disabling one (`links disable <id>`) is permanent; payments already started can still
  be paid.
- A link's payments have `external_id` `link_<12-char link id>_<random>`: the link id is
  `external_id.slice(5, 17)` (link ids are letters and digits; the random part may hold `-` or `_`).

## 4. Confirm with webhooks, never with the browser

Register the endpoint once: `npx talo-hub webhooks add https://<site>/api/webhooks/talo --dotenv .env.local`
(it checks the URL first; `--vercel <env>` on Vercel), and store
the `whsec_…` secret it prints as `TALO_HUB_WEBHOOK_SECRET`.

Each event is a POST with `Talo-Hub-Event` and `Talo-Hub-Signature: t=<unix>,v1=<hex>`, where
`v1 = HMAC-SHA256(secret, "<t>.<raw body>")`. Verify it on the **raw** body, reject a `t` older
than 5 minutes, and answer 2xx quickly. Failed deliveries are retried for about 9 hours.

```ts
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyTaloHub(rawBody: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(header.split(",").map((p) => { const [k, ...v] = p.trim().split("="); return [k, v.join("=")]; }));
  const t = Number(parts.t);
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
  if (!/^[0-9a-f]{64}$/.test(parts.v1 ?? "")) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  return timingSafeEqual(expected, Buffer.from(parts.v1, "hex"));
}
```

Test vector (https://talopartnerhub.vercel.app/docs/webhooks.md): secret `whsec_test_secret`, t `1793000000`, the body shown there → v1 `ee8e039a9e5e9ce3eac17705f1133bfd58776d46f5163e01ad6a42d2cc540b9e`.

Before acting on an event, re-read the payment with `GET /api/v1/payments/{id}`: deliveries are
retried independently and can arrive out of order.

Body: `{ id, type, created_at, data: { payment, previous_status } }`. Types: `payment.created`,
`payment.updated`, `payment.paid` (SUCCESS or OVERPAID: fulfil the order), `payment.expired`,
`account.updated`. Statuses: `PENDING`, `SUCCESS`, `OVERPAID`, `UNDERPAID` (paid less: ask for
the rest, don't fulfil), `EXPIRED`. Handle each event idempotently by `id`.

## 5. Test end to end in sandbox

1. Create a payment (step 2).
2. `npx talo-hub sandbox pay <payment_id>` (or the `simulate_transfer` tool, or
   `POST /api/v1/sandbox/transfers { payment_id, amount? }`). Pass a smaller `--amount` to test
   UNDERPAID, a larger one for OVERPAID.
3. The webhook gets `payment.updated` and `payment.paid`. During local development,
   `npx talo-hub events listen --forward-to http://localhost:3000/api/webhooks/talo` relays events
   to localhost. They are unsigned and carry `talo-hub-forwarded: 1`: accept those only when
   `NODE_ENV=development`.

## Other operations

| | |
|---|---|
| Refund | `POST /api/v1/payments/{id}/refunds` with `{ amount }` (partial) or no body (full). Asynchronous |
| Change amount | `PUT /api/v1/payments/{id}/price { amount }` |
| Extend | `PUT /api/v1/payments/{id}/expiration { expires_at }` (only PENDING) |
| Look up | `GET /api/v1/payments/{id}`, `GET /api/v1/payments?from=&to=` |
| Payment links | `GET /api/v1/payment-links`, `GET /api/v1/payment-links/{id}` (with its payments), `DELETE /api/v1/payment-links/{id}` (for good) |
| Balance, movements | `GET /api/v1/balance`, `GET /api/v1/transactions` (501 `talo_partner_unsupported` until Talo enables them for partners) |

Errors are `{ "error": { "code", "message" } }`. `talo_reconnect_required` means the account
owner has to run `npx talo-hub login` again (`--production` for a `tph_live_` key).

Before going live: switch to a `tph_live_` key, register the production webhook, and make one
small real transfer.
