Integrating Clip Payments in Node: Transparent vs Redirected Checkout & Card Token Charges (2026) — Cesar Ayala
← All posts

Integrating Clip Payments in Node: Transparent vs Redirected Checkout & Card Token Charges (2026)

Clip offers three checkout modes: Transparent (stay on your site, tokenize the card and charge via API — needs a PCI AoC unless you use Clip's SDK), Redirected (a hosted payment link), and Embedded. The Transparent flow is Card Token (generatecardtoken, a 15-minute token) then the Payments API (realizarpago) with the token, amount and "currency":"MXN".

The three Clip checkout modes, and how to choose

Clip gives you three ways to take a card online, and picking the wrong one is the difference between a two-hour integration and a PCI audit. Checkout Transparente keeps the customer on your site: you tokenize the card and charge it through the API. That transparent flow is two calls — generatecardtoken (the Card Token API) hands you a Card Token ID that lives for 15 minutes, then realizarpago (the Payments API) charges it with the token, the amount, and "currency": "MXN". Checkout Redireccionado is a hosted payment link. Checkout Embebido embeds Clip’s checkout in your page. Want the lowest-effort path? Take the redirected link. Want to own the UX? Take Transparente — but read the next section first, because the PCI question decides how you build it.

This is the fourth gateway in a series. If you’ve read my Conekta integration and Mercado Pago integration guides, the engineering discipline here is identical — only the endpoint names change. If you’re still choosing, my payment gateways in Mexico comparison is where to start.

Transparente vs Redireccionado: who owns the card capture

Checkout Transparente

  • Customer never leaves your site
  • You tokenize with generatecardtoken then charge with realizarpago
  • PCI AoC required if your server touches raw card data
  • Clip's SDK tokenizes without you touching card data — no PCI cert needed
  • Most control over the checkout UX

Checkout Redireccionado

  • Customer is redirected to Clip's hosted payment link
  • Clip owns the card form entirely
  • No card data ever reaches your server
  • Lowest-effort integration to ship
  • Least control over look and feel
Transparente keeps the customer on your site and needs you to think about PCI; Redireccionado hands the whole capture to Clip's hosted page.

Does your server touch card data? The PCI AoC question

Here’s the fork that decides your whole Transparente build. If your server ever receives a raw card number — PAN, CVV, expiry — you need a valid PCI DSS Attestation of Compliance (AoC). That’s not a checkbox; it’s an audit, a scope, and an ongoing obligation most teams do not want.

Clip’s answer is the SDK. It tokenizes the card on the client, so the raw card data goes from the browser straight to Clip and never lands on your infrastructure — your server only ever sees the resulting opaque Card Token ID. Because your server never touches card data, the SDK path does not require a PCI AoC. Same move Conekta.js and Stripe.js make: stay out of PCI scope while keeping the customer on your site.

So the honest decision guide is three lines:

  • Redireccionado — Clip’s hosted link takes the card. Zero PCI concern, least code.
  • Transparente + SDK — the SDK tokenizes client-side. No PCI AoC needed, and you keep your UX.
  • Transparente, server touches the PAN — you need a PCI AoC. Only do this if you already have one and know why.

For almost everyone, Transparente + SDK is the right answer: you own the checkout, you stay out of PCI scope, and your server only ever handles tokens.

The Transparent flow, step 1 — capture the card with Clip’s SDK

Step one produces a Card Token ID, and it happens in the browser. You load Clip’s SDK, collect the card in Clip’s fields, and the SDK calls generatecardtoken on your behalf. The raw card data goes to Clip; your page gets back a token ID with a hard 15-minute expiration — a short-lived, single-purpose credential, not something you store. Capture it, ship it to your server, charge it promptly.

The client side looks like this — treat the SDK method names as illustrative and confirm the exact API against developer.clip.mx:

// Browser — Clip's SDK tokenizes the card so your server never sees the PAN.
// The SDK calls the Card Token API (generatecardtoken) for you.
const clip = new ClipSDK(process.env.CLIP_PUBLISHABLE_KEY);

async function onPay() {
  // The SDK collects the card in its own fields and returns a Card Token ID.
  const { id: cardTokenId } = await clip.card.createToken();
  // cardTokenId expires in 15 minutes — send it to YOUR server and charge now.
  await fetch("/api/clip/charge", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ cardTokenId, internalOrderId: "ord_8f21" }),
  });
}

The one rule that matters here: do not persist the Card Token ID. It dies in 15 minutes, so the architecture is capture-then-charge in the same user action. Queue tokens for later and you’ve misunderstood the lifetime — every retry hits an expired token and fails.

