Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.getplu.com/llms.txt

Use this file to discover all available pages before exploring further.

Webhooks notify your server when events happen — user KYC status changes, card transactions, wallet deposits, and collateral updates.

Setup

Configure your webhook endpoint using the Set Webhook endpoint. You’ll receive a webhookSecret (prefixed whsec_) that you’ll use to verify webhook signatures.

Events

KYC Events

EventStatusDescription
user.kyc-eventapprovedUser KYC approved — user can now create cards
user.kyc-eventdeniedUser KYC denied — includes reason with one or more denial codes
user.kyc-eventneedsVerificationAdditional verification required — includes verificationLink

Transaction Events

EventTypeDescription
card.transaction-eventdebitCard transaction approved
card.transaction-eventdebit (declined)Card transaction declined — includes declinedReason
card.transaction-eventcross-borderFX fee charged for non-USD merchant transactions
card.transaction-eventcredit (reversed)Card transaction reversed

Wallet Events

EventDescription
wallet.deposit.cryptoUSDC deposit received and credited to your wallet balance
wallet.deposit.card-withdrawalFunds from a card withdrawal (or liquidation) landed in your partner wallet. data.reference echoes the value passed on POST /partner/card/withdraw.
wallet.withdrawal.cryptoWithdrawal request reached a terminal state — data.status is completed, cancelled, or rejected

Card Events

EventDescription
card.deposit.cryptoUSDC deposit received and credited to a crypto card’s balance

Payload Format

All webhooks are delivered as POST requests with a JSON body.

KYC Webhook

Approved
{
  "event": "user.kyc-event",
  "timestamp": "2026-02-19T20:49:29.927Z",
  "data": {
    "serviceId": "acme-user-7mFQc5aCslAWtkd",
    "status": "approved",
    "email": "[email protected]",
    "firstName": "Jane",
    "lastName": "Doe"
  }
}
Denied
{
  "event": "user.kyc-event",
  "timestamp": "2026-02-19T20:49:29.927Z",
  "data": {
    "serviceId": "acme-user-7mFQc5aCslAWtkd",
    "status": "denied",
    "email": "[email protected]",
    "firstName": "Jane",
    "lastName": "Doe",
    "reason": "WRONG_USER_REGION, REGULATIONS_VIOLATIONS"
  }
}
Needs Verification
{
  "event": "user.kyc-event",
  "timestamp": "2026-02-19T21:02:49.083Z",
  "data": {
    "serviceId": "acme-user-G9KZgnhgbndDdJg",
    "status": "needsVerification",
    "email": "[email protected]",
    "firstName": "Jane",
    "lastName": "Doe",
    "verificationLink": "https://kyc.getplu.com/verify?userId=e5508685-2771-48fd-8cbc-c8c3e6df9179"
  }
}

Transaction Webhooks

Successful debit (USD merchant)
{
  "event": "card.transaction-event",
  "timestamp": "2026-02-19T20:59:59.793Z",
  "data": {
    "serviceCardId": "acme-corp-card-NC3dIGTEgSi89Bf",
    "serviceTransactionId": "acme-corp-card-transactions-ygmyO0zwKt",
    "amount": 10,
    "status": "approved",
    "type": "debit",
    "description": "OPEN AI"
  }
}
Declined transaction
{
  "event": "card.transaction-event",
  "timestamp": "2026-02-19T20:52:06.188Z",
  "data": {
    "serviceCardId": "acme-corp-card-NC3dIGTEgSi89Bf",
    "serviceTransactionId": "acme-corp-card-transactions-1W65PpxTZ1",
    "amount": 150,
    "fxFee": 0,
    "status": "declined",
    "type": "debit",
    "description": "OPEN AI",
    "declinedReason": "Insufficient funds"
  }
}
Successful debit (non-USD merchant) Non-USD transactions produce two webhooks: the debit itself, followed by a separate cross-border FX fee webhook.
{
  "event": "card.transaction-event",
  "timestamp": "2026-02-19T20:53:16.322Z",
  "data": {
    "serviceCardId": "acme-corp-card-NC3dIGTEgSi89Bf",
    "serviceTransactionId": "acme-corp-card-transactions-ZtyJ0iPEU5",
    "amount": 15,
    "status": "approved",
    "type": "debit",
    "description": "OPEN AI"
  }
}
FX Fee (follows the debit)
{
  "event": "card.transaction-event",
  "timestamp": "2026-02-19T20:53:16.448Z",
  "data": {
    "serviceTransactionId": "acme-corp-card-transactions-ZtyJ0iPEU5_fx",
    "serviceCardId": "acme-corp-card-NC3dIGTEgSi89Bf",
    "type": "cross-border",
    "amount": 0.45,
    "description": "FX Fee for 15 usd purchase @ OPEN AI",
    "status": "approved"
  }
}
The FX fee transaction ID is the original transaction ID with _fx appended. Use this to correlate the fee with its parent transaction.
Reversed transaction Sent when a previously approved transaction is reversed (e.g. merchant-initiated refund, chargeback, or authorization release). The reversal credits the original amount back to the card balance.
{
  "event": "card.transaction-event",
  "timestamp": "2026-02-19T21:15:42.610Z",
  "data": {
    "serviceCardId": "acme-corp-card-NC3dIGTEgSi89Bf",
    "serviceTransactionId": "acme-corp-card-transactions-ygmyO0zwKt_rev",
    "amount": 10,
    "fxFee": 0,
    "status": "reversed",
    "type": "credit",
    "description": "OPEN AI"
  }
}
FieldTypeDescription
serviceCardIdstringThe card that received the reversal credit
serviceTransactionIdstringOriginal transaction ID with _rev appended
amountnumberUSD amount credited back to the card
fxFeenumberAlways 0 for reversals
statusstringAlways reversed
typestringAlways credit (funds returned to the card)
descriptionstringOriginal merchant name
The reversal transaction ID is the original transaction ID with _rev appended. Use this to correlate the reversal with the original debit. If the original transaction had an FX fee, only the debit is reversed — the FX fee is not refunded.

