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