> 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/cardinal-songbird-integration.md).

# Cardinal Songbird Integration

Guide to integrate Cardinal Songbird into your checkout as part of the Firstoken 3D Secure flow.

Songbird is used at two points in the flow:

1. **Device fingerprinting** — after Setup, before Enroll.
2. **Challenge (step-up)** — only when Enroll returns `Pending_authentication`.

All other steps (Setup, Enroll, Validate, Payment) are handled through the Firstoken API. See [3D Secure: Full Implementation Guide](/api-docs/guides/3d-secure-full-implementation-guide.md) for those steps. This page covers the Cardinal frontend integration only.

***

### Credentials overview

| Item                    | Provided by                              | Purpose                                            |
| ----------------------- | ---------------------------------------- | -------------------------------------------------- |
| Firstoken API Key       | You — created in your Firstoken account  | `x-api-key` header on all 3DS and payment requests |
| 3DS service enabled     | Firstoken — enabled on your API Key      | Required for authentication endpoints to work      |
| Cardinal Org Unit ID    | Firstoken — shared when 3DS is activated | Reference / support                                |
| Cardinal API Identifier | Firstoken — shared when 3DS is activated | Reference / support                                |
| Cardinal API Key        | Firstoken — shared when 3DS is activated | Reference / support                                |
| `reference_id`          | Setup response                           | `authentication.reference_id` in Enroll            |
| `token`                 | Setup response                           | `authentication.token` in Enroll                   |
| `access_token`          | Setup response                           | JWT for `Cardinal.setup('init', { jwt })`          |
| Songbird script URL     | This page                                | Load the SDK in your checkout page                 |
| `acs_url` and `pareq`   | Enroll response                          | Challenge only — passed to `Cardinal.continue()`   |

***

### Setup response values

| Value          | Where it comes from | Where you use it                                         |
| -------------- | ------------------- | -------------------------------------------------------- |
| `access_token` | Setup response      | `Cardinal.setup('init', { jwt })` — Songbird fingerprint |
| `token`        | Setup response      | Enroll → `authentication.token`                          |
| `reference_id` | Setup response      | Enroll → `authentication.reference_id`                   |

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

***

### Staging vs. Production

#### Songbird script URL

| Environment | Script URL                                                      |
| ----------- | --------------------------------------------------------------- |
| Staging     | `https://songbirdstag.cardinalcommerce.com/edge/v1/songbird.js` |
| Production  | `https://songbird.cardinalcommerce.com/edge/v1/songbird.js`     |

#### Challenge ACS URL

The ACS URL is returned in the Enroll response — you do not configure it manually.

| Environment | Domain in `acs_url`                    |
| ----------- | -------------------------------------- |
| Staging     | `centinelapistag.cardinalcommerce.com` |
| Production  | `centinelapi.cardinalcommerce.com`     |

{% hint style="danger" %}
Use Staging Songbird URL with Staging Cardinal credentials and Production Songbird URL with Production Cardinal credentials. Never mix environments.
{% endhint %}

***

### Flow overview

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

    C->>FT: POST /setup (card token)
    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 → payment
    else Pending_authentication
        FT-->>C: acs_url, pareq, authentication_transaction_id
        C->>S: Cardinal.continue()
        S-->>C: payments.validated
        C->>FT: POST /validate → POST /payment
    end
```

***

### Step 1 — Load Songbird

Add the script to your checkout page based on environment:

```html
<!-- Staging -->
<script src="https://songbirdstag.cardinalcommerce.com/edge/v1/songbird.js"></script>