Crypto Deposit Webhook

Sent when a USDC transfer to your deposit address is confirmed and credited to your wallet balance.
{
  "event": "wallet.deposit.crypto",
  "timestamp": "2026-02-27T14:57:49.432Z",
  "data": {
    "partnerId": "service-partner-PdhFKbfvYr",
    "amount": 5,
    "txHash": "0x44d5bb06c5029ddfa64dbbb5bc10f91461acb7b2a9ebf1753f41cb32529d646b",
    "chain": "base-sepolia",
    "token": "usdc",
    "fromAddress": "0xa40aCe28a2d66f81D396c7F0ACd46Ae2e4407089",
    "toAddress": "0xFe3E1AD10Ae3ed07Dd79deb3E20E6118dEcE6904"
  }
}
FieldTypeDescription
partnerIdstringYour partner service ID
amountnumberUSD amount credited to your wallet
txHashstringOn-chain transaction hash — use this to verify on a block explorer
chainstringBlockchain network (base in production, base-sepolia in staging)
tokenstringToken received (currently always usdc)
fromAddressstringSender’s wallet address
toAddressstringYour deposit address that received the funds
Your wallet balance is credited automatically when the deposit is confirmed on-chain. You can check your updated balance via the Get Balance endpoint.

Card Withdrawal Wallet Credit Webhook

Sent when a card withdrawal (POST /partner/card/withdraw) or full card liquidation (DELETE /partner/card/terminate/:serviceCardId) credits funds back to your partner wallet. Use data.reference to match the credit to the originating user transaction on your side.
Withdrawal
{
  "event": "wallet.deposit.card-withdrawal",
  "timestamp": "2026-05-06T14:57:49.432Z",
  "data": {
    "partnerId": "service-partner-PdhFKbfvYr",
    "serviceCardId": "acme-card-xyz789jkl012mno",
    "serviceTransactionId": "acme-user-card-transactions-abc123def456ghi",
    "amount": 25,
    "fee": 0.5,
    "netAmount": 24.5,
    "currency": "USD",
    "reference": "ref-test-001"
  }
}
Liquidation
{
  "event": "wallet.deposit.card-withdrawal",
  "timestamp": "2026-05-06T14:58:00.110Z",
  "data": {
    "partnerId": "service-partner-PdhFKbfvYr",
    "serviceCardId": "acme-card-xyz789jkl012mno",
    "serviceTransactionId": "acme-user-card-transactions-zzz999yyy888xxx",
    "amount": 12.34,
    "fee": 0.5,
    "netAmount": 11.84,
    "currency": "USD",
    "source": "liquidation",
    "reference": "ref-liq-001"
  }
}
FieldTypeDescription
partnerIdstringYour partner service ID
serviceCardIdstringThe card the funds were withdrawn from
serviceTransactionIdstringThe card transaction ID — also returned synchronously from the withdraw call
amountnumberGross amount withdrawn from the card (USD)
feenumberWithdrawal fee charged by Bitmama
netAmountnumberAmount actually credited to your partner wallet (amount - fee)
currencystringAlways USD
sourcestring | undefinedPresent and equal to "liquidation" only when the credit came from a card termination; absent for normal withdrawals
referencestring | undefinedEchoes the reference you passed on the withdraw/terminate request. Required on /withdraw, optional on /terminate
This event also fires for partner card liquidations when the terminated card has a positive balance. Use the source field to distinguish liquidations from regular withdrawals.

Crypto Withdrawal Webhook

Sent when a withdrawal request reaches a terminal state. The data.status field tells you which:
  • completed — USDC was sent on-chain. Includes txHash and explorerUrl.
  • cancelled — You cancelled the request via Cancel Withdrawal. Wallet was refunded.
  • rejected — An admin rejected the request. Wallet was refunded. reason may be present.
