card.3ds.received
Card 3DS / OTP verification code received
When Emitted
Emitted after our issuing provider sends a 3DS / OTP verification code callback and it is successfully stored in card_3ds_events.
Currently only two integrations produce this event: WAS and INT.
data.id is the public card resource ID itself (crd_<id>); retrieval still uses GET /v1/cards/{id}.
The actual verification code is exposed through three controlled additional fields in the event body:
- otp_code: the short code for the user to enter manually.
- expires_at: the code’s expiration time, populated only when supplied by the upstream.
- application_id: for a card originating from an application, the public cap_<id> identifies that application
so you can associate it directly; cards without an originating application omit this field.
⚠ The event body does not expose our internal event_key or identify the upstream provider.
Merchants should associate records using only the member, card, and application identifiers they already hold.
⚠ This is different from card.status.updated: receiving a code does not mean the card status changed.
In particular, WAS continues attempting automatic activation after an activation code arrives.
Check the subsequent card.activated event or retrieve card status to determine success; do not treat this event as successful activation.
Triggered after a 3DS or OTP callback from our issuing provider is stored; not initiated directly by a merchant OpenAPI endpoint.
Payload
{
"event_id": "evt_8dc2a13d65904ef29c5ea2f2fe7a1dc2",
"event_type": "card.3ds.received",
"created_at": "2026-09-07T11:38:19Z",
"merchant_id": "acme",
"livemode": true,
"data": {
"object": "card",
"id": "crd_e70b3d41-5a28-4c96-91f0-6b2a8c05d7e3",
"external_member_id": "u_88123",
"status": "received",
"status_version": 13,
"application_id": "cap_3882c371-572f-4d5a-af08-f61fc0964de9",
"otp_code": "54695634",
"expires_at": "2026-09-07T11:38:19.549Z"
}
}
Payload Fields
| Field | Type | Description |
|---|---|---|
id | string | Use this for deduplication. evt_… remains unchanged when the same event is redelivered. |
type | string | Always card.3ds.received |
created_at | string | Time the event was created (RFC3339), not its delivery time. It is unchanged on redelivery. |
data.object | string | Object type; determines which endpoint to query with data.id |
data.id | string | Object ID; use it to retrieve details. |
data.status | string | Treat unrecognized values as unknown and raise an alert; do not fall back to “processing” |
data.status_version | number | Monotonically increasing; use it to discard older states that arrive late. |
Signature Verification and Deduplication
Verify the signature against the raw request body bytes. Do not parse and reserialize the JSON: your JSON library may change key order or whitespace, which changes the signature and can look like a key configuration error.
// Node · Run before parsing JSON
const raw = await readRawBody(req); // Buffer / string; do not use parsed req.body
const expect = crypto.createHmac("sha256", WEBHOOK_SECRET).update(raw).digest("hex");
const got = req.headers["z-signature"]; // Format: t=<unix>,v1=<hex>
if (!timingSafeEqual(expect, parseV1(got))) return res.status(400).end();
// Deduplicate using the envelope id, not data.id
if (await seen(JSON.parse(raw).id)) return res.status(200).end();
For the full procedure, including timestamp tolerance and redelivery semantics, see Webhook Guide.