Event: card.3ds_otp
Sent when the card network requests a 3D Secure (3DS) one-time passcode (OTP) for a card issued under your partner program. Your application is responsible for delivering the OTP to the cardholder via your preferred channel (SMS, email, push notification, in-app).
3DS OTPs are time-sensitive. Deliver the OTP to your cardholder within 60 seconds of receiving this event. After that window the cardholder’s transaction may fail.
Payload
| Field | Type | Description |
|---|
cardId | string | Contro card ID the OTP applies to |
cardholderId | string | null | Partner cardholder ID associated with the card |
last4 | string | Last 4 digits of the card number, for display in your message to the cardholder |
otpCode | string | The 3DS one-time passcode to deliver to the cardholder |
transactionAmount | string | undefined | Transaction amount the OTP authorizes, in major units (e.g. "42.50") |
transactionCurrency | string | undefined | ISO 4217 currency code of the transaction |
merchant | string | undefined | Merchant name attempting the transaction |
timestamp | string | ISO 8601 timestamp the event was generated |
{
"cardId": "card_xyz789",
"cardholderId": "ch_abc123",
"last4": "0000",
"otpCode": "123456",
"transactionAmount": "42.50",
"transactionCurrency": "USD",
"merchant": "Coffee Shop",
"timestamp": "2026-04-16T10:00:00Z"
}
Response
Your endpoint must return a 2xx status code within 30 seconds to acknowledge receipt. Any non-2xx response or timeout triggers the retry policy, but note that retries past the 60-second OTP validity window will not help the cardholder complete the transaction.
| Status code | Meaning |
|---|
200 | OTP received; you have dispatched it to the cardholder |
202 | Received; will dispatch asynchronously |
| Any non-2xx | Delivery failed — Contro will retry per the retry policy |
Example handler
app.post("/webhooks/contro", async (req, res) => {
const eventType = req.headers["x-contro-event"];
if (eventType === "card.3ds_otp") {
const { cardholderId, last4, otpCode, merchant, transactionAmount, transactionCurrency } = req.body;
const cardholder = await db.cardholders.findById(cardholderId);
await sms.send(cardholder.phoneNumber, {
message: `Your verification code for the ${transactionCurrency} ${transactionAmount} purchase at ${merchant} (card ending ${last4}) is ${otpCode}.`,
});
}
res.status(200).send("OK");
});
Security considerations
- Treat
otpCode as sensitive: do not log it, do not persist it past the immediate dispatch.
- Verify the
X-Contro-Signature header before trusting the payload — see signature verification.
- Use a fast, idempotent dispatch path. Retries can deliver the same
otpCode more than once; sending it twice to the cardholder is acceptable, but failing to send it on the first attempt is not.