<!-- Production -->
<!-- <script src="https://songbird.cardinalcommerce.com/edge/v1/songbird.js"></script> -->
```

***

### Step 2 — Device fingerprinting

Run immediately after receiving the Setup response, before calling Enroll.

#### Required input

| Input                | Source                                                  |
| -------------------- | ------------------------------------------------------- |
| `access_token` (JWT) | Setup response → `data.consumer_auth_info.access_token` |

#### Initialize Songbird and capture SessionId

```javascript
function captureDeviceFingerprint(cardinalJwt) {
  return new Promise((resolve, reject) => {

    Cardinal.configure({
      logging: { level: 'verbose' }, // use 'error' in Production
    });

    Cardinal.on('payments.setupComplete', (data) => {
      const sessionId = data.SessionId || data.sessionId;

      if (!sessionId) {
        reject(new Error('Songbird did not return a SessionId'));
        return;
      }

      resolve(sessionId);
    });

    Cardinal.on('payments.error', (err) => {
      reject(err);
    });

    Cardinal.setup('init', {
      jwt: cardinalJwt,
    });
  });
}
```

#### `payments.setupComplete` response

```json
{
  "SessionId": "0_abc123def456..."
}
```

Store this value and pass it as `device_info.fingerprint_session_id` in Enroll.

{% hint style="warning" %}
Run fingerprinting and Enroll in the same checkout session without reloading the page. The `SessionId` is bound to the `reference_id` from Setup and expires with the session.
{% endhint %}

***

### Device information (`device_info`)

The `device_info` object is sent in the Enroll request. It combines data from Cardinal Songbird, your frontend, and your backend.

#### From Cardinal Songbird

| Field                    | Source                                    |
| ------------------------ | ----------------------------------------- |
| `fingerprint_session_id` | `SessionId` from `payments.setupComplete` |

#### From your frontend (browser APIs)

Collect these with JavaScript immediately before Enroll:

| Field                        | How to collect                           |
| ---------------------------- | ---------------------------------------- |
| `user_agent`                 | `navigator.userAgent`                    |
| `http_browser_screen_width`  | `String(screen.width)`                   |
| `http_browser_screen_height` | `String(screen.height)`                  |
| `http_browser_color_depth`   | `String(screen.colorDepth)`              |
| `http_browser_language`      | `navigator.language`                     |
| `http_browser_java_enabled`  | `navigator.javaEnabled()`                |
| `http_browser_js_enabled`    | `true`                                   |
| `http_browser_time_offset`   | `String(new Date().getTimezoneOffset())` |
| `cookies_accepted`           | `navigator.cookieEnabled`                |
| `http_accept_content`        | `'application/json'`                     |

#### From your backend

| Field        | Source                                                     |
| ------------ | ---------------------------------------------------------- |
| `ip_address` | Client public IP — the browser cannot reliably expose this |

Inject the IP when rendering the checkout page:

```javascript
// Backend (e.g. Express)
// const clientIp = req.headers['x-forwarded-for']?.split(',')[0] || req.socket.remoteAddress;

// Frontend
const clientIp = window.CLIENT_IP; // set by your backend
```

For staging and testing, a fixed test IP or `'0.0.0.0'` may be used.

#### `buildDeviceInfo` helper

```javascript
function buildDeviceInfo(sessionId, clientIp) {
  return {
    fingerprint_session_id: sessionId,
    user_agent: navigator.userAgent,
    http_browser_screen_width: String(screen.width),
    http_browser_screen_height: String(screen.height),
    http_browser_color_depth: String(screen.colorDepth),
    http_browser_language: navigator.language || 'en-US',
    http_browser_java_enabled: navigator.javaEnabled(),
    http_browser_js_enabled: true,
    http_browser_time_offset: String(new Date().getTimezoneOffset()),
    cookies_accepted: navigator.cookieEnabled,
    http_accept_content: 'application/json',
    ip_address: clientIp || '0.0.0.0',
  };
}
```

#### Usage

```javascript
const sessionId = await captureDeviceFingerprint(setupResponse.data.consumer_auth_info.access_token);
const deviceInfo = buildDeviceInfo(sessionId, window.CLIENT_IP);

