ROKIConnect

Mode 2 - Embedded components

Card fields rendered by ROKI inside a secure iframe on your own site. The customer never leaves your page, and the card number never touches your code.

22.1 The flow

1. Browser loads the ROKI SDK and mounts the component with pk_* only
2. Customer fills the card fields (inside ROKI's iframe)
3. Your "pay" button calls payment.submit()
4. The SDK returns a single-use tok_* to your page
5. Your page posts that tok_* to YOUR OWN backend
6. Your backend calls POST /api/connect/embed/confirm with sk_* + tok_* + amount
7. You get approved / declined / pending (3-D Secure)

The amount is set by your backend at step 6, not by the browser. Mounting needs no payment to exist beforehand.

22.2 Frontend

<script src="https://aura.roki.systems/connect/components/v1/roki.js"></script>

<form id="order-form">
  <div id="roki-payment"></div>
  <input type="hidden" name="roki_payment_token" id="roki_payment_token">
  <button type="submit" id="buy-now" disabled>Buy now</button>
</form>

<script>
  const roki = RokiConnect({ publishableKey: 'pk_test_...', locale: 'en' });

  const payment = roki.createPaymentComponent({
    customer: { name: 'John Doe', email: 'john@example.com' },
    appearance: { theme: 'light', accentColor: '#F97316' }
  });
  payment.mount('#roki-payment');

  // Keep the button disabled until the card fields are complete.
  payment.on('form_complete', (data) => { buyBtn.disabled = !(data && data.complete); });

  form.addEventListener('submit', (e) => {
    e.preventDefault();
    buyBtn.disabled = true;          // also disable while confirming
    payment.submit();
  });

  payment.on('payment_token_ready', async (data) => {
    // Send the token to YOUR backend. Never call ROKI's secret-key endpoints from here.
    const res = await fetch('/your-backend/confirm-payment', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ payment_token: data.payment_token, order_id: 123 })
    });
    showResult(await res.json());
  });

  payment.on('payment_failed', (data) => {
    buyBtn.disabled = false;
    showResult({ status: 'declined', IsoResponseCode: data.IsoResponseCode, Errors: data.Errors });
  });
</script>

SDK options: paymentId (optional), customer, description, metadata, locale, saveCard, appearance.theme (light/dark), appearance.accentColor. You cannot replace the iframe's HTML.

22.3 Backend confirm

POST https://aura.roki.systems/api/connect/embed/confirm
Authorization: Bearer sk_test_...

{
  "amount": 500.00,
  "currency_code": "HNL",
  "external_reference": "order-123",
  "payment_token": "tok_xxxx",
  "publishable_key": "pk_test_...",
  "success_redirect_url": "https://merchant.example/success",
  "failed_redirect_url": "https://merchant.example/failed"
}

Note the base path: /api/connect/embed, not /api/connect/v1.

Both redirect URLs must be HTTPS. There is no local-development exception, despite what the error message claims. Rejecting one returns:

422 success_redirect_url_invalid
"success_redirect_url debe ser una URL HTTPS valida (http solo se permite en desarrollo local)."

There is no local-development exception. What is accepted:

URL Accepted
http://localhost:4000/ no
http://127.0.0.1/ no
http://anything.test/ok no
https://localhost:4000/ yes
https://your-site.com/gracias?order=1 yes

So http is refused everywhere, including loopback, and query strings are fine. To develop locally you need TLS on localhost, a tunnel, or - simplest - point the two URLs at any HTTPS page you already control. They only matter when a 3-D Secure challenge sends the customer away and back; the ordinary approved/declined answer arrives in the /confirm response itself.

The validation order helps when debugging: the body is validated before the token, so a success_redirect_url_invalid means the request never got as far as looking at your tok_*.

22.3.1 A pending 3-D Secure answer, transcribed from a real one

When 3-D Secure needs to step in, the answer is not an error even though it reads like one. Three things in this single response will mislead an integrator:

HTTP 202
{
  "status": "pending",
  "code": "authentication_required",
  "error_code": "authentication_required",
  "error_message": "La autenticacion del pago aun esta en progreso. Consulte GET /payments/{id} o espere el webhook.",
  "transaction_id": "21e41243-4214-46b5-a29b-ff7966adb629",
  "amount": 25,
  "currency": "340",
  "authentication_url": "https://aura.roki.systems/connect/components/v1/confirm-challenge/21e41243-...?expires=1786751541&sig=a0d14de9..."
}
  1. The status code is 202, not 200. Code written as if (res.status !== 200) fail() rejects a payment that is merely awaiting authentication - and the customer may still complete it, leaving the order failed while the money moves.
  2. It carries error_code and error_message although nothing failed. if (body.error_code) is the natural check to write and it is wrong here. Branch on status instead: approved, declined, pending.
  3. The authentication_url is signed and expires. The observed window was 15 minutes (expires is a Unix timestamp, with a sig that seals it). Do not store it, email it, or render it later - send the customer there immediately.

Load that URL as a full-page redirect or a real popup. The official documentation's example puts it in a 1x1 invisible iframe, where the challenge cannot be completed and the payment simply never advances.

The transaction_id in this response is the one you will see again in the webhook and the one void and refund take. Persist it here, before the customer disappears into the challenge.

22.4 The three outcomes

approved carries transaction_id plus transaction_details with the full financial breakdown

declined carries the processor's own fields, capitalized as the processor sends them:

{ "status": "declined", "IsoResponseCode": "05", "Errors": [{ "Code": "201", "Message": "..." }] }

Branch on Errors[0].Code, not on the message text.

pending means 3-D Secure is required:

{ "status": "pending", "authentication_url": "https://...", "transaction_id": "9e2b..." }

The final outcome arrives through the usual payment.approved / payment.failed webhooks - do not treat pending as a failure.

How you present authentication_url decides whether high-value payments work. ROKI's tested flow redirects the customer to that URL. Some code samples instead append it as a 1x1 invisible iframe:

// DO NOT ship this as your only 3DS path.
frame.style.cssText = 'position:absolute;width:1px;height:1px;opacity:0;border:0;';

That is fine only for frictionless authentication, where the issuer approves silently. The moment the issuer requires a challenge - a code by SMS, the bank's app, a password - the customer sees nothing at all and the payment hangs forever. Challenges are most common on exactly the high-value transactions you least want to lose.

Use one of these instead:

Whichever you pick, the webhook remains the source of truth: a customer who completes the challenge and then closes the tab still produces payment.approved, and your order must be fulfilled from that event, not from the redirect.

22.5 What to get right

Keep the pay button disabled until form_complete reports the fields are valid, and disable it again while confirming - otherwise a double click produces two tokens. Treat tok_* as single-use: on a decline, either create a new payment or remount the component (a reusable: true payment makes the remount cleaner). And never put sk_* in the page: the token goes to your server, your server calls ROKI.