> ## Documentation Index
> Fetch the complete documentation index at: https://rain-sandbox-trial.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Cards & Authentication

> React to card lifecycle changes, digital wallet decisions, and 3D Secure challenges as they happen on a cardholder's account.

Card webhooks cover status changes, digital wallet provisioning, and 3D Secure authentication challenges issued during card transactions.

The following events are available:

| Event                                         | Description                                                          |
| --------------------------------------------- | -------------------------------------------------------------------- |
| [`card.created`](#card-created)               | A card is created, whether issued via the API or internally by Rain. |
| [`card.updated`](#card-updated)               | A card status changes or a card is added to a digital wallet.        |
| [`card.notification`](#card-notification)     | A card provisioning or wallet token decision is made.                |
| [`challenge.requested`](#challenge-requested) | A 3D Secure challenge is issued during card authentication.          |

## `card.created`

Sent whenever a card is created, whether you issue it through the API or Rain creates it internally. This webhook is informational only and does not require a response.

```json Payload theme={null}
{
    "id": "bda1b5ca-2bca-4c62-8e17-d80ee6f5aa59",
    "resource": "card",
    "action": "created",
    "version": "1.0.0",
    "body": {
        "id": "277a9918-d463-4bf2-a783-116d02cb9a51",
        "userId": "5086685d-3849-4ec8-bb94-6a2afb73663b",
        "type": "virtual",
        "status": "active",
        "limit": {
            "amount": 8000,
            "frequency": "per24HourPeriod"
        },
        "last4": "5600",
        "expirationMonth": "4",
        "expirationYear": "2032",
        "cardCreationSource": "api_initiated"
    }
}
```

| Field                | Type                | Description                                                                                                               |
| -------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `id`                 | `string`            | Rain card ID                                                                                                              |
| `userId`             | `string`            | ID of the cardholder user                                                                                                 |
| `type`               | `string`            | `virtual` or `physical`                                                                                                   |
| `status`             | `string`            | Card status: `active`, `locked`, `canceled`, or `notActivated`                                                            |
| `limit`              | `object` (optional) | Spending limit configuration                                                                                              |
| `limit.amount`       | `number`            | Limit amount in cents                                                                                                     |
| `limit.frequency`    | `string`            | `per24HourPeriod`, `per7DayPeriod`, `per30DayPeriod`, `perYearPeriod`, `allTime`, or `perAuthorization`                   |
| `last4`              | `string`            | Last 4 digits of the card number                                                                                          |
| `expirationMonth`    | `string`            | Card expiration month (no leading zero)                                                                                   |
| `expirationYear`     | `string`            | Card expiration year                                                                                                      |
| `cardCreationSource` | `string` (optional) | How the card was created: `api_initiated` (through the API) or `rain_initiated` (internally by Rain). Present when known. |
| `currency`           | `string` (optional) | The program's primary currency, lowercase ISO-4217. Added in v1.1.0.                                                      |

The body matches [`card.updated`](#card-updated) without `statusChangeReason` and `tokenWallets`, plus the optional `cardCreationSource`. For version history, see the [`card.created` webhook reference](/changelog/webhooks/v1/card/created).

## `card.updated`

Sent when a card status changes or when a user adds their card to a digital wallet. This webhook is informational only and does not require a response.

```json Payload theme={null}
{
    "id": "bda1b5ca-2bca-4c62-8e17-d80ee6f5aa59",
    "resource": "card",
    "action": "updated",
    "version": "1.1.0",
    "body": {
        "id": "277a9918-d463-4bf2-a783-116d02cb9a51",
        "userId": "5086685d-3849-4ec8-bb94-6a2afb73663b",
        "type": "virtual",
        "status": "canceled",
        "limit": {
            "amount": 8000,
            "frequency": "per24HourPeriod"
        },
        "last4": "5600",
        "expirationMonth": "4",
        "expirationYear": "2032",
        "statusChangeReason": "api_update_cancel_request"
    }
}
```

| Field                | Type                  | Description                                                                                                                                                               |
| -------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                 | `string`              | Rain card ID                                                                                                                                                              |
| `userId`             | `string`              | ID of the cardholder user                                                                                                                                                 |
| `type`               | `string`              | `virtual` or `physical`                                                                                                                                                   |
| `status`             | `string`              | Card status: `active`, `locked`, `canceled`, or `notActivated`                                                                                                            |
| `limit`              | `object` (optional)   | Spending limit configuration                                                                                                                                              |
| `limit.amount`       | `number`              | Limit amount in cents                                                                                                                                                     |
| `limit.frequency`    | `string`              | `per24HourPeriod`, `per7DayPeriod`, `per30DayPeriod`, `perYearPeriod`, `allTime`, or `perAuthorization`                                                                   |
| `last4`              | `string`              | Last 4 digits of the card number                                                                                                                                          |
| `expirationMonth`    | `string`              | Card expiration month (no leading zero)                                                                                                                                   |
| `expirationYear`     | `string`              | Card expiration year                                                                                                                                                      |
| `tokenWallets`       | `string[]` (optional) | Digital wallets where the card is tokenized: `Apple` or `Google Pay`                                                                                                      |
| `statusChangeReason` | `string` (optional)   | The reason for the card status change                                                                                                                                     |
| `createdAt`          | `string`              | ISO-8601 timestamp of when the card was created. Added in v1.2.0.                                                                                                         |
| `updatedAt`          | `string`              | ISO-8601 timestamp of this update. Added in v1.2.0.                                                                                                                       |
| `successorCard`      | `object` (optional)   | The card that replaced this one, with `id`, `last4`, `expirationMonth`, and `expirationYear`. Present only when `statusChangeReason` is `card_replaced`. Added in v1.3.0. |
| `currency`           | `string` (optional)   | The program's primary currency, lowercase ISO-4217. Added in v1.4.0.                                                                                                      |
| `networkCardId`      | `string` (optional)   | The card's network token identifier, for programs with network token reporting enabled. Added in v1.5.0 (replacing `primaryAccountIdentifier` from v1.2.0–v1.4.0).        |

Fields marked with a version are present only when your webhook configuration pins that version or later; see [Versioning](/docs/webhook-delivery#versioning) and the [`card.updated` version history](/changelog/webhooks/v1/card/updated).

The `statusChangeReason` field can take these values:

| Value                             | Description                                                           |
| --------------------------------- | --------------------------------------------------------------------- |
| `api_lock_request`                | Card locked via the lock card endpoint                                |
| `pin_limit_exceeded`              | Card automatically locked due to too many incorrect PIN attempts      |
| `api_unlock_request`              | Card unlocked via the unlock card endpoint                            |
| `last_4_api_unlock_request`       | Card activated via the verify last 4 endpoint                         |
| `api_deactivate_endpoint_request` | Card canceled via the deactivate card endpoint                        |
| `api_cancel_request`              | Card canceled via the update card endpoint (status set to `canceled`) |
| `api_update_cancel_request`       | Card canceled via the update card endpoint                            |
| `user_deleted`                    | Card canceled because the associated user was deleted                 |
| `team_member_deleted`             | Card canceled because the team member was removed                     |
| `card_replaced`                   | Card canceled because it was replaced with a new card                 |
| `api_update_request`              | Card status changed via the update card endpoint                      |
| `wallet_provisioned`              | Card was added to a digital wallet (Apple Pay, Google Pay)            |
| `rain_initiated`                  | Card status was changed internally by Rain                            |

## `card.notification`

The `notification` webhook is sent when there are important status updates or decisions regarding card provisioning, particularly related to digital wallet token provisioning. This webhook is informational only and does not require a response.

<Warning>
  **Limited availability:**

  This webhook is currently triggered when:

  * A card provisioning request is declined or encounters an issue
  * Token wallet provisioning decisions are made
</Warning>

```json Payload theme={null}
{
    "id": "c3d4e5f6-a1b2-4890-9c8d-7e6f5a4b3c2d",
    "resource": "card",
    "action": "notification",
    "version": "1.0.0",
    "body": {
        "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "card": {
            "id": "94534236-927b-44f3-9429-27994c7ebc49",
            "userId": "6d73762a-c401-4898-8f00-d85707e310c0"
        },
        "tokenWallet": "Apple",
        "reasonCode": "PROVISIONING_DECLINED",
        "decisionReason": {
            "code": "WALLET_PROVIDER_RISK_THRESHOLD_EXCEEDED",
            "description": "This tokenization request was declined by the wallet provider due to their proprietary risk assessment system."
        }
    }
}
```

| Field                        | Type                | Description                                                           |
| ---------------------------- | ------------------- | --------------------------------------------------------------------- |
| `id`                         | `string`            | Notification ID                                                       |
| `card.id`                    | `string`            | Rain card ID                                                          |
| `card.userId`                | `string` (optional) | ID of the cardholder user                                             |
| `tokenWallet`                | `string`            | The digital wallet associated with the event: `Apple` or `Google Pay` |
| `reasonCode`                 | `string`            | The reason for the notification                                       |
| `decisionReason`             | `object` (optional) | Additional details about the decision                                 |
| `decisionReason.code`        | `string`            | The decision reason code                                              |
| `decisionReason.description` | `string` (optional) | Human-readable description of the decision reason                     |

The `reasonCode` field takes one of these values:

| Value                   | Description                                          |
| ----------------------- | ---------------------------------------------------- |
| `PROVISIONING_DECLINED` | The digital wallet provisioning request was declined |
| `TOKEN_SUSPENDED`       | An existing wallet token was suspended               |
| `TOKEN_RESUMED`         | A suspended wallet token was reactivated             |
| `TOKEN_DELETED`         | A wallet token was removed                           |

The `TOKEN_*` lifecycle notifications are sent only for programs with network token reporting enabled; contact Rain to enable them. `decisionReason` applies to `PROVISIONING_DECLINED` events.

The `decisionReason.code` field tells you who made the decision and what the cardholder can do about it. Rain may add new codes without a version bump, so treat the field as an open-ended string and handle unrecognized values gracefully.

**Decisions made by Rain**

| Value                         | Description                                                                                                             | Resolution                                                                                                           |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `TOKENIZATION_LIMIT_EXCEEDED` | The cardholder reached the limit on device token activations, enforced per 24 hours and over a rolling multi-day window | Have the cardholder retry after the window elapses. The `decisionReason.description` states which limit was reached. |
| `UNSUPPORTED_REGION`          | The cardholder's country of residence is not supported for Apple Pay provisioning                                       | None. Apple Pay is not available in the cardholder's region.                                                         |
| `CVC_NOT_VALID`               | The CVC/CVV entered does not match the card                                                                             | Have the cardholder re-enter the correct CVC                                                                         |
| `EXPIRY_DATE_INVALID`         | The expiry date entered does not match the card                                                                         | Have the cardholder re-enter the correct expiration date                                                             |
| `UNKNOWN`                     | The wallet provider returned a decision reason Rain has not mapped                                                      | Log the event and handle gracefully. Contact Rain support if you see this code frequently.                           |

**Decisions forwarded from the wallet provider.** Apple and Google decline these based on their own risk assessment. Rain cannot override them.

| Value                                             | Description                                                                         | Resolution                                                    |
| ------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| `WALLET_PROVIDER_RISK_THRESHOLD_EXCEEDED`         | The wallet provider declined due to its risk assessment                             | Have the cardholder wait 24–48 hours before retrying          |
| `DEVICE_SCORE_TOO_LOW`                            | The wallet provider's device score is below its minimum threshold                   | Have the cardholder try a different device or update their OS |
| `EXCESSIVE_PROVISIONING_ATTEMPTS_ACROSS_ACCOUNTS` | The wallet provider detected excessive provisioning attempts across wallet accounts | Have the cardholder wait 24–48 hours before retrying          |
| `EXCESSIVE_CARDS_AND_PROVISIONING_ATTEMPTS`       | The wallet provider detected many different cards added in a short period           | Have the cardholder wait 24–48 hours before retrying          |

Declines surface to the cardholder as a generic "Could Not Add Card" error in the wallet regardless of cause, so match this webhook against support reports to identify the actual reason. See [Push Provisioning troubleshooting](/docs/push-provisioning#troubleshooting).

For version history, see the [`card.notification` webhook reference](/changelog/webhooks/v1/card/notification).

## `challenge.requested`

This webhook is sent when a 3D Secure challenge is initiated for a card transaction. It carries the challenge timing and the transaction context, so you can put the challenge in front of the cardholder.

The same event covers both opt-in 3DS paths, and `deliveryMethod` tells you which flow a challenge belongs to:

| `deliveryMethod`                    | Flow                                                            | What you do                                                                                                                           |
| ----------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `SMS`, `EMAIL`, `WHATSAPP`, `OTHER` | [3DS Forwarding](/docs/3ds#deep-dive-3ds-forwarding)            | Deliver `challenge.oneTimePassword` to the cardholder through that channel.                                                           |
| `PUSH`                              | [Push Notifications (OOB)](/docs/3ds#deep-dive-out-of-band-oob) | Prompt the cardholder to approve or deny in your app, then report the decision to the `/3ds` endpoint. There is no one-time password. |

<Info>
  **Opt-in capability:** By default, Rain handles 3D Secure step-up directly and sends no webhook. You receive `challenge.requested` only if your program is enabled for 3DS Forwarding or Push Notifications. This is independent of whether your program is Rain-Managed or Partner-Managed. See [3D Secure (3DS)](/docs/3ds) for the full Forwarding and OOB flows.
</Info>

The diagram below traces a Forwarding challenge, where you deliver a one-time password. For the OOB equivalent, see the [Push Notifications (OOB) flow](/docs/3ds#push-notifications-oob-flow).

<div className="wf-diagram">
  <div className="legend">
    <span className="lg"><span className="swatch send" />Webhook Rain sends you</span>
    <span className="lg"><span className="swatch handle" />Your handler (ack 2xx)</span>
    <span className="lg"><span className="swatch action" />Action / API call</span>
    <span className="lg"><span className="swatch ext" />Merchant / network</span>
  </div>

  <div className="diagram-shell">
    <svg id="challenge-flow" role="img" aria-label="How a 3D Secure challenge flows: the cardholder checks out, the merchant sends a 3DS request, Rain generates a challenge and POSTs challenge.requested to your app, you deliver the OTP, and the cardholder completes the challenge" viewBox="0 0 1080 900" width="1080" height="900" style={{width: "100%", height: "auto"}}><defs><marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" className="marker-fill-default" /></marker><marker id="arrowSend" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" className="marker-fill-send" /></marker></defs><line x1="150" y1="70" x2="150" y2="880" className="lane-line" stroke-width="1.5" stroke-dasharray="2 6" /><line x1="400" y1="70" x2="400" y2="880" className="lane-line" stroke-width="1.5" stroke-dasharray="2 6" /><line x1="650" y1="70" x2="650" y2="880" className="lane-line" stroke-width="1.5" stroke-dasharray="2 6" /><line x1="900" y1="70" x2="900" y2="880" className="lane-line" stroke-width="1.5" stroke-dasharray="2 6" /><path d="M 150 168 C 150 184, 400 184, 400 200" fill="none" className="conn-default" stroke-width="1.7" marker-end="url(#arrow)" opacity="0.92" /><path d="M 400 268 C 400 284, 650 284, 650 300" fill="none" className="conn-default" stroke-width="1.7" marker-end="url(#arrow)" opacity="0.92" /><path d="M 650 368 L 650 400" fill="none" className="conn-default" stroke-width="1.7" marker-end="url(#arrow)" opacity="0.92" /><path d="M 650 468 C 650 484, 900 484, 900 500" fill="none" className="conn-send" stroke-width="1.7" stroke-dasharray="5 4" marker-end="url(#arrowSend)" opacity="0.92" /><path d="M 900 568 C 900 584, 150 584, 150 600" fill="none" className="conn-default" stroke-width="1.7" marker-end="url(#arrow)" opacity="0.92" /><path d="M 150 668 C 150 684, 400 684, 400 700" fill="none" className="conn-default" stroke-width="1.7" marker-end="url(#arrow)" opacity="0.92" /><path d="M 400 768 C 400 784, 650 784, 650 800" fill="none" className="conn-default" stroke-width="1.7" marker-end="url(#arrow)" opacity="0.92" /><foreignObject x="56" y="16" width="188" height="54"><div className="lane-head"><span className="ico">👤</span><span className="nm">Cardholder</span></div></foreignObject><foreignObject x="306" y="16" width="188" height="54"><div className="lane-head"><span className="ico">🏬</span><span className="nm">Merchant / ACS</span></div></foreignObject><foreignObject x="556" y="16" width="188" height="54"><div className="lane-head rain"><span className="ico">🌧️</span><span className="nm">Rain</span></div></foreignObject><foreignObject x="806" y="16" width="188" height="54"><div className="lane-head"><span className="ico">📱</span><span className="nm">Your app</span></div></foreignObject><foreignObject x="56" y="100" width="188" height="68"><div className="card action"><span className="ct"><span className="tag">Cardholder</span><span className="lab">Checkout with 3DS-enrolled card</span></span></div></foreignObject><foreignObject x="306" y="200" width="188" height="68"><div className="card ext"><span className="ct"><span className="tag">Merchant / ACS</span><span className="lab">3DS authentication request</span></span></div></foreignObject><foreignObject x="556" y="300" width="188" height="68"><div className="card action"><span className="ct"><span className="tag">Rain</span><span className="lab">Generate challenge (OTP, TTL)</span></span></div></foreignObject><foreignObject x="556" y="400" width="188" height="68"><div className="card send"><span className="ct"><span className="tag">Webhook</span><span className="lab mono">challenge.requested</span></span></div></foreignObject><foreignObject x="806" y="500" width="188" height="68"><div className="card handle"><span className="ct"><span className="tag">Handle</span><span className="lab">Deliver OTP (push / in-app)</span></span></div></foreignObject><foreignObject x="56" y="600" width="188" height="68"><div className="card action"><span className="ct"><span className="tag">Cardholder</span><span className="lab">Enter OTP</span></span></div></foreignObject><foreignObject x="306" y="700" width="188" height="68"><div className="card ext"><span className="ct"><span className="tag">Merchant / ACS</span><span className="lab">Validate</span></span></div></foreignObject><foreignObject x="556" y="800" width="188" height="68"><div className="card action"><span className="ct"><span className="tag">Result</span><span className="lab">Pass or fail; auth proceeds</span></span></div></foreignObject></svg>
  </div>
</div>

<Warning>
  This event is synchronous with a 60-second timeout: deliver the one-time password to the cardholder and return a `2xx` response within that window. If you do not respond in time, the authentication fails and the transaction can be declined. The `expiryTime` and `timeToLive` fields define how long the password itself stays valid (300 seconds in the example), which is separate from this response deadline.
</Warning>

```json Payload theme={null}
{
    "id": "d4f8a2b1-3c5e-4a7f-9b1d-6e8f0a2c4d5e",
    "resource": "challenge",
    "action": "requested",
    "version": "2.0.0",
    "body": {
        "id": "8a35e8a4-9a3f-41bb-b262-7a2249b2179d",
        "challenge": {
            "startTime": "2026-03-06T10:00:00.000Z",
            "expiryTime": "2026-03-06T10:05:00.000Z",
            "timeToLive": 300,
            "oneTimePassword": "123456"
        },
        "transaction": {
            "amount": 5000,
            "currency": "USD",
            "merchantName": "AMAZON",
            "merchantCountry": "US"
        },
        "card": {
            "id": "94534236-927b-44f3-9429-27994c7ebc49"
        },
        "deliveryMethod": "SMS"
    }
}
```

| Field                         | Type                | Description                                                                                                                  |
| ----------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `id`                          | `string`            | The notification's own ID. Use it to keep your handler idempotent.                                                           |
| `challenge.startTime`         | `string`            | ISO 8601 timestamp of when the challenge started                                                                             |
| `challenge.expiryTime`        | `string`            | ISO 8601 timestamp of when the challenge expires                                                                             |
| `challenge.timeToLive`        | `number`            | Time remaining in seconds                                                                                                    |
| `challenge.oneTimePassword`   | `string` (optional) | One-time password for the challenge. Absent on OOB challenges, which have no password.                                       |
| `challenge.id`                | `string` (optional) | Challenge ID. Present only on OOB challenges. Echo it back as `challengeId` when you report the decision to `/3ds`.          |
| `transaction.amount`          | `number`            | Transaction amount in the currency's minor unit (for example, `5000` is \$50.00)                                             |
| `transaction.currency`        | `string`            | Currency code (for example, `USD`)                                                                                           |
| `transaction.merchantName`    | `string`            | Merchant name                                                                                                                |
| `transaction.merchantCountry` | `string`            | Merchant country                                                                                                             |
| `transaction.id`              | `string` (optional) | Transaction ID. Present only on OOB challenges. Echo it back as `apataTransactionId` when you report the decision to `/3ds`. |
| `card.id`                     | `string`            | Rain card ID                                                                                                                 |
| `deliveryMethod`              | `string`            | Challenge delivery method: `SMS`, `EMAIL`, `WHATSAPP`, `PUSH`, or `OTHER`                                                    |

The envelope `version` field can be absent for some tenants, so treat it as optional when you parse. For the full schema and version history, see the [`challenge.requested` webhook reference](/changelog/webhooks/v2/challenge/requested).
