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.
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.
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.
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.
<!-- 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:
- Create a visible
<iframe>on your page (standard sizes are 390x400 or 500x600). - Create an HTML form targeting that iframe.
- Add a hidden input field named
JWTcontaining theP3DS_PAYLOADvalue. - 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.
<!-- 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 -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"
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.
{
"JWT": "[opaque JWT value]",
"DDC_URL": "https://centinelapistag.cardinalcommerce.com/V1/Cruise/Collect",
"DDC_REFERENCEID": "8839201113"
}
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"
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 */,
};
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.
}
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.
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.
}
{
"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 -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]"
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 */,
};
{
"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
}