Completed
{
  "event": "wallet.withdrawal.crypto",
  "timestamp": "2026-04-20T11:55:09.477Z",
  "data": {
    "withdrawalId": "65f0d9f4c8a3b21e9d4a6c01",
    "amount": 100,
    "fee": 0.5,
    "netAmount": 99.5,
    "toAddress": "0xa40aCe28a2d66f81D396c7F0ACd46Ae2e4407089",
    "chain": "base",
    "token": "usdc",
    "status": "completed",
    "txHash": "0x44d5bb06c5029ddfa64dbbb5bc10f91461acb7b2a9ebf1753f41cb32529d646b",
    "explorerUrl": "https://basescan.org/tx/0x44d5bb06c5029ddfa64dbbb5bc10f91461acb7b2a9ebf1753f41cb32529d646b"
  }
}
Cancelled
{
  "event": "wallet.withdrawal.crypto",
  "timestamp": "2026-04-20T11:43:02.108Z",
  "data": {
    "withdrawalId": "65f0d9f4c8a3b21e9d4a6c01",
    "amount": 100,
    "fee": 0.5,
    "netAmount": 99.5,
    "toAddress": "0xa40aCe28a2d66f81D396c7F0ACd46Ae2e4407089",
    "chain": "base",
    "token": "usdc",
    "status": "cancelled"
  }
}
Rejected
{
  "event": "wallet.withdrawal.crypto",
  "timestamp": "2026-04-20T11:50:14.882Z",
  "data": {
    "withdrawalId": "65f0d9f4c8a3b21e9d4a6c01",
    "amount": 100,
    "fee": 0.5,
    "netAmount": 99.5,
    "toAddress": "0xa40aCe28a2d66f81D396c7F0ACd46Ae2e4407089",
    "chain": "base",
    "token": "usdc",
    "status": "rejected",
    "reason": "Suspicious destination address"
  }
}
FieldTypeDescription
withdrawalIdstringThe withdrawal record id
amountnumberTotal USD amount debited from your wallet
feenumberFlat fee deducted ($0.50)
netAmountnumberUSDC actually sent on-chain (only meaningful for completed)
toAddressstringDestination address provided in the request
chainstringAlways base
tokenstringAlways usdc
statusstringOne of completed, cancelled, rejected
txHashstringOn-chain transaction hash. Present only for completed
explorerUrlstringBasescan link. Present only for completed
reasonstringAdmin reason. May be present for rejected
Pre-terminal transitions (pendingapproved) do not fire webhooks. You only receive a webhook when the withdrawal reaches a terminal state.

Card Crypto Deposit Webhook

Sent when a USDC transfer to a crypto card’s deposit address is confirmed and credited to the card’s balance.
{
  "event": "card.deposit.crypto",
  "timestamp": "2026-02-27T15:12:33.841Z",
  "data": {
    "serviceCardId": "acme-card-xyz789jkl012mno",
    "partnerId": "service-partner-PdhFKbfvYr",
    "amount": 25,
    "txHash": "0x55e6cc07d6030ddfa75eccc6bd11f92472bdb8b3b0fcg2864g52dc33630e757c",
    "chain": "base-sepolia",
    "token": "usdc",
    "fromAddress": "0xa40aCe28a2d66f81D396c7F0ACd46Ae2e4407089",
    "toAddress": "0xAb4F2CE20Bf4fe18Ee90efc4E21F7838dFdF7915"
  }
}
FieldTypeDescription
serviceCardIdstringThe card that received the deposit
partnerIdstringYour partner service ID
amountnumberUSD amount credited to the card
txHashstringOn-chain transaction hash
chainstringBlockchain network (base in production, base-sepolia in staging)
tokenstringToken received (currently always usdc)
fromAddressstringSender’s wallet address
toAddressstringCard’s deposit address that received the funds

Signature Verification

Every webhook includes an X-Webhook-Signature header containing an HMAC-SHA256 hex digest. Always verify this signature before processing the webhook.
const crypto = require('crypto');

function verifyWebhook(req, webhookSecret) {
  const signature = req.headers['x-webhook-signature'];
  const payload = JSON.stringify(req.body);

  const expected = crypto
    .createHmac('sha256', webhookSecret)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

Retry Policy

If your endpoint doesn’t respond with a 2xx status code, the webhook is automatically retried with exponential backoff:
AttemptDelay after failureCumulative wait
1st retry1 minute~1 min
2nd retry5 minutes~6 min
3rd retry15 minutes~21 min
4th retry1 hour~81 min
After all retry attempts are exhausted, the webhook is marked as permanently failed. All webhook deliveries — including every retry attempt — are logged with status codes and response bodies for debugging.
Your webhook endpoint must respond within 30 seconds. Long-running processing should be handled asynchronously after acknowledging the webhook with a 200 response.
Retries are processed automatically every 30 seconds. If your endpoint comes back online within the retry window, pending webhooks will be delivered on the next retry cycle without any manual intervention.