Quickstart
-
Get a test key.
Keys look like
sk_test_...and belong to a project. They are secret server-side keys: never put them in a browser or mobile app. A lost key is replaced, not recovered. The dashboard at dashboard.pachapay.io is a placeholder for now, so keys are not self-serve yet.Terminal window export PACHAPAY_KEY=sk_test_... -
Find a payment method.
List the methods available for a country and currency. Use the
codeof a method in the next step.Terminal window curl "https://api.pachapay.io/v1/payment-methods?country=CL¤cy=CLP" \-H "Authorization: Bearer $PACHAPAY_KEY" -
Create a payment.
Every
POSTneeds anIdempotency-Key. See Idempotency.Terminal window curl https://api.pachapay.io/v1/payments \-H "Authorization: Bearer $PACHAPAY_KEY" \-H "Idempotency-Key: order-8841-attempt-1" \-H "Content-Type: application/json" \-d '{"amount": 100000,"currency": "CLP","country": "CL","method_code": "CL.KHIPU","merchant_reference": "order-8841","payer": {"name": "Maria Lopez","email": "maria@example.com","document": { "type": "RUT", "number": "12345678-5" }},"return_url": "https://merchant.example/return"}'const res = await fetch('https://api.pachapay.io/v1/payments', {method: 'POST',headers: {Authorization: `Bearer ${process.env.PACHAPAY_KEY}`,'Idempotency-Key': 'order-8841-attempt-1','Content-Type': 'application/json',},body: JSON.stringify({amount: 100000, // integer, minor unitscurrency: 'CLP', // separate fieldcountry: 'CL',method_code: 'CL.KHIPU',merchant_reference: 'order-8841',payer: {name: 'Maria Lopez',email: 'maria@example.com',document: { type: 'RUT', number: '12345678-5' },},return_url: 'https://merchant.example/return',}),});if (!res.ok) throw new Error(JSON.stringify(await res.json()));const payment = await res.json();The amount is
100000minor units. CLP has no decimals, so this is 100,000 pesos. For a currency with 2 decimals, 10.00 is sent as1000.The response is the payment object. It is illustrative here, and fields not shown are omitted:
{"id": "pay_0192f3a1","object": "payment","status": "requires_action","amount": 100000,"amount_received": 0,"currency": "CLP","country": "CL","method_code": "CL.KHIPU","method_family": "bank_redirect","merchant_reference": "order-8841","next_action": { "type": "redirect", "redirect_url": "https://bank.example/pay/abc" },"created_at": "2026-10-16T12:00:00Z"}With the default
hostedflow the payment also has acheckout_urlto send the payer to. Otherwise follownext_action. See Payments. -
Register a webhook endpoint.
The URL must be HTTPS. The secret is shown once; store it.
Terminal window curl https://api.pachapay.io/v1/webhook-endpoints \-H "Authorization: Bearer $PACHAPAY_KEY" \-H "Idempotency-Key: webhook-setup-1" \-H "Content-Type: application/json" \-d '{ "url": "https://merchant.example/webhooks/pachapay", "events": ["payment.succeeded", "payment.failed", "payment.expired"] }' -
Receive the event and verify the signature.
Verify against the raw request body, before parsing JSON.
import { createHmac, timingSafeEqual } from 'node:crypto';export function verify(rawBody: string, header: string, secret: string): boolean {const parts = Object.fromEntries(header.split(',').map((p) => p.split('=') as [string, string]));const t = parts.t;if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // older than 5 minutesconst expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();// During secret rotation the header holds two v1 values; accept either.return header.split(',').filter((p) => p.startsWith('v1=')).some((p) => {const got = Buffer.from(p.slice(3), 'hex');return got.length === expected.length && timingSafeEqual(got, expected);});}Respond with any
2xxwithin 10 seconds. Handle duplicates byevent.id. Details in Webhooks. -
Read the current state.
Events can arrive out of order. Treat the webhook as a signal and read the object when it matters.
Terminal window curl https://api.pachapay.io/v1/payments/pay_0192f3a1 \-H "Authorization: Bearer $PACHAPAY_KEY"