Skip to content
ESENGo to the Portal

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).

How it works

The path of a payment with the widget, between the payer's browser, your backend and Chytapay.

  1. Your backend creates the checkout charge with POST /payment-request and the merchant account's id_token.
  2. Chytapay returns the paymentRequestId.
  3. Your backend passes it to your page, which doesn't create charges: it only displays one that already exists.
  4. 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.
  5. The payer transfers to the CVU.
  6. Chytapay notifies your backend with payment_request.payment_received.

What the payer sees

  1. Enters their document (CUIL, CUIT or DNI), which is what the charge reconciles by.
  2. 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.
  3. Gets the CVU, assigned only now, with a copy button and a countdown.
  4. Transfers from their account.
  5. Sees the result: with the full amount, onSuccess. With less, the charge stays in partial_payment and 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_test_... / pk_live_... In the browser. It's public Read a charge, look up a document's holder and associate the payer. X-Chyta-Publishable-Key header
Secret (sk) sk_test_... / sk_live_... Only on your server Create and cancel charges through /v1/payments. 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.

  1. Open the Portal and pick the environment: Sandbox for pk_test, Production for pk_live.
  2. Open Credenciales (Credentials), Widget de pago (Payment widget) section.
  3. Click Generar clave (Generate key) if you don't have one. The Clave publicable (Publishable key) stays visible to copy.
  4. 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, the pk works 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.
expiresInMinutes No Integer from 3 to 60, 15 by default. Replaces dueDates.
webhookUrl 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 CVU or bankAccountInfo: they're assigned when the payer identifies themselves.
  • expiresInMinutes becomes a timestamped expiration in dueDates. When reached, the charge moves to overdue and payment_request.expired arrives.

As with standard, a repeated referenceId returns 409 ReferenceIdAlreadyExists.

3Install the SDK

From the CDN, the SDK is available as window.ChytaPay:

HTML
<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:

cURL
npm i @chytapay/integration-widget
Node
import { 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:

Node
// 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-src https://unpkg.com Loading the SDK from the CDN. With npm the SDK ships in your bundle and you don't need it.
connect-src https://integration-api.chytapay.com.ar Fetching the charge.
frame-src https://widget.chytapay.com.ar Showing the checkout iframe.
frame-src mercadopago: and intent: Letting the Open in Mercado Pago button launch the app.
img-src https://widget.chytapay.com.ar 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

  1. Generate the pk_test under Credenciales (Credentials), with the environment on Sandbox.
  2. Create the charge on https://integration-api.test.chytapay.com.ar with the id_token of a test merchant account.
  3. 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',
      });
    });
  4. 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 onError? 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.
apiBaseUrl No Defaults to https://integration-api.chytapay.com.ar. Change it only for Sandbox.
iframeUrl No Defaults to https://widget.chytapay.com.ar/checkout/.
requestTimeoutMs No Per-call timeout, in ms. Defaults to 10000.

ChytaPay.checkout(options)

Option Required? What it is
paymentId Yes The paymentRequestId.
onSuccess No Once, on reaching total_payment. Receives the paymentId.
onError No On overdue or an unrecoverable error. Receives an Error.
onCancel No When the buyer closes it or the charge moves to canceled.
onWalletOpen 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-AR by default. The text stays in Spanish.
theme No CSS variables prefixed with --chytapay-, such as --chytapay-primary.
pollSchedule 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.

Node
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 /v1/payments/:id pk or sk State, amount, amount paid and CVU.
GET /v1/payments/:id/payer-lookup?document= pk or sk The document's name in ARCA, without changing the charge.
POST /v1/payments/:id/payer pk or sk Associates the payer by CUIL, CUIT or DNI and returns the CVU.
POST /v1/payments sk only Creates a standard charge. 409 if the integration doesn't have exactly one active merchant account.
POST /v1/payments/:id/cancel 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.

Next steps