Getting started
From sign-up in the portal to your first event in your backend, all in Sandbox: credentials, a connected merchant account, a charge, a simulated payment and the webhook.
By the end you will have a Sandbox charge paid with a simulated payment and the payment_request.payment_received event in your backend. No real money moves.
Before you begin
- A public HTTPS URL for webhooks. During development, a tunnel works.
curl, Node 18 or later, or Python 3 withrequests.
Set the Sandbox API URL in your terminal. The Production one is in Operations and errors.
export API=https://integration-api.test.chytapay.com.ar1Sign up and set up
You sign up yourself in the portal, with no one to wait for. You start in Sandbox, the test environment.
- Go to chytapay.com.ar/portal/login, choose Crear cuenta (Create account), confirm your email and fill in your company details. When you finish, the portal shows your Sandbox
clientIdandclientSecret. - Store them in your secrets manager. The
clientSecretis shown only once: if you lose it, rotate it under Credenciales (Credentials). You don't use them in this walkthrough: they are for OAuth with real merchant accounts. - Add your HTTPS webhook URL under Configuración (Settings). The validation token is optional, at least 16 characters long, and arrives in the
validation-tokenheader.
2Get a test merchant account
The Integration API takes as Bearer the idToken of a merchant account linked to your integration. It lasts 1 hour.
Sandbox shortcut: a test merchant account
In Sandbox you don't need OAuth: the portal gives you the token of a test merchant account that is already linked. Go to Cuentas de comercio (Merchant accounts), choose Generar tokens de prueba (Generate test tokens) and store the idToken as COMMERCE_TOKEN. It lasts 1 hour: when it expires, generate another. To check which merchant the token belongs to, call GET /me.
The real flow: OAuth
In Production, each merchant account authorizes your integration with OAuth: authorize, return to your redirect_uri with code and state, and exchange for tokens. The redirect_uri is not the webhook URL. See how to connect merchant accounts with OAuth.
3Create a charge
In your backend, create the charge with the merchant account token and a due date between tomorrow and 35 days out. Send "conciliationType": "cuil" and the payer's CUIL or DNI in customer.taxDocument: the payment is matched by the CUIL of whoever transfers (why CUIL).
cURL
DUE_DATE=$(date -d '+7 days' +%F 2>/dev/null || date -v+7d +%F)
curl -X POST "$API/payment-request" \
-H "Authorization: Bearer $COMMERCE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"referenceId": "test-0001",
"amount": 1500,
"description": "Test charge",
"dueDates": ["'"$DUE_DATE"'"],
"conciliationType": "cuil",
"sendWhatsappNotification": false,
"sendEmailNotification": false,
"customer": { "name": "Test payer", "taxDocument": "20123456786" }
}'Node
const dueDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
const res = await fetch(`${process.env.API}/payment-request`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.COMMERCE_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
referenceId: 'test-0001',
amount: 1500,
description: 'Test charge',
dueDates: [dueDate],
conciliationType: 'cuil',
sendWhatsappNotification: false,
sendEmailNotification: false,
customer: { name: 'Test payer', taxDocument: '20123456786' },
}),
});
const charge = await res.json();
console.log(res.status, charge.state, charge.CVU);Python
from datetime import date, timedelta
due_date = (date.today() + timedelta(days=7)).isoformat()
res = requests.post(
f"{os.environ['API']}/payment-request",
headers={"Authorization": f"Bearer {os.environ['COMMERCE_TOKEN']}"},
json={
"referenceId": "test-0001",
"amount": 1500,
"description": "Test charge",
"dueDates": [due_date],
"conciliationType": "cuil",
"sendWhatsappNotification": False,
"sendEmailNotification": False,
"customer": {"name": "Test payer", "taxDocument": "20123456786"},
},
)
charge = res.json()
print(res.status_code, charge["state"], charge["CVU"])Response
{
"type": "standard",
"paymentRequestId": "00000000-0000-0000-0000-000000000000",
"referenceId": "test-0001",
"amount": 1500,
"description": "Test charge",
"dueDates": ["2026-10-07"],
"state": "pending",
"sendWhatsappNotification": false,
"sendEmailNotification": false,
"customer": { "name": "Test payer", "taxDocument": "20123456786" },
"CVU": "0000000000000000000000",
"bankAccountInfo": { "accountHolderName": "...", "accountHolderTaxId": "...", "bankName": "..." }
}You get a 201 with the charge in pending. Store the CVU in the CVU variable for stage 4.
referenceIdis 8 to 100 characters (letters, numbers,-and_) and unique per integration and merchant account, canceled charges included. A repeat returns409ReferenceIdAlreadyExists: to test again, change it.- With
amount,sendWhatsappNotificationandsendEmailNotificationare required. In Sandbox,false. - Amounts in pesos, with up to 2 decimal places.
The other fields, surcharges, drafts and batches are in Create charges.
4Simulate the payment
Simulate full payment in the Portal, under Simulador de pagos (Payment simulator): enter the charge's CVU, the amount and the payer's details: source CVU, source name and, as the source CUIL, the charge's.
- With the charge's CUIL, the payment is matched and
payment_request.payment_receivedarrives. - With a different CUIL it does not reconcile and the event is
payment.unmatched. - If the event doesn't arrive, check that it is enabled under Webhooks and that you have a URL set under Configuración.
Warning: payment.unmatched is off by default. Enable it in Webhooks: without it you never learn about unmatched payments, such as a third party paying with a different CUIL.
5Receive the event
Chytapay sends a POST to your webhook URL. Return a 2xx within 5 seconds or it retries.
| Header | Purpose |
|---|---|
X- |
HMAC-SHA256 signature, t=<unix>,v1=<hex>. Verify it before you trust the event |
X- |
Same across all retries: deduplicate by this ID |
validation- |
The token you set with the URL |
The body, trimmed:
{
"event": "payment_request.payment_received",
"stateType": "total_payment",
"livemode": false,
"referenceId": "test-0001",
"requestedAmount": 1500,
"paidAmount": 1500,
"thisPaymentAmount": 1500,
"remainingAmount": 0,
"reconciledPayerCuil": 20123456786
}Route on event and decide on stateType: total_payment or partial_payment. livemode is false in Sandbox.
The example server, full signature verification and retries are in Webhooks. To test only your endpoint, Webhooks has a button that sends a test payment.unmatched. It does not exist in production.
Go to production
It is the same integration: you request Production under Credenciales (Credentials), and if the automatic check approves it you generate the production credentials with the environment selector on Production. If not, the request goes to review. What to check before you collect is in Operations and errors.