await enroll({ device_info: deviceInfo, /* card, order_info, bill_to, authentication */ });
```

***

### Step 3 — Enroll

Pass `device_info` together with the values saved from Setup:

```javascript
authentication: {
  reference_id: setupResponse.data.consumer_auth_info.reference_id,
  token: setupResponse.data.consumer_auth_info.token,
  return_url: 'https://your-store.com/3ds-return',
  device_channel: 'Browser',
  mode: 'S',
}
```

#### Enroll outcomes

| Status                      | Action                                                                                      |
| --------------------------- | ------------------------------------------------------------------------------------------- |
| `Authentication_successful` | Frictionless — save `consumer_auth_info` and proceed to payment. No further Songbird steps. |
| `Pending_authentication`    | Challenge required — continue to Step 4.                                                    |
| `Authentication_failed`     | Authentication declined — handle per your policy.                                           |

#### Fields to save when challenge is required

| Enroll field                                       | Used for                                                                  |
| -------------------------------------------------- | ------------------------------------------------------------------------- |
| `consumer_auth_info.acs_url`                       | `AcsUrl` in `Cardinal.continue()`                                         |
| `consumer_auth_info.pareq`                         | `Payload` in `Cardinal.continue()`                                        |
| `consumer_auth_info.authentication_transaction_id` | `OrderDetails.TransactionId` and Validate `authentication.transaction_id` |

***

### Step 4 — Challenge (`Pending_authentication` only)

Songbird must already be initialized from Step 2. Do not call `Cardinal.setup` again.

```javascript
const { consumer_auth_info } = enrollResponse.data;

Cardinal.on('payments.validated', (data) => {
  if (data.ActionCode === 'SUCCESS' || data.ActionCode === 0) {
    // Call POST /validate
  } else {
    // Challenge cancelled or failed — handle per your policy
  }
});

Cardinal.continue(
  'cca',
  {
    AcsUrl: consumer_auth_info.acs_url,
    Payload: consumer_auth_info.pareq,
  },
  {
    OrderDetails: {
      TransactionId: consumer_auth_info.authentication_transaction_id,
    },
  }
);
```

After a successful challenge, call Validate with:

```json
{
  "authentication": {
    "transaction_id": "<authentication_transaction_id from Enroll>"
  }
}
```

***

### Data to persist at each step

| Step        | Action                                      | Save                                                                        |
| ----------- | ------------------------------------------- | --------------------------------------------------------------------------- |
| Setup       | `POST /setup`                               | `reference_id`, `token`, `access_token`                                     |
| Fingerprint | `Cardinal.setup` + `payments.setupComplete` | `SessionId`                                                                 |
| Enroll      | `POST /enroll`                              | `status`; if challenge: `acs_url`, `pareq`, `authentication_transaction_id` |
| Challenge   | `Cardinal.continue` + `payments.validated`  | Confirmation → call Validate                                                |
| Validate    | `POST /validate`                            | `consumer_auth_info` for Payment                                            |

***

### Minimal reference snippet

```javascript
// 1. Setup response
const { reference_id, token, access_token } = setupResponse.data.consumer_auth_info;

// 2. Fingerprint
const sessionId = await captureDeviceFingerprint(access_token);

// 3. Enroll
await fetch('https://api.firstoken-staging.co/v1/risk/authentication/enroll', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': FIRSTOKEN_API_KEY,
  },
  body: JSON.stringify({
    card: {
      number: cardToken,
      expiration_date: cardToken,
    },
    device_info: buildDeviceInfo(sessionId, window.CLIENT_IP),
    authentication: {
      reference_id,
      token,
      return_url: 'https://your-store.com/3ds-return',
      device_channel: 'Browser',
      mode: 'S',
    },
    // transaction_info, order_info, bill_to...
  }),
});

// 4. If Pending_authentication → Cardinal.continue(acs_url, pareq, authentication_transaction_id)
// 5. POST /validate → POST /payment
```

***

### Common errors

| Symptom                       | Likely cause                                                                      |
| ----------------------------- | --------------------------------------------------------------------------------- |
| Enroll fails or 3DS skipped   | Missing `fingerprint_session_id`, or expired `SessionId`                          |
| `SessionId` is undefined      | Missing Setup `access_token`, Songbird not loaded, or page reloaded between steps |
| Challenge modal does not open | Cardinal credentials do not match the merchant on your API Key                    |
| Error 91003 on challenge      | Missing `authentication_transaction_id` in `OrderDetails.TransactionId`           |
| Staging/Production mismatch   | Staging Songbird script used with Production Cardinal credentials, or vice versa  |
| Incomplete `device_info`      | Only `fingerprint_session_id` sent — all browser fields are required              |