The Transparent flow, end to end

  1. Load Clip's SDKPublishable key in the browser; card fields owned by Clip
  2. Tokenize the cardSDK calls generatecardtoken — returns a Card Token ID (15-min expiry)
  3. Send token to your serverOnly the opaque token ID crosses to your backend
  4. Charge with realizarpagoPOST /payments with token + amount + currency MXN + idempotency key
  5. Wait for the webhookNotification arrives; query Consultar un Pago for the real status
  6. Reconcile and fulfillGrant access only on a verified, confirmed status — never client-side success
Two API calls do the work: the SDK tokenizes client-side, then your server charges the short-lived token.

The Transparent flow, step 2 — charge with the Payments API

Now your server holds the Card Token ID and does the charge. The endpoint is realizarpago — the Payments API, POST /payments. You send the Card Token ID, the amount, and "currency": "MXN". Clip returns a payment object with a status.

// Server — charge the Card Token ID with the Payments API (realizarpago).
import crypto from "node:crypto";

async function chargeClip({ cardTokenId, internalOrderId }) {
  // Deterministic idempotency key from YOUR order id — a retry must not double-charge.
  const idempotencyKey = crypto
    .createHash("sha256")
    .update(`charge:${internalOrderId}`)
    .digest("hex");

  // Card tokens are created on api-secure.payclip.com; the charge goes to api.payclip.com. Confirm both in the reference.
  const auth = Buffer.from(`${process.env.CLIP_API_KEY}:`).toString("base64");
  const res = await fetch("https://api.payclip.com/payments", {
    method: "POST",
    headers: {
      // Clip's reference uses Basic auth (Base64 of your API key). Confirm against developer.clip.mx/reference.
      "Authorization": `Basic ${auth}`,
      "Content-Type": "application/json",
      "Idempotency-Key": idempotencyKey,
    },
    body: JSON.stringify({
      amount: 499.0,            // confirm amount units against the reference
      currency: "MXN",
      payment_method: { token: cardTokenId },
      description: `Order ${internalOrderId}`,
    }),
  });

  const payment = await res.json();
  // Do NOT treat this response as final truth — reconcile via the webhook + Consultar un Pago.
  return payment;
}

Two cautions the docs won’t shout at you. First, the exact request body field names and the amount’s unit convention must come from the reference — I’ve pinned the endpoint (realizarpago / POST /payments) and the "currency": "MXN" requirement because those are verified, but confirm the body shape against developer.clip.mx. Second, the charge response is not your source of truth. Even when it looks successful, the authoritative status comes from re-querying Clip.

One more thing to get right: the Authorization header format. Every request carries your API key in an Authorization header, and Clip’s reference uses Basic auth — the Base64 encoding of your key — not the Bearer scheme Stripe made you reflexively reach for. Conflate the two and you get a flat 401. Read the exact format off developer.clip.mx/reference and match it byte-for-byte. Keep the secret API key server-side only — it moves money. The publishable key belongs in the browser for the SDK; the secret key never does.

Handling the webhook — id, origin, event_type, then Consultar un Pago

Clip sends a webhook when a payment’s status changes. The notification is deliberately thin. A verified shape looks like this:

{
  "id": "pinpad-...",
  "origin": "pinpad-payments-api",
  "event_type": "PINPAD_INTENT_STATUS_CHANGED"
}

Notice what’s not there: a trustworthy final status. The webhook tells you that something changed, not what the money did. To get the authoritative status you take the identifier from the notification and call the “Consultar un Pago” (Get Payment) endpoint. That fetched status — not the webhook body — is what you act on.

Be careful with field names. The notification carries id, origin, and event_type; I am not going to invent status enums like CREATED or COMPLETED for online checkout, because those aren’t verified for this flow. Read the actual status values off developer.clip.mx/reference and switch on those. Thin event, then re-query for truth — the same pattern I unpack in webhooks vs polling vs API.

// Webhook handler — the notification is a trigger, not the truth.
app.post("/webhooks/clip", express.json(), async (req, res) => {
  const { id, origin, event_type } = req.body;

  // 1) Verify the notification is authentic (check the reference for Clip's mechanism).
  // 2) Re-query the real status — never trust the event body's implied outcome.
  const payment = await getPayment(id); // "Consultar un Pago" (Get Payment)

  // 3) Switch on the REAL status from the API — use Clip's documented values, not invented ones.
  await reconcile(payment); // idempotent: safe to run twice for a redelivered event

  res.status(200).send("ok"); // ack fast
});

And never trust the notification alone. This is the same reliability checklist that every gateway in this series ends on, and it’s non-negotiable.

  • Verify authenticity. Anyone can POST JSON at your webhook URL. Confirm Clip’s authenticity mechanism in the reference and validate every notification before acting. The idempotency-and-replay reasoning I wrote up for Stripe signature verification applies here directly — different gateway, identical threat model.
  • Use idempotency keys. One on the charge (as above), and dedupe inbound webhooks on their id. Clip redelivers; networks retry. Without it, a retried charge double-bills and a redelivered webhook double-fulfills.
  • Don’t invent status enums. Switch only on the values Clip documents for this flow. A guessed COMPLETED is how you silently mis-handle a real state.
  • Re-query for truth. The webhook is the doorbell; “Consultar un Pago” is who’s actually at the door.

