Checkout widget
Embed the Chytapay checkout in your site: create the charge on your backend, install the SDK, and open the widget with your test or live publishable key.
By the end, your buyer pays by bank transfer without leaving your site. You need the id_token of a connected merchant account and Portal access as an integration admin.
To get to know the widget before integrating it, visit the widget page (in Spanish).
ChytaPay.checkoutHow it works
The path of a payment with the widget, between the payer's browser, your backend and Chytapay.
- Your backend creates the
checkoutcharge withPOST /payment-requestand the merchant account'sid_token. - Chytapay returns the
paymentRequestId. - Your backend passes it to your page, which doesn't create charges: it only displays one that already exists.
- The SDK, with your publishable key (
pk), reads the charge and feeds the Chytapay iframe. The payer enters their ID and gets the CVU. The iframe makes no API calls and sees no keys. - The payer transfers to the CVU.
- Chytapay notifies your backend with
payment_request.payment_received.
What the payer sees
- Enters their document (CUIL, CUIT or DNI), which is what the charge reconciles by.
- Sees the name ARCA has for that document: the transfer must come from an account in their name. It's informational: if ARCA doesn't respond, the flow continues.
- Gets the CVU, assigned only now, with a copy button and a countdown.
- Transfers from their account.
- Sees the result: with the full amount,
onSuccess. With less, the charge stays inpartial_paymentand the widget waits for the rest.
Warning: If someone else transfers, the payment isn't applied: the charge stays in pending and, if you enabled that event, a payment.unmatched with no_cuil_match arrives. See third-party payers.
1Generate the publishable key
Keys belong to the integration, not to the merchant account:
| Key | Format | Where it goes | What it can do |
|---|---|---|---|
Publishable (pk) |
pk_ / pk_ |
In the browser. It's public | Read a charge, look up a document's holder and associate the payer. X- header |
Secret (sk) |
sk_ / sk_ |
Only on your server | Create and cancel charges through /. Authorization: Bearer sk_... |
One pk works for every merchant account and is all this guide needs. The sk is generated in the Portal, under Credenciales (Credentials), Widget de pago (Payment widget) section, with Generar clave secreta (Generate secret key). It's shown only once, and rotating it invalidates the previous one.
- Open the Portal and pick the environment: Sandbox for
pk_test, Production forpk_live. - Open Credenciales (Credentials), Widget de pago (Payment widget) section.
- Click Generar clave (Generate key) if you don't have one. The Clave publicable (Publishable key) stays visible to copy.
- Optional: under Dominios autorizados (Authorized domains), click Configurar (Configure) and add your site's exact origins (scheme, host, and port if not the default, no path or trailing slash), for example
https://shop.example.com. With at least one, other origins get 403. With none, thepkworks from any site.
Production: A pk_test doesn't work in production: generate the pk_live under Credenciales (Credentials), with the environment on Production. livemode is true only for real production events.
2Create the checkout charge
Use it when you don't know the payer in advance, as in ecommerce. On your backend, create the charge with type: "checkout":
cURL
curl -X POST https://integration-api.test.chytapay.com.ar/payment-request \
-H "Authorization: Bearer $ID_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "checkout",
"referenceId": "order-1001",
"amount": 15000,
"description": "Store purchase",
"expiresInMinutes": 15
}'Response
{
"paymentRequestId": "00000000-0000-0000-0000-000000000000",
"type": "checkout",
"referenceId": "order-1001",
"amount": 15000,
"description": "Store purchase",
"dueDates": ["2026-10-01T15:15:00Z"],
"expiresInMinutes": 15,
"state": "pending",
"sendWhatsappNotification": false,
"sendEmailNotification": false
}You get a 201 with the charge in pending. The paymentRequestId is the paymentId you pass to the widget.
| Field | Required? | What it is |
|---|---|---|
type |
Yes | "checkout". Without it, the charge is standard. |
referenceId |
Yes | 8 to 100 characters: letters, numbers, - and _. Unique per integration and merchant account. |
amount |
Yes | Pesos, greater than 0, up to 2 decimals and 200,000,000. No draft. |
description |
Yes | What the payer sees, 3 to 500 characters. |
expiresIn |
No | Integer from 3 to 60, 15 by default. Replaces due. |
webhook |
No | HTTPS webhook URL for this charge only. |
metadata |
No | Key-value pairs returned in every event. |
Sending customer, dueDates, surcharge, conciliationType or a payer notification field (sendWhatsappNotification, sendEmailNotification, sendWhatsappPaymentConfirmation, reminderNotificationDate, deferredInitialNotificationDate) returns 400 with the field name.
- Always reconciles by CUIL.
- The response has no
CVUorbankAccountInfo: they're assigned when the payer identifies themselves. expiresInMinutesbecomes a timestamped expiration indueDates. When reached, the charge moves tooverdueandpayment_request.expiredarrives.
As with standard, a repeated referenceId returns 409 ReferenceIdAlreadyExists.
3Install the SDK
From the CDN, the SDK is available as window.ChytaPay:
<script src="https://unpkg.com/@chytapay/integration-widget@1" defer></script>defer lets the browser download the SDK without blocking your page from rendering, and runs it once the HTML is parsed, before DOMContentLoaded. An inline script placed above it won't find ChytaPay, so call ChytaPay.init from a DOMContentLoaded handler, as in the next step.
From npm, with a bundler:
npm i @chytapay/integration-widgetimport { ChytaPay } from '@chytapay/integration-widget';With @1 you get new 1.x releases; to control each one, pin the exact version (@1.0.0). Without a version, unpkg serves the latest. See the SDK changelog, also shipped in the package.
4Open the checkout
On your page, initialize the SDK with your pk and open the checkout with the paymentRequestId:
// 1. Once, after the SDK has loaded. The default URLs are the production ones.
document.addEventListener('DOMContentLoaded', () => {
ChytaPay.init({ pk: YOUR_INTEGRATION_PK });
});
// 2. Call it when the buyer confirms, with the charge your backend created.
function openCheckout(paymentRequestId) {
return ChytaPay.checkout({
paymentId: paymentRequestId,
onSuccess: (paymentId) => {
// The full amount arrived: confirm the purchase in your UI.
},
onError: (error) => {
// Expired or failed with no recovery: offer to create a new charge.
},
onCancel: () => {
// The buyer closed the checkout without paying, or the charge was canceled.
},
});
}
// 3. Optional: the returned handle closes the checkout from your code. Doesn't fire onCancel.
// const handle = openCheckout(paymentRequestId);
// handle.close();A modal opens and closes with Escape, a backdrop click or the X. To embed it, pass container.
onSuccess is for your UI. The payment confirmation is the payment_request.payment_received event on your backend, with its signature verified.
Warning: Treat the paymentRequestId as sensitive: with the pk, which is public, it's enough to read the charge and associate a payer. Keep it out of URLs and logs.
If your store uses CSP
If your page sends a Content Security Policy, allow these origins. Without them, the browser blocks the SDK, the checkout or the Mercado Pago button.
| Directive | Value | What for |
|---|---|---|
script- |
https:// |
Loading the SDK from the CDN. With npm the SDK ships in your bundle and you don't need it. |
connect- |
https:// |
Fetching the charge. |
frame- |
https:// |
Showing the checkout iframe. |
frame- |
mercadopago: and intent: |
Letting the Open in Mercado Pago button launch the app. |
img- |
https:// and data: |
The logo and animation on the loading screen. |
If your CSP doesn't set frame-src, the browser falls back to child-src, and then to default-src. If you change apiBaseUrl or iframeUrl in init, allow those origins instead of the production ones.
The button launches the app from a hidden frame on your page, which is why mercadopago: and intent: belong in frame-src. Without them, the payer sees the "app didn't open" notice, with the CVU ready to copy.
Try it in Sandbox
- Generate the
pk_testunder Credenciales (Credentials), with the environment on Sandbox. - Create the charge on
https://integration-api.test.chytapay.com.arwith theid_tokenof a test merchant account. - Initialize the SDK pointing to Sandbox:
Node
document.addEventListener('DOMContentLoaded', () => { ChytaPay.init({ pk: YOUR_INTEGRATION_PK_TEST, apiBaseUrl: 'https://integration-api.test.chytapay.com.ar', }); }); - Open the checkout. The widget shows the amount and asks for the document.
Leave iframeUrl at its default: the iframe doesn't call the API.
When something fails
| What the payer sees | What happened | on? |
What you do |
|---|---|---|---|
| "El documento ingresado no es válido. Revisalo e intentá de nuevo." (The document entered isn't valid.) | Not a CUIL, CUIT or DNI. | No | Nothing: they fix it there. |
| "No pudimos asignarte una cuenta para transferir. Probá de nuevo en unos minutos." (We couldn't assign you an account.) | Switched documents and no CVU is free. Transient. | No | Don't create another charge. |
| "Este cobro ya tiene un pagador asociado y no se puede cambiar." (This charge already has a payer.) | Switched documents with money credited or in a final state. | Yes | A new charge, if needed. |
| "Este cobro ya no admite pagos." (This charge no longer accepts payments.) | Paid, expired or canceled. | Yes | A new charge. |
It also fires onError if the charge expires (overdue, expiresInMinutes elapsed) or if it shows "No se pudo cargar el pago." (The payment couldn't be loaded.) on open: paymentId missing or from another integration (404), unknown pk (401) or origin outside the authorized domains (403). partial_overdue doesn't fire it: the widget keeps waiting.
Quick reference
See the SDK options and endpoints
ChytaPay.init(config)
Once, before checkout. Without pk, it throws.
| Option | Required? | What it is |
|---|---|---|
pk |
Yes | Your publishable key. |
api |
No | Defaults to https://. Change it only for Sandbox. |
iframe |
No | Defaults to https://. |
request |
No | Per-call timeout, in ms. Defaults to 10000. |
ChytaPay.checkout(options)
| Option | Required? | What it is |
|---|---|---|
paymentId |
Yes | The payment. |
on |
No | Once, on reaching total_. Receives the paymentId. |
on |
No | On overdue or an unrecoverable error. Receives an Error. |
on |
No | When the buyer closes it or the charge moves to canceled. |
on |
No | After each tap on Open in Mercado Pago. Receives an event. Since SDK 1.2.0. |
container |
No | Element to embed it in. Without it, a modal. With it, you handle closing. |
locale |
No | Amount and date format, es- by default. The text stays in Spanish. |
theme |
No | CSS variables prefixed with --, such as --. |
poll |
No | Fixed ms or a function of elapsed time. Defaults to 1 s until 20 s, 2 s until 40 s and 10 s after. |
demo |
No | true opens a demo with fictitious data, no API. paymentId becomes optional. |
checkout returns close(), which unmounts the widget without firing onCancel. Polling stops at a final state, or at most two minutes after expiration. SDK states: pending, partial_payment, total_payment, partial_overdue, overdue and canceled; the final ones are total_payment, overdue and canceled.
onWalletOpen(event)
Fires after each tap on Open in Mercado Pago, with the outcome of that tap. Use it to see in which browsers the app launches. If your function throws, the checkout is unaffected.
ChytaPay.checkout({
paymentId: paymentRequestId,
onWalletOpen: ({ wallet, opened, os, browser, via }) => {
// { wallet: 'mercadopago', opened: false, os: 'android', browser: 'firefox', via: 'sdk' }
},
});| Field | Values |
|---|---|
wallet |
"mercadopago" |
opened |
true if the page went to the background after the tap. On Android Chrome it is also true when the app is missing and the Play Store opens. |
os |
"ios" or "android" |
browser |
"safari", "chrome", "firefox", "samsung", "edge", "opera", "instagram", "facebook", "tiktok", "webview" or "other" |
via |
"sdk" if the SDK opened the link from your page; "iframe" if the widget's own link did. |
The event carries no user agent and nothing that identifies the payer. If the app doesn't open within 2.5 seconds, the widget tells the payer and leaves the CVU ready to copy.
/v1/payments endpoints
The SDK uses them, on the same host as the Integration API.
| Method and path | Key | What it does |
|---|---|---|
GET / |
pk or sk |
State, amount, amount paid and CVU. |
GET / |
pk or sk |
The document's name in ARCA, without changing the charge. |
POST / |
pk or sk |
Associates the payer by CUIL, CUIT or DNI and returns the CVU. |
POST / |
sk only |
Creates a standard charge. 409 if the integration doesn't have exactly one active merchant account. |
POST / |
sk only |
Cancels if it received no money and isn't in a final state. |
A charge from another integration returns 404, like a missing one. A pk on an sk-only endpoint returns 403.