# 3D Secure: Inovio Gateway

The 5-step server-and-browser workflow for running 3D Secure 2.0 through the Inovio Gateway's device data collection and challenge flow.

Source: https://developer.inoviopay.com/api/3ds-gateway.html  
Markdown: https://developer.inoviopay.com/api/3ds-gateway.md

> **SDK coverage of this flow**
> The PHP SDK owns every server leg of this flow: `$client->threeDSecure()->prepare()` (Step 1), the `ThreeDS` block on a normal `sale()`/`authorize()` (Step 3), and `completeSale()`/`completeAuthorize()` (Step 5). Node, Python and Java carry only `BrowserData` on the request (Step 3) and read the challenge outcome from `result.nextAction` (`kind: 'threeDSChallenge'`); none of the three expose a `prepare()`/DDC call or a completion-leg method, so Steps 1 and 5 have no SDK path in those three languages today. If you need the full gateway-managed flow outside PHP, drive the endpoints directly as shown in the cURL examples below, or see [the SDKs](https://developer.inoviopay.com/sdks/index.md).

This flow allows the Inovio Gateway to manage the 3D Secure 2.0 process. A successful 3DS implementation requires a strict sequence of events spanning your backend servers and the customer's browser. Below is the complete 5-step workflow.

### Step 1: Request Device Data Collection (DDC) Parameters

Before you can authorize the card, you must collect device profile data. Make a server-to-server request to the 3DS endpoint (`https://api.inoviopay.com/payment/3dsrequest.cfm`) to get your session parameters.

**Required Parameters:** `REQ_USERNAME`, `REQ_PASSWORD`, `MERCH_ACCT_ID`, and `PMT_BIN` (the first 6 digits of the customer's card), along with your standard billing and transaction details.

The response will contain a `JWT`, a `DDC_URL`, and a `DDC_REFERENCEID`. Store the Reference ID on your server; you will need it in Step 3.


### Step 2: Execute DDC on the Client Side

Device profiling **must** happen on the customer's device. Using the response from Step 1, create a hidden HTML form on your checkout page containing the `JWT` as an input field, and POST it targeting a hidden iframe to the `DDC_URL`.

> **Client-Side Execution Required**
> Do not attempt to POST the JWT to the DDC URL from your backend server. The provider must read the browser's user-agent and device footprint directly to satisfy the issuer's risk algorithms.

```html
<!-- Hidden iframe to execute device profiling -->
<iframe id="ddc-iframe" name="ddc-iframe" style="display:none;"></iframe>

<form id="ddc-form" target="ddc-iframe" method="POST" action="[DDC_URL_FROM_STEP_1]">
  <input type="hidden" name="JWT" value="[JWT_FROM_STEP_1]" />
</form>

<script>
  // Automatically submit the form when the page loads
  document.getElementById('ddc-form').submit();
</script>
```

### Step 3: Initial Enrollment Check

Once the DDC iframe has executed (typically wait 1-2 seconds or listen for the provider's javascript `postMessage`), submit your standard transaction request (e.g., `CCAUTHCAP` or `CCAUTHORIZE`) to the primary gateway endpoint.

You must include two additional parameters in this authorization call:

- `REQUEST_ENROLLMENT=1` (Flags this as a 3DS check)
- `DDC_REFERENCEID` (The exact ID returned in Step 1)
- `P3DS_RETURN_URL` (Where the customer should be sent after the challenge)

If the issuer determines the transaction is low-risk, they may grant a frictionless flow, returning a standard `APPROVED` response. If a challenge is required, the gateway will return a `PENDING` status along with the ACS URL and challenge payload parameters.


### Step 4: Presenting the Challenge (If Required)

If you receive a `PENDING` status, the cardholder's bank requires them to complete an authentication challenge (like entering an SMS code). The response will contain an `P3DS_ACS_URL` and a `P3DS_PAYLOAD`.

To display this challenge to the user without them leaving your checkout flow:

1. Create a visible `<iframe>` on your page (standard sizes are 390x400 or 500x600).
2. Create an HTML form targeting that iframe.
3. Add a hidden input field named `JWT` containing the `P3DS_PAYLOAD` value.
4. Use JavaScript to automatically submit the form to the `P3DS_ACS_URL`.

Once the cardholder completes the challenge inside the iframe, the issuer will automatically redirect the iframe to the `P3DS_RETURN_URL` you specified in Step 3. Your backend should listen on that URL to capture the final variables.

```html
<!-- 1. Visible iframe for the user to complete the challenge -->
<!-- Standard 3DS window sizes: 250x400, 390x400, 500x600, 600x400 -->
<iframe id="challenge-iframe" name="challenge-iframe" width="390" height="400" frameborder="0"></iframe>

<!-- 2. Form targeting the iframe -->
<form id="challenge-form" target="challenge-iframe" method="POST" action="[P3DS_ACS_URL_FROM_STEP_3]">
  <!-- 3. The payload from Step 3 -->
  <input type="hidden" name="JWT" value="[P3DS_PAYLOAD_FROM_STEP_3]" />
</form>

<script>
  // 4. Automatically submit the form to load the ACS UI into the iframe
  document.getElementById('challenge-form').submit();
</script>
```

### Step 5: Final Authorization

After the customer completes the challenge and the issuer redirects back to your server, you must submit the final call to Inovio to complete the transaction.

Send a standard transaction request (matching the call you made in Step 3) but include the authentication results:

- `P3DS_PROCTRANSID`: The Transaction ID returned from the ACS.
- `REQUEST_PARES`: The authentication response payload from the ACS.

**cURL**

```bash
curl -X POST "https://api.inoviopay.com/payment/3dsrequest.cfm" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&merch_acct_id=110203&pmt_bin=411111"
```

**PHP**

```php
use Inovio\Gateway\{Credentials, InovioClient, ThreeDSPrepare};

$client = new InovioClient(
    new Credentials('api_user', 'P@ssw0rd!', '12345', merchAcctId: '110203'),
    'SANDBOX'
);

$ddc = $client->threeDSecure()->prepare(
    ThreeDSPrepare::bin('411111', 'USD', 'US')
);

// $ddc->jwt, $ddc->ddcUrl — POST jwt to ddcUrl in a hidden iframe (Step 2).
// $ddc->ddcReferenceId — store it; you need it for the ThreeDS block in Step 3.
```

**Response**

```json
{
  "JWT": "[opaque JWT value]",
  "DDC_URL": "https://centinelapistag.cardinalcommerce.com/V1/Cruise/Collect",
  "DDC_REFERENCEID": "8839201113"
}
```

**cURL**

```bash
curl -X POST "https://api.inoviopay.com/payment/pmt_service.cfm" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "request_action=CCAUTHCAP&req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&request_response_format=JSON&pmt_numb=4111111111111111&pmt_expiry=122026&pmt_key=123&request_currency=USD&li_value_1=49.99&xtl_order_id=INV-999&request_enrollment=1&ddc_referenceid=[DDC_REFERENCEID_FROM_STEP_1]&p3ds_return_url=https://yourshop.example.com/3ds-return"
```

**PHP**

```php
use Inovio\Gateway\Model\{BrowserData, LineItem, Money, PaymentMethods, ThreeDS};
use Inovio\Gateway\Request\TransactionRequest;

$req = (new TransactionRequest(
    PaymentMethods::card('4111111111111111', '122026', '123'),
    [new LineItem('SKU-992', 1, Money::of('49.99', 'USD'))]
))->withIdempotency('INV-999');

// BrowserData is required — the gateway silently skips 3DS without it.
$req->browser = new BrowserData(
    language: 'en-US', userAgent: $_SERVER['HTTP_USER_AGENT'], header: $_SERVER['HTTP_ACCEPT']
);
$req->threeDS = new ThreeDS($ddc->ddcReferenceId, 'https://yourshop.example.com/3ds-return');

$result = $client->sale($req);

match ($result->status) {
    'APPROVED', 'DECLINED' => /* frictionless — check $result->threeDS->eci */,
    'PENDING' => /* $result->nextAction->jwt / ->redirectUrl / ->procTransId — Step 4 challenge */,
    default => /* RUNNING | FAILED */,
};
```

**Node**

```ts
const result = await client.sale({
  paymentMethod: PaymentMethods.card('4111111111111111', '122026', '123'),
  lineItems: [{ productId: 'SKU-992', count: 1, value: Money.of('49.99', 'USD') }],
  idempotency: { xtlOrderId: 'INV-999' },
  // BrowserData is required — the gateway silently skips 3DS without it.
  browser: { language: 'en-US', userAgent: req.headers['user-agent']!, header: req.headers['accept']! },
});

if (result.status === 'PENDING' && result.nextAction?.kind === 'threeDSChallenge') {
  // result.nextAction.jwt / .redirectUrl / .procTransId — Step 4 challenge.
  // Node has no prepare()/completeSale() — drive Steps 1 and 5 over HTTP directly.
}
```

**Python**

```python
result = client.sale(TransactionRequest(
    payment_method=PaymentMethods.card("4111111111111111", "122026", "123"),
    line_items=[LineItem("SKU-992", 1, Money.of("49.99", "USD"))],
    idempotency=Idempotency(xtl_order_id="INV-999"),
    # BrowserData is required — the gateway silently skips 3DS without it.
    browser=BrowserData(language="en-US", user_agent=user_agent, header=accept_header),
))

if result.status is TransactionStatus.PENDING and result.next_action and result.next_action.kind == "threeDSChallenge":
    pass  # result.next_action.jwt / .redirect_url / .proc_trans_id — Step 4 challenge.
    # Python has no prepare()/complete_sale() — drive Steps 1 and 5 over HTTP directly.
```

**Java**

```java
TransactionRequest req = new TransactionRequest(
    PaymentMethods.card("4111111111111111", "122026", "123"),
    new LineItem("SKU-992", 1, Money.of("49.99", "USD")))
    .idempotency("INV-999");

// BrowserData is required — the gateway silently skips 3DS without it.
req.browser = new RequestParts.BrowserData("en-US", userAgent, acceptHeader);

TransactionResult result = client.sale(req);

if (result.status() == TransactionStatus.PENDING
    && result.nextAction() != null && "threeDSChallenge".equals(result.nextAction().kind())) {
    // result.nextAction().jwt / .redirectUrl / .procTransId — Step 4 challenge.
    // Java has no prepare()/completeSale() — drive Steps 1 and 5 over HTTP directly.
}
```

**Response**

```json
{
  "REQUEST_ACTION": "CCAUTHCAP",
  "REQ_ID": "68192033",
  "TRANS_STATUS_NAME": "PENDING",
  "TRANS_VALUE": 49.99,
  "CURR_CODE_ALPHA": "USD",
  "TRANS_VALUE_SETTLED": 49.99,
  "CURR_CODE_ALPHA_SETTLED": "USD",
  "TRANS_EXCH_RATE": "",
  "TRANS_ID": 8839201112,
  "CUST_ID": 9928102,
  "XTL_CUST_ID": "cUsT992xP",
  "PO_ID": 77281920,
  "XTL_ORDER_ID": "INV-999",
  "BATCH_ID": 882910,
  "PROC_NAME": "Inovio Primary",
  "MERCH_ACCT_ID": 110203,
  "CARD_BRAND_NAME": "Mastercard",
  "CARD_TYPE": "MASTERCARD BLACK CARD",
  "CARD_CLASS": "Consumer Credit",
  "CARD_PREPAID": 0,
  "CARD_BANK": "CREDOMATIC INTERNATIONAL",
  "CARD_COUNTRY": "CRI",
  "CARD_DETAIL": "Credit",
  "CARD_BALANCE": "",
  "PMT_L4": "3535",
  "PMT_ID": 8829102,
  "PMT_ID_XTL": "",
  "PMT_AAU_UPDATE_DT": "",
  "PMT_AAU_UPDATE_DESC": "",
  "PROC_UDF01": "",
  "ANI_RESP_DECISION": "",
  "PROC_UDF02": "",
  "PROC_AUTH_RESPONSE": "",
  "PROC_RETRIEVAL_NUM": "",
  "PROC_REFERENCE_NUM": "",
  "PROC_REDIRECT_URL": "",
  "AVS_RESPONSE": "",
  "CVV_RESPONSE": "",
  "CARD_BRAND_TRANSID": "",
  "REQUEST_API_VERSION": "4.14",
  "P3DS_VENDOR": "",
  "P3DS_RESPONSE": "",
  "P3DS_ACS_URL": "https://acs.issuer-example.com/acs/challenge",
  "P3DS_PAYLOAD": "[opaque challenge payload]",
  "PO_LI_ID_1": "8829103",
  "PO_LI_COUNT_1": 1,
  "PO_LI_AMOUNT_1": "49.99",
  "PO_LI_PROD_ID_1": "SKU-992",
  "MBSHP_ID_1": "88291",
  "TRANS_NTOKEN_USED": 1
}
```

**cURL**

```bash
curl -X POST "https://api.inoviopay.com/payment/pmt_service.cfm" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "request_action=CCAUTHCAP&req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&request_response_format=JSON&pmt_numb=4111111111111111&pmt_expiry=122026&pmt_key=123&request_currency=USD&li_value_1=49.99&xtl_order_id=INV-999&p3ds_proctransid=[FROM_ACS]&request_pares=[FROM_ACS]"
```

**PHP**

```php
use Inovio\Gateway\Model\ThreeDSChallengeResult;

// Reuse the SAME $req that ran the Step 3 enrollment leg.
$final = $client->threeDSecure()->completeSale(
    $req,
    new ThreeDSChallengeResult($_POST['TransactionId'], $_POST['Response'] ?? '')
);

match ($final->status) {
    'APPROVED' => /* $final->threeDS->eci — 05/06 means full authentication (liability shift) */,
    'DECLINED' => /* $final->outcome->service */,
    default => /* PENDING | RUNNING | FAILED */,
};
```

**Response**

```json
{
  "REQUEST_ACTION": "CCAUTHCAP",
  "REQ_ID": "68192033",
  "TRANS_STATUS_NAME": "APPROVED",
  "TRANS_VALUE": 49.99,
  "CURR_CODE_ALPHA": "USD",
  "TRANS_VALUE_SETTLED": 49.99,
  "CURR_CODE_ALPHA_SETTLED": "USD",
  "TRANS_EXCH_RATE": "",
  "TRANS_ID": 8839201112,
  "CUST_ID": 9928102,
  "XTL_CUST_ID": "cUsT992xP",
  "PO_ID": 77281920,
  "XTL_ORDER_ID": "INV-999",
  "BATCH_ID": 882910,
  "PROC_NAME": "Inovio Primary",
  "MERCH_ACCT_ID": 110203,
  "CARD_BRAND_NAME": "Mastercard",
  "CARD_TYPE": "MASTERCARD BLACK CARD",
  "CARD_CLASS": "Consumer Credit",
  "CARD_PREPAID": 0,
  "CARD_BANK": "CREDOMATIC INTERNATIONAL",
  "CARD_COUNTRY": "CRI",
  "CARD_DETAIL": "Credit",
  "CARD_BALANCE": "",
  "PMT_L4": "3535",
  "PMT_ID": 8829102,
  "PMT_ID_XTL": "",
  "PMT_AAU_UPDATE_DT": "",
  "PMT_AAU_UPDATE_DESC": "",
  "PROC_UDF01": "",
  "ANI_RESP_DECISION": "",
  "PROC_UDF02": "",
  "PROC_AUTH_RESPONSE": "AUTH99",
  "PROC_RETRIEVAL_NUM": "1029384A-B8C7-D6E5-F4G3-H2I1J0K9L8M7",
  "PROC_REFERENCE_NUM": "REF10293847",
  "PROC_REDIRECT_URL": "",
  "AVS_RESPONSE": "M",
  "CVV_RESPONSE": "M",
  "CARD_BRAND_TRANSID": "",
  "REQUEST_API_VERSION": "4.14",
  "P3DS_VENDOR": "",
  "P3DS_RESPONSE": "Y",
  "PO_LI_ID_1": "8829103",
  "PO_LI_COUNT_1": 1,
  "PO_LI_AMOUNT_1": "49.99",
  "PO_LI_PROD_ID_1": "SKU-992",
  "MBSHP_ID_1": "88291",
  "TRANS_NTOKEN_USED": 1
}
```
