Direct Card API and 3D Secure

Advanced. Only for merchants that are PCI DSS compliant and receive raw card data on their own servers. Raw card access must also be enabled on your Dominipay account (otherwise you get RAW_CARD_NOT_ENABLED). If this does not describe you, use the hosted checkout.

The direct card API is server to server, but 3D Secure still needs the cardholder browser. A backend only integration cannot complete cards that require authentication (common for EU and UK cards, and for countries where 3D Secure is forced).

Flow

  1. Your server calls POST /wallet/card/charge with the card.
  2. If the response is status: ok, the payment is approved.
  3. If the response is status: requires_action, send client_secret and publishable_key to the cardholder browser and run stripe.handleNextAction({ clientSecret }).
  4. The cardholder authenticates with their bank.
  5. Your server calls POST /wallet/card/confirm with payment_intent_id.
  6. You receive the paid callback and the USD balance is credited.

1. Charge

POST /wallet/card/charge

Headers:

Content-Type: application/json
Idempotency-Key: order-1042-attempt-1

Body:

{
  "token": "YOUR_TOKEN",
  "secret": "YOUR_SECRET",
  "amount": 100.00,
  "card": {
    "number": "4242424242424242",
    "exp_month": 12,
    "exp_year": 2030,
    "cvc": "123"
  },
  "email": "[email protected]",
  "name": "John Doe",
  "postback": "https://example.com/callbacks/card",
  "return_url": "https://example.com/orders/1042/3ds-return"
}
FieldRequiredDescription
amountYesUSD. Business minimum 5.00.
card.numberYes12 to 19 digits, no spaces. Must pass the Luhn check.
card.exp_monthYes1 to 12.
card.exp_yearYesFour digits, 2024 to 2099.
card.cvcYes3 or 4 digits, as a string.
email, nameNoCardholder details. Recommended.
postbackNoCallback URL. Must be a valid URL. Without it you get no callbacks.
return_urlNoUsed when 3D Secure needs a full page redirect. A platform default is used when omitted.
radar_sessionNoStripe Radar session id collected with Stripe.js. Improves fraud scoring.
idempotency_keyNoSame as the Idempotency-Key header.

Send an Idempotency-Key per logical payment. A retry with the same key returns the same charge instead of charging twice. Without a key, every request is a new charge.

Never log, store or echo card data. Responses never include it. All responses carry Cache-Control: no-store.

Approved

{
  "status": "ok",
  "idTransaction": "3f2b8c1e-7a4d-4e3b-9c2a-1d5e6f7a8b9c",
  "payment_intent_id": "pi_3TestExample0000000001",
  "stripe_status": "succeeded",
  "requires_action": false
}

The USD balance is credited asynchronously and a paid callback follows.

3D Secure required

{
  "status": "requires_action",
  "idTransaction": "3f2b8c1e-7a4d-4e3b-9c2a-1d5e6f7a8b9c",
  "payment_intent_id": "pi_3TestExample0000000001",
  "client_secret": "pi_3TestExample0000000001_secret_TestExample",
  "next_action": { "type": "use_stripe_sdk" },
  "publishable_key": "pk_test_YOUR_PUBLISHABLE_KEY",
  "message": "Card requires 3DS authentication. Run stripe.handleNextAction(client_secret) on the customer browser, then POST /api/wallet/card/confirm with payment_intent_id."
}

Processing

status: processing means the result is not known yet. Wait for the callback or check the status endpoint.

Declined (HTTP 422)

{
  "status": "error",
  "message": "Your card has insufficient funds.",
  "error_code": "insufficient_funds",
  "decline_code": "insufficient_funds",
  "idTransaction": "3f2b8c1e-7a4d-4e3b-9c2a-1d5e6f7a8b9c"
}

Each direct API attempt is final. To retry, call /wallet/card/charge again with a new idempotency key. Do not retry hard declines such as stolen_card, lost_card or fraudulent.

2. Run 3D Secure in the browser

<script src="https://js.stripe.com/v3/"></script>
<script>
  const stripe = Stripe(publishableKey); // publishable_key from the charge response
  const { error, paymentIntent } = await stripe.handleNextAction({ clientSecret });
  // Whatever the outcome, tell your server to call /wallet/card/confirm.
</script>

3. Confirm

POST /wallet/card/confirm

{
  "token": "YOUR_TOKEN",
  "secret": "YOUR_SECRET",
  "payment_intent_id": "pi_3TestExample0000000001"
}

Responses:

ResultBody
Approved (200){"status": "ok", "idTransaction": "...", "payment_intent_id": "...", "stripe_status": "succeeded"}
Another challenge (200, rare)status: requires_action with a new client_secret and next_action. Repeat step 2.
Processing (200)status: processing.
Failed (422)status: error with error_code, decline_code, stripe_status (requires_payment_method or canceled) and message.
Not yours (404)error_code: NOT_FOUND.
Unexpected (422)error_code: CONFIRM_FAILED.

Calling confirm for a payment that already succeeded returns status: ok again, so it is safe to repeat.

If you never call confirm after 3D Secure, the payment stays in requires_action and is not approved.

Callbacks

Direct API charges use the same callbacks as the hosted checkout (paid, refunded, partially_refunded, disputes). A failure that you already received synchronously (422 from /charge or /confirm) does not produce a callback. A failure that the processor reports before you call /confirm (for example a failed 3D Secure) produces a cancelled callback with failure_code. Treat the synchronous response as the source of truth and the callback as a backup. See Webhooks.

Errors specific to this API

HTTPerror_codeMeaning
422INVALID_CARD_NUMBERFails the Luhn check. Nothing was sent to the processor.
422cvc_check_failedThe issuer reported a wrong CVC. The attempt is recorded as failed.
422RAW_CARD_NOT_ENABLEDRaw card access is not enabled for the account.
422PM_CREATE_FAILED, PI_CREATE_FAILEDUnexpected processor error. Check the status before retrying.
422issuer or processor codeDeclined. See Error codes.
429RATE_LIMITEDPlatform wide limit reached. Back off.
429CARD_TESTING_BLOCKED, CARD_VELOCITY_BLOCKEDThis card is temporarily blocked after repeated failures or approvals.
503STRIPE_TEMPORARILY_UNAVAILABLEProcessor unreachable. No charge was created. Safe to retry with the same idempotency key.

Validation errors on this API return only the names of the invalid fields, never their values:

{
  "status": "error",
  "message": "Validation failed",
  "errors": ["card.number", "card.cvc"]
}

Did this page help you?