3DS Authentication

Read pending 3D Secure challenges on corporate cards and approve or decline them.

Before You Start

Read the following guides before proceeding:

GuideWhy
Getting StartedPlatform overview and setup
Api BasicsRequired headers and request configuration
AuthenticationHow to obtain the corporation token
WebhooksChallenges arrive by webhook
Card DetailsCards must be Active to transact

Overview

When a merchant requests 3D Secure authentication on a corporate card, the transaction is held and a
challenge is raised. The corporation must approve or decline it, and until it does, the transaction does
not proceed.

Challenges are pushed to POST {your_webhook_base_url}/v2/webhooks/3ds and can also be polled. Both
paths surface the same set: the challenges still awaiting a decision for this corporation.


List Pending Challenges

GET /api/v1/cards/3ds/requests

Requires CardSu or CardViewDetails. Returns the corporation's active challenges — those still
awaiting a decision. A decided challenge disappears from the list.

Response:

[
  {
    "transaction_id": "00000000000000000000000000000001",
    "card_id": "64120850-73a1-4df5-a074-d463258c9deb",
    "card_last_4": "5678",
    "amount": "100.00",
    "currency": "USD",
    "merchant_name": "Amazon UK"
  }
]
FieldDescription
transaction_idIssuer-side transaction identifier. The {transactionId} path parameter for the decision
card_idThe card being challenged
card_last_4Last 4 digits of that card
amountTransaction amount, as a string
currencyISO 4217 currency of the transaction
merchant_nameMerchant that requested authentication

The response is a bare JSON array, not an object with a data key.

The list is scoped to the whole corporation, not to the calling employee. Any holder of
CardViewDetails sees challenges on every corporate card, including cards linked to other employees.

Code

const response = await fetch(`${baseUrl}/api/v1/cards/3ds/requests`, {
  headers: { 'Authorization': `Bearer ${corporationToken}` }
});
const challenges = await response.json();
response = requests.get(
    f"{base_url}/api/v1/cards/3ds/requests",
    headers={"Authorization": f"Bearer {corporation_token}"},
)
challenges = response.json()
req, _ := http.NewRequest("GET", baseURL+"/api/v1/cards/3ds/requests", nil)
req.Header.Set("Authorization", "Bearer "+corporationToken)

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()

var challenges []CardTransactionConfirmationResponse
json.NewDecoder(resp.Body).Decode(&challenges)

Approve a Challenge

POST /api/v1/cards/3ds/requests/{transactionId}/approve

Requires CardSu or CardManage. Takes no body.

Path parameters:

ParameterDescription
{transactionId}transaction_id from the pending list or the 3DS webhook. Matched case-insensitively

Response:

{}

Decline a Challenge

POST /api/v1/cards/3ds/requests/{transactionId}/decline

Requires CardSu or CardManage. Takes no body.

Response:

{}

Decisions Are Final

Both endpoints resolve the transaction within the corporation's active set before acting. A
challenge that has already been decided is no longer active, so a second call — a retry, a duplicate
webhook delivery, or an approval after a decline — is refused with 400 ErrorNotFound and
No active 3DS request for this transaction.

The same response is returned for a transaction_id belonging to another corporation and for one that
never existed. A 400 ErrorNotFound therefore means "not yours to decide, or already decided" — it does
not distinguish the two.

Treat the error as success for idempotency purposes. A webhook redelivery that produces
No active 3DS request for this transaction means the decision already landed. Do not escalate it as
a failure, and do not follow it with the opposite decision.


Reading Card Details During a Challenge

A cardholder completing a challenge often needs the card's PIN. The action-confirmation flow has a
dedicated action type for this: sign the message with action_type 3dsChallenge and exchange it at
POST /api/v1/confirmation/signature/verify. The resulting action token is scoped to PIN retrieval.

Use GetCardDetails for PAN, expiry and CVV. See Card Details.


Webhooks

Endpoint: POST {your_webhook_base_url}/v2/webhooks/3ds

Delivered when a challenge is raised. The payload matches an item of the pending-challenge list.

Delivery is best-effort: a 10-second timeout, no retries. Poll GET /api/v1/cards/3ds/requests on a
schedule as well — a challenge left undecided blocks a real transaction.

See Webhooks.


Error Handling

{
  "error_reason": "ErrorNotFound",
  "error_description": "No active 3DS request for this transaction",
  "error_category": {
    "category": "CategoryValidationFailure",
    "http_status_code": 400
  },
  "error_details": [
    { "key": "field", "details": "transaction_id" },
    { "key": "issue", "details": "not_found" }
  ]
}

Validation Errors (400)

Error ReasonError DetailsDescriptionResolution
ErrorMissingFieldfield: transaction_idtransaction id is requiredInclude the transaction id in the path
ErrorNotFoundfield: transaction_idNo active 3DS request for this transactionAlready decided, or belongs to another corporation

Permission Errors (403)

Error ReasonDescriptionResolution
ErrorPermissionDeniedUser does not have required permissionsReading needs CardViewDetails; deciding needs CardManage. A role with only one of them can see challenges but not act, or act but not see

Server Errors (500)

Error ReasonDescriptionResolution
ErrorGeneralFailed to query active 3DS requestsThe 3DS service was unavailable. Retry — the challenge is still held
ErrorGeneralFailed to approve 3DS request in DBThe decision was not recorded. Re-read the pending list before retrying
ErrorGeneralFailed to decline 3DS request in DBAs above

Did this page help you?