> For the complete documentation index, see [llms.txt](https://firstoken.gitbook.io/api-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://firstoken.gitbook.io/api-docs/guides/3d-secure-full-implementation-guide.md).

# 3D-Secure: Full Implementation Guide

This page covers the Firstoken API steps: Setup, Enroll, Validate, and Payment. For the Cardinal Songbird frontend integration (device fingerprinting and challenge), see [Cardinal Songbird Integration](/api-docs/guides/cardinal-songbird-integration.md).

***

### Before you start

#### What you configure

| Item                                                        | Who provides it                                             |
| ----------------------------------------------------------- | ----------------------------------------------------------- |
| Firstoken API Key                                           | You — create it in your Firstoken account                   |
| 3DS service enabled                                         | Firstoken — enabled on your API Key                         |
| Cardinal credentials (Org Unit ID, API Identifier, API Key) | Firstoken — shared when 3DS is activated                    |
| `return_url`                                                | You — your store URL for 3DS return                         |
| Client IP (`device_info.ip_address`)                        | Your backend — inject when rendering checkout or via an API |
| Card tokens (`card.number`, `card.expiration_date`)         | Your frontend — Captures SDK                                |

#### API authentication

Every Firstoken request uses:

```http
Content-Type: application/json
x-api-key: YOUR_FIRSTOKEN_API_KEY
```

***

### Card fields (`card.number`, `card.expiration_date`)

Send `card` in Setup, Enroll, Validate, and Payment — same values across all four steps (same `reference_code`, amount, and currency too).

Firstoken only detokenizes these two fields when talking to Cybersource. All other payload fields (`bill_to`, `order_info`, etc.) are sent as plain data.

#### Accepted formats

| Format                        | Example                        | Typical use                              |
| ----------------------------- | ------------------------------ | ---------------------------------------- |
| Permanent token (detokenize)  | UUID from Captures SDK         | Production — standard integration        |
| Temporary token (transaction) | UUID scoped to one transaction | Single payment / transaction-scoped flow |
| Plain values                  | PAN + `MM/YY`                  | Staging / manual testing only            |

**Permanent token — production:**

```json
"card": {
  "number": "{{98fc05c8-b0df-483f-9091-6b19d6528877 : detokenize}}",
  "expiration_date": "{{98fc05c8-b0df-483f-9091-6b19d6528877 : detokenize}}"
}
```

You may use the same UUID in both `number` and `expiration_date`. Firstoken resolves card data from that token.

**Permanent token:**

```
{{98fc05c8-b0df-483f-9091-6b19d6528877 : detokenize}}
```

**Temporary token — scoped to a single payment flow:**

```
{{b6fc05c8-b0df-483f-9091-6b1e4652aa77 : transaction}}
```

**Plain values:**

```json
"card": {
  "number": "4000000000002701",
  "expiration_date": "12/28"
}
```

***

### Setup response values

These come from the Setup response and are separate from the card token:

| Value                                  | Source         | Used in                                      |
| -------------------------------------- | -------------- | -------------------------------------------- |
| `data.consumer_auth_info.reference_id` | Setup response | Enroll → `authentication.reference_id`       |
| `data.consumer_auth_info.token`        | Setup response | Enroll → `authentication.token`              |
| `data.consumer_auth_info.access_token` | Setup response | Songbird → `Cardinal.setup('init', { jwt })` |

{% hint style="info" %}
&#x20;`token` and `access_token` are different values from the same Setup response. Enroll uses `token`. Songbird uses `access_token`.
{% endhint %}

***

### Flow overview

```mermaid
sequenceDiagram
    participant C as Checkout
    participant FT as Firstoken API
    participant S as Cardinal Songbird

    C->>FT: POST /setup
    FT-->>C: reference_id, token, access_token
    C->>S: Cardinal.setup('init', { jwt: access_token })
    S-->>C: payments.setupComplete → SessionId
    C->>FT: POST /enroll (+ SessionId, device_info)
    alt Authentication_successful
        FT-->>C: consumer_auth_info → proceed to payment
    else Pending_authentication
        FT-->>C: acs_url, pareq, authentication_transaction_id
        C->>S: Cardinal.continue()
        S-->>C: payments.validated
        C->>FT: POST /validate
        FT-->>C: consumer_auth_info
    end
    C->>FT: POST /payment
```

Run Setup, fingerprinting, and Enroll in the same checkout session without reloading the page.

***

### Staging vs. Production

{% hint style="danger" %}
Do not mix environments. All URLs, API Keys, and Songbird scripts must belong to the same environment.
{% endhint %}

| Configuration        | Staging                                                         | Production                                                  |
| -------------------- | --------------------------------------------------------------- | ----------------------------------------------------------- |
| 3DS API base         | `https://api.firstoken-staging.co/v1/risk/authentication`       | `https://api.firstoken.co/v1/risk/authentication`           |
| Payments API base    | `https://api.firstoken-staging.co/v1/payments`                  | `https://api.firstoken.co/v1/payments`                      |
| Firstoken API Key    | Your Staging API Key                                            | Your Production API Key                                     |
| Songbird script      | `https://songbirdstag.cardinalcommerce.com/edge/v1/songbird.js` | `https://songbird.cardinalcommerce.com/edge/v1/songbird.js` |
| Challenge ACS domain | `centinelapistag.cardinalcommerce.com`                          | `centinelapi.cardinalcommerce.com`                          |

***

### Step 1 — Setup

**Endpoint:** `POST {3DS_API_BASE}/setup`

**Request:**

```json
{
  "transaction_info": {
    "type": "setup",
    "reference_code": "ORDER-12345"
  },
  "card": {
    "number": "{{98fc05c8-b0df-483f-9091-6b19d6528877 : detokenize}}",
    "expiration_date": "{{98fc05c8-b0df-483f-9091-6b19d6528877 : detokenize}}"
  }
}
```

**Save from the response:**

| Field                                  | Used for                                |
| -------------------------------------- | --------------------------------------- |
| `data.consumer_auth_info.reference_id` | Enroll `authentication.reference_id`    |
| `data.consumer_auth_info.token`        | Enroll `authentication.token`           |
| `data.consumer_auth_info.access_token` | Songbird fingerprint (`Cardinal.setup`) |

***

### Step 2 — Device fingerprint (Songbird)

Immediately after Setup, before Enroll:

1. Pass `access_token` to `Cardinal.setup('init', { jwt })`.
2. Listen for `payments.setupComplete`.
3. Save `SessionId` for Enroll `device_info.fingerprint_session_id`.

Full details, `device_info` fields, and code samples: [Cardinal Songbird Integration](/api-docs/guides/cardinal-songbird-integration.md).

***

### Step 3 — Enroll

**Endpoint:** `POST {3DS_API_BASE}/enroll`

**Request:**

```json
{
  "transaction_info": {
    "type": "check_enroll",
    "reference_code": "ORDER-12345"
  },
  "card": {
    "number": "{{98fc05c8-b0df-483f-9091-6b19d6528877 : detokenize}}",
    "expiration_date": "{{98fc05c8-b0df-483f-9091-6b19d6528877 : detokenize}}"
  },
  "order_info": {
    "amount_details": {
      "total_amount": "1000",
      "currency": "COP"
    }
  },
  "bill_to": {
    "first_name": "John",
    "last_name": "Doe",
    "email": "buyer@example.com",
    "phone_number": "4158880000",
    "address_1": "1 Market St",
    "address_2": "Suite 200",
    "city": "San Francisco",
    "state": "CA",
    "postal_code": "94105",
    "country": "US"
  },
  "buyer_info": {
    "mobile_phone": "4158880000"
  },
  "device_info": {
    "fingerprint_session_id": "SESSION_ID_FROM_SONGBIRD",
    "ip_address": "203.0.113.10",
    "user_agent": "Mozilla/5.0 ...",
    "http_browser_screen_width": "1920",
    "http_browser_screen_height": "1080",
    "http_browser_color_depth": "24",
    "http_browser_language": "en-US",
    "http_browser_java_enabled": false,
    "http_browser_js_enabled": true,
    "http_browser_time_offset": "300",
    "cookies_accepted": true,
    "http_accept_content": "application/json"
  },
  "authentication": {
    "reference_id": "REFERENCE_ID_FROM_SETUP",
    "token": "TOKEN_FROM_SETUP",
    "return_url": "https://your-store.com/3ds-return",
    "device_channel": "Browser",
    "mode": "S"
  }
}
```

#### Field sources

| Field                                | Source                                       |
| ------------------------------------ | -------------------------------------------- |
| `reference_code`                     | Same value as Setup                          |
| `card`                               | Same object as Setup                         |
| `order_info`, `bill_to`              | Your checkout                                |
| `buyer_info.mobile_phone`            | Usually same as `bill_to.phone_number`       |
| `device_info.fingerprint_session_id` | Songbird `SessionId`                         |
| `device_info.*` (browser fields)     | JavaScript (`navigator`, `screen`)           |
| `device_info.ip_address`             | Your backend                                 |
| `authentication.reference_id`        | Setup response                               |
| `authentication.token`               | Setup response (`token`, not `access_token`) |
| `authentication.return_url`          | Your configured return URL                   |

#### Enroll outcomes

| Status                      | Next step                                                                                 |
| --------------------------- | ----------------------------------------------------------------------------------------- |
| `Authentication_successful` | Save `consumer_auth_info` → Payment (no challenge)                                        |
| `Pending_authentication`    | Save `acs_url`, `pareq`, `authentication_transaction_id` → Challenge → Validate → Payment |
| `Authentication_failed`     | Decline or retry per your policy                                                          |

**If challenge is required, save:**

| Response field                                     | Used for                                  |
| -------------------------------------------------- | ----------------------------------------- |
| `consumer_auth_info.acs_url`                       | `Cardinal.continue()` → `AcsUrl`          |
| `consumer_auth_info.pareq`                         | `Cardinal.continue()` → `Payload`         |
| `consumer_auth_info.authentication_transaction_id` | `OrderDetails.TransactionId` and Validate |

***

### Step 4 — Challenge (Songbird)

Only when Enroll returns `Pending_authentication`. Songbird must already be initialized from Step 2.

See [Cardinal Songbird Integration ](/api-docs/guides/cardinal-songbird-integration.md)— Step 4.

After the user completes the challenge (`payments.validated` with success), call Validate.

***

### Step 5 — Validate

Only after a successful challenge.

**Endpoint:** `POST {3DS_API_BASE}/validate`

Send the same `card`, `order_info`, and `bill_to` as Setup and Enroll, plus `authentication.transaction_id` from Enroll.

**Request:**

```json
{
  "transaction_info": {
    "type": "validate_result",
    "reference_code": "ORDER-12345"
  },
  "card": {
    "number": "{{98fc05c8-b0df-483f-9091-6b19d6528877 : detokenize}}",
    "expiration_date": "{{98fc05c8-b0df-483f-9091-6b19d6528877 : detokenize}}"
  },
  "order_info": {
    "amount_details": {
      "total_amount": "1000",
      "currency": "COP"
    }
  },
  "bill_to": {
    "first_name": "John",
    "last_name": "Doe",
    "email": "buyer@example.com",
    "phone_number": "4158880000",
    "address_1": "1 Market St",
    "address_2": "Suite 200",
    "city": "San Francisco",
    "state": "CA",
    "postal_code": "94105",
    "country": "US"
  },
  "authentication": {
    "transaction_id": "AUTHENTICATION_TRANSACTION_ID_FROM_ENROLL"
  }
}
```

Save `data.consumer_auth_info` from the response for use in Payment.

***

### Step 6 — Payment

**Endpoint:** `POST {PAYMENTS_API_BASE}/`

**Request:**

```json
{
  "transaction_info": {
    "type": "payment",
    "reference_code": "ORDER-12345"
  },
  "card": {
    "number": "{{98fc05c8-b0df-483f-9091-6b19d6528877 : detokenize}}",
    "expiration_date": "{{98fc05c8-b0df-483f-9091-6b19d6528877 : detokenize}}"
  },
  "order_info": {
    "amount_details": {
      "total_amount": "1000",
      "currency": "COP"
    },
    "installments": 1
  },
  "bill_to": {
    "first_name": "John",
    "last_name": "Doe",
    "email": "buyer@example.com",
    "phone_number": "4158880000",
    "address_1": "1 Market St",
    "address_2": "Suite 200",
    "city": "San Francisco",
    "state": "CA",
    "postal_code": "94105",
    "country": "US"
  },
  "device_info": {
    "ip_address": "203.0.113.10"
  },
  "authentication": {
    "eci": "05",
    "cavv": "...",
    "commerce_indicator": "...",
    "pares_status": "...",
    "veres_enrolled": "...",
    "xid": "..."
  }
}
```

#### `authentication` object source

| Flow         | Source of `authentication`                  |
| ------------ | ------------------------------------------- |
| Frictionless | `consumer_auth_info` from Enroll response   |
| Challenge    | `consumer_auth_info` from Validate response |

{% hint style="info" %}
Send the full `consumer_auth_info` object returned by Firstoken. Do not build the `authentication` object manually.
{% endhint %}

Use the same `reference_code`, card tokens, amount, and currency across Setup, Enroll, Validate, and Payment.

***

### Staging test cards

You can use these as plain values or tokenize them with Captures SDK and use the resulting UUIDs:

| Scenario     | Visa               | Mastercard         |
| ------------ | ------------------ | ------------------ |
| Frictionless | `4000000000002701` | `5200000000001005` |
| Challenge    | `4000000000002503` | `5200000000002151` |

Use a valid expiration date (e.g. `12/27`).

***

### Integration checklist

* \[ ] Captures SDK tokenizes card before 3DS flow
* \[ ] Same `card` object in Setup, Enroll, Validate, and Payment
* \[ ] Same `reference_code` across all steps
* \[ ] Setup → fingerprint → Enroll without page reload
* \[ ] Enroll includes complete `device_info` (not only `fingerprint_session_id`)
* \[ ] `authentication.token` in Enroll uses Setup `token` (not `access_token`)
* \[ ] Challenge uses `authentication_transaction_id` in `OrderDetails.TransactionId`
* \[ ] Payment includes full `authentication` from Enroll or Validate
* \[ ] Payment includes `installments` and `device_info.ip_address`
* \[ ] Staging tested before Production cutover

***

### Staging to Production checklist

* \[ ] Production Firstoken API Key
* \[ ] URLs on `api.firstoken.co` (no `-staging`)
* \[ ] Production Songbird script URL
* \[ ] Production Cardinal credentials (from Firstoken)
* \[ ] Production `return_url`
* \[ ] Real client IP injected from backend
* \[ ] HTTPS on checkout
* \[ ] Tested with real cards

***

### Common errors

| Symptom                           | Likely cause                                                           |
| --------------------------------- | ---------------------------------------------------------------------- |
| Payment `System_error`            | Wrong currency for merchant, or incomplete `authentication` object     |
| Enroll fails                      | Missing `fingerprint_session_id`, or fingerprint not run before Enroll |
| `SessionId` undefined             | Songbird not loaded, or Setup `access_token` missing                   |
| Challenge modal does not open     | Cardinal merchant mismatch on your API Key                             |
| Confused `token` / `access_token` | Enroll uses `token`; Songbird uses `access_token`                      |
| Staging/Production mismatch       | Mixed API URLs, API Keys, or Songbird script                           |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://firstoken.gitbook.io/api-docs/guides/3d-secure-full-implementation-guide.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
