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


---

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