The four Clip reliability rules

AuthenticityValidate every inbound notification against Clip's documented mechanism before acting. Anyone can POST to your URL.
IdempotencyDeterministic key on the charge; dedupe webhooks on id. Retries and redeliveries must not double-charge or double-fulfill.
Re-query for statusThe webhook only says 'something changed'. Call Consultar un Pago for the authoritative status.
No invented enumsSwitch only on status values Clip documents. Never hard-code a guessed CREATED/COMPLETED.
The same discipline as every gateway in this series — only the endpoint names change.

If you don’t need to own the checkout UX, skip all of the above. Checkout Redireccionado is a payment link: you create a “link de pago” through Clip, redirect the customer to Clip’s hosted page, they pay, and they return to your store. Clip owns the entire card capture, so no card data ever touches your server and PCI is entirely Clip’s problem.

The reconciliation discipline does not change, though. That return redirect is not proof of payment — a user can hit the URL directly, or bail before the charge settles. You confirm the money the same way: wait for the webhook, then query “Consultar un Pago” for the real status. The client-side “success” screen is the least trustworthy signal in the whole flow.

The Redirected payment-link flow

Create payment linkAsk Clip for a link de pago tied to your internal order id
Redirect the customerSend them to Clip's hosted page — no card data touches you
Customer pays on ClipClip owns the form; PCI is entirely Clip's problem
Customer returnsBack to your success URL — a UX signal, NOT confirmation
Webhook + Consultar un PagoConfirm the real status via the API before you fulfill
Clip owns the card capture on its hosted page; the customer's return redirect is a UX convenience, never proof of payment.

Reconciling Clip against your ledger

Because the authoritative status lives in Clip’s API and not in your process, you need a small reconciliation loop — the same one that keeps the books honest in my multi-rail reconciliation guide.

Three jobs:

  1. Map the payment back to your record. Carry your internal order id through the flow so that when the webhook fires, you know which of your orders it settles. Without that mapping you’re guessing.
  2. Reconcile against Clip’s API, not your logs. For every payment you think is open, re-query “Consultar un Pago” and make your ledger match Clip’s status — grant, refund, or expire accordingly.
  3. Keep a fallback poll. Webhooks get missed — endpoint down, deploy in flight. A periodic job that re-queries pending payments catches anything the notification dropped.

The rule throughout: the verified, fetched Clip status is truth. Not the redirect the customer landed on, not the SDK’s client-side result, not even the raw webhook body. And when a charge later needs reversing, remember the fiscal side — a refund in Mexico usually means a CFDI de Egreso, not just an API call.

No sandbox — testing in production with real MXN$0.01 charges

Here’s the part that surprises every engineer coming from Stripe: Clip has no sandbox and no test cards. You test in production, with tiny real charges. Some banks accept a MXN$1 or even MXN$0.01 charge with practically null commission, so you charge a cent, confirm the full flow end-to-end, and refund it.

The upside is real: a successful test charge means you’re actually production-ready — no “worked in sandbox, broke in prod” surprise, because there was never a sandbox to diverge from. But it demands discipline. Charge a tiny amount, refund it immediately, and put an idempotency key on the test charge so a retry doesn’t double-bill your own card. You’re moving real money from the first keystroke, so guard those credentials. I wrote the full testing playbook in the sibling post: testing Clip with real charges when there’s no sandbox.

Where Clip fits next to Stripe, Mercado Pago and Conekta

Clip completes the quartet. It’s a Mexican gateway that also does physical terminals, and for online payments it exposes a REST API in the same shape as its peers: tokenize, charge, listen for a webhook, re-query for truth. If you already shipped Conekta or Mercado Pago, the only genuinely new things to learn about Clip are the endpoint names (generatecardtoken, realizarpago, “Consultar un Pago”), the 15-minute token lifetime, and the no-sandbox reality.

The engineering rules never change across gateways:

  • Keep the card out of your server — use the SDK for Transparente, or the hosted link for Redireccionado, so PCI stays Clip’s problem.
  • Charge the 15-minute token promptly with realizarpago, "currency": "MXN", and an idempotency key.
  • Treat the webhook as a trigger, not truth — re-query “Consultar un Pago” for the real status and never invent status enums.
  • Reconcile against Clip’s API with a fallback poll, and never fulfill on a client-side “success.”

I build these LATAM payment integrations in production — real pesos moving through Clip, Conekta, Mercado Pago, and Stripe for Mexican clients. If you want a Clip integration that tells the truth about what’s paid, reach out and let’s wire it correctly the first time.

Sources: Clip developer portal, Clip FAQ — no sandbox, and Clip testing reference.