> ## Documentation Index
> Fetch the complete documentation index at: https://partner-docs.contro.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# card.3ds_otp

> Fired when the card network requests a 3D Secure one-time passcode for a partner-issued card

## 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).

<Warning>
  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.
</Warning>

## 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 |

```json theme={null}
{
  "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](/partner/webhooks#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

```javascript theme={null}
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](/partner/webhooks#verify-signatures).
* 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.
