3-D Secure

3-D Secure authentication, one call, the final charge.

A card charge can be born requires_action asking for 3-D Secure authentication. The hosted SDK runs the challenge and completes it at POST /charges/{id}/confirm-3ds, resolving the final canonical charge.

When 3DS happens

When a 3DS rule requires authentication AND the resolved acquirer supports 3DS, the charge is born requires_action and includes next_action instead of authorizing immediately. If there is no 3DS (no rule, or an acquirer without support), next_action is absent and the flow proceeds normally.

The charge's next_action

next_action only appears while the charge is requires_action. sdk_payload is opaque, acquirer-specific and browser-bound — hand it to the SDK without inspecting it.

next_action
{  "id": "ch_xxx",  "object": "charge",  "status": "requires_action",  "amount_cents": 10990,  "currency": "BRL",  "payment_method": "credit_card",  "capture_method": "automatic",  "captured": false,  "installments": 1,  "next_action": {    "type": "three_ds_challenge",    "three_ds": {      "authentication_id": "tds_xxx",      "acquirer": "cielo",      "sdk_payload": {}    }  },  "request_id": "req_xxx"}

Include the hosted SDK

Load the hosted script and create a client with create(). It installs the global Bunne. Do not pass a key in the browser: today we only issue secret keys (sk_), which must never be exposed. The browser only runs the challenge; confirm-3ds is a backend call.

HTML
<script src="https://bunne.com.br/sdk/v1/bunne-3ds.js"></script><script>  // Installs window.Bunne. Bind non-secret config once.  // No API key in the browser: we only issue secret keys (sk_), which must  // never be exposed. The browser only runs the challenge; confirm-3ds is a  // backend call. Point baseUrl at your API (or your proxy).  const client = Bunne.create({ baseUrl: "https://bunne.com.br" });</script>

The one-call way

threeDSecure(charge) does everything: if the charge does not require 3DS it resolves the charge unchanged (frictionless, no network call); otherwise it runs the challenge and POSTs to confirm-3ds, resolving the final charge. Because confirm needs the sk_ key, use this shortcut only where the key is safe (backend or server-rendered). Today, with no publishable key, prefer the two-step flow below.

JavaScript
// charge is the JSON returned by POST /charges (created from your backend).// Its status may be "requires_action" with a next_action, or already final.const finalCharge = await client.threeDSecure(charge);// finalCharge.status is now final: "paid" / "authorized" / "refused".// No 3DS? threeDSecure resolves the same charge unchanged, with no network call.

Recommended: challenge in the browser, confirm on your backend

In the browser, run initThreeDS(next_action): it runs the challenge with NO key and resolves { authentication_id, acquirer, result }. Send that object to YOUR backend, which POSTs to /charges/{id}/confirm-3ds with the sk_ key (see below). No key ever touches the browser.

JavaScript
// 1. Run the challenge only (acquirer-agnostic). No network call yet.//    Resolves { authentication_id, acquirer, result }.const resolved = await client.initThreeDS(charge.next_action);// 2. Send `resolved` ({ authentication_id, result }) to YOUR backend, which//    confirms with the sk_ key (see the backend example below). If you ever//    issue a browser-safe key, client.confirm3DS(charge.id, resolved) does//    this call for you.

Pure backend (run the challenge yourself)

Teams running the 3DS challenge themselves confirm directly: POST /charges/{id}/confirm-3ds with { authentication_id, result }. It returns the canonical charge and is idempotent — a duplicate confirm is a replay and never authorizes twice.

cURL
curl -X POST https://bunne.com.br/api/v1/charges/ch_xxx/confirm-3ds \  -H "Authorization: Bearer sk_test_xxx" \  -H "Idempotency-Key: order_123_confirm_3ds" \  -H "Content-Type: application/json" \  -d '{    "authentication_id": "tds_xxx",    "result": { "authenticated": true }  }'

Security: never expose sk_ in the browser

Never put a secret sk_... key in the browser (the SDK warns if it detects one). Today we only issue secret keys, so the browser only runs the challenge and /charges/{id}/confirm-3dsis called from your backend with sk_.

Endpoint reference

POST/charges/{id}/confirm-3ds

Confirm 3-D Secure

Completes a `requires_action` charge after the client runs the 3-D Secure challenge. Send the `authentication_id` from the charge's `next_action` and the opaque `result` produced by the challenge. Authorizes only the acquirer bound at prepare time (no cross-acquirer fallback) and returns the canonical charge. A duplicate confirm is an idempotent replay and never authorizes twice.

Auth: Bearer API keycharges:createIdempotency-Key supported

Headers

FieldTypeDescription
Idempotency-KeystringRecommended for confirm retries.

Parameters and body

FieldTypeDescription
idrequiredpath stringCharge id with `ch_` prefix, currently `requires_action`.
authentication_idrequiredstringThe `tds_` id from the charge's `next_action.three_ds.authentication_id`.
resultrequiredobjectOpaque challenge result produced by the 3-D Secure SDK. Passed through verbatim; must not carry raw card data.

Response fields

FieldTypeDescription
idstring`ch_` public id.
statusstring`paid`/`authorized` on success, `refused` on a failed authentication.
capturedbooleanTrue after payment/capture.
paid_atdatetimeUTC timestamp when the charge was paid.
request_idstringRequest trace id.
Request examples
curl -X POST https://bunne.com.br/api/v1/charges/ch_xxx/confirm-3ds \  -H "Authorization: Bearer sk_test_xxx" \  -H "Idempotency-Key: order_123_confirm_3ds" \  -H "Content-Type: application/json" \  -d '{  "authentication_id": "tds_xxx",  "result": {    "authenticated": true  }}'
Response
{  "id": "ch_xxx",  "object": "charge",  "status": "paid",  "amount_cents": 10990,  "currency": "BRL",  "payment_method": "credit_card",  "capture_method": "automatic",  "captured": true,  "installments": 1,  "authorization_code": "MOCK-AUTH",  "acquirer_return_code": null,  "acquirer_return_message": null,  "refused_reason": null,  "buyer": {    "name": "Ada Lovelace",    "email": "ada@example.com",    "phone": "5511999990000",    "document": "93541134780",    "document_type": "cpf",    "ip": "203.0.113.10",    "address": {      "line_1": "Av. Paulista, 1000",      "line_2": null,      "zip_code": "01310100",      "city": "Sao Paulo",      "state": "SP",      "country": "BR"    }  },  "metadata": { "order_id": "order_123" },  "paid_at": "2026-06-25T12:10:05.000Z",  "created_at": "2026-06-25T12:09:40.000Z",  "updated_at": "2026-06-25T12:10:05.000Z",  "request_id": "req_xxx"}