Skip to main content
Webhooks let you receive instant HTTP notifications when events happen — a KYC is approved, a card transaction occurs, a wallet deposit is received, etc. Instead of polling our API, we push the data to your server.
You must configure at least one active webhook endpoint before you can create customers. This ensures you can receive important notifications about your customers’ activity.

How webhooks work

Step 1: Set up your server

Before creating a webhook, your server must be ready to receive POST requests. Here’s a minimal example:
Your endpoint must return a 2xx status code to acknowledge receipt. Any other status code will be treated as a failure and trigger retries.

Step 2: Register your webhook

Once your server is ready, register your endpoint:
1

We validate your URL

Must start with https://. HTTP URLs are rejected.
2

We send a ping

A test POST is sent immediately to your URL. It must respond with a 2xx status code.
3

Endpoint is created

If the ping succeeds, the endpoint is created and the response includes your secret — this is the only time the secret is shown. Store it securely.
Response:
Save the secret immediately! It is only shown once, at creation time. You need it to verify webhook signatures. If you lose it, use the /rotate-secret endpoint to generate a new one (the old one becomes invalid immediately).

Step 3: You’re ready

That’s it. From now on, every time an event you subscribed to occurs, we’ll POST it to your URL with the full payload and signature headers.

Available events (30)

Subscribe to specific events, or use "*" to receive everything.

Webhook payload format

Every webhook delivery is wrapped in this envelope:

Event payloads

Below is the detailed data payload for each event, along with when it is triggered.

KYC events

Triggered when: The customer’s identity verification is approved. The customer reaches the verified level. The verification is billed at this moment (see balance.debited, reason: kyc_verification).
verified_identity is the identity read on the document. identity_mismatch is true when it differs from the identity you declared for this customer (name or date of birth). In that case status is restricted and a kyc.identity_mismatch event follows.
Triggered when: A KYC is approved but the identity read on the document differs from the one you declared. The customer is restricted by KemyCard and cannot be used until you call POST /kyc/{customerCode}/synchronize. The activate endpoint does not lift this restriction.
Triggered when: You called POST /kyc/{customerCode}/synchronize. The declared identity now matches the verified identity and the restriction is lifted.
Triggered when: The verification is declined (unreadable document, blurry photo, fraud detected, etc.) or abandoned by the customer. Request a new link with POST /kyc/{customerCode}/link to let the customer retry.
Triggered when: The verification session expired before the customer completed it. Request a new link to restart.
Triggered when: The submitted documents are insufficient and the customer must submit again. Request a new link and redirect the customer.
Triggered when: The verification has been submitted and requires a manual review. No action needed: a final kyc.approved or kyc.rejected event follows.

POA events

Triggered when: The customer’s proof of address is approved. The customer reaches the full level. The verification is billed at this moment (see balance.debited, reason: poa_verification).
Triggered when: The proof of address is rejected (name or address does not match, document too old, unreadable document, etc.). The customer stays verified. Submit a new document with POST /poa/{customerCode}/document to retry. If the document is genuine but does not show the customer’s full name, open a support ticket from your secure KemyCard account with the customer_code (priority channel); to follow up, email [email protected] with your ticket number.

Card events

Every card event carries the same base fields, plus the fields specific to the event:
Triggered when: A card requested with POST /cards has been issued and is ready to use. Call GET /cards/{code}/sensitive to get its number, CVV and expiry.
Triggered when: A card requested with POST /cards could not be issued. The card is failed and everything that was debited (initial load and fees) is refunded to the same cards_visa or cards_mastercard balance.
Triggered when: A top-up requested with POST /cards/{code}/topup has been applied. balance is the new card balance.
Triggered when: A top-up could not be applied, for example because the card was terminated in the meantime. The amount and the fee are refunded to the same cards_visa or cards_mastercard balance.
Triggered when: An operation occurs on the card: payment, declined payment, merchant refund or fee. The transaction object has the same format as GET /cards/{code}/transactions; balance is the card balance after the operation.
transaction.type values: settle (payment), auth, decline, refund, fee, auth_fee, fx_fee.
Triggered when: A merchant asks for a 3-D Secure confirmation. The event carries the one-time code your customer must enter on the merchant page: relay it to your customer immediately, it is only valid for a few minutes. KemyCard does not send it to your customer.
Triggered when: You froze the card with POST /cards/{code}/freeze. Payments are declined until it is unfrozen. The payload is the base card payload, with status: frozen.
Triggered when: You unfroze the card with POST /cards/{code}/unfreeze. The payload is the base card payload, with status: active.
Triggered when: The card has been terminated and can no longer be used. Its remaining balance is returned to your cards_visa or cards_mastercard balance, and any top-up still in progress is refunded (see card.topup.failed).

Wallet events

Triggered when: A crypto deposit has been received and confirmed on a customer’s wallet.
Triggered when: A crypto withdrawal initiated by the partner has been confirmed on-chain.
Triggered when: A crypto withdrawal has failed (insufficient on-chain balance, provider error, etc.).

Virtual account events

Triggered when: An incoming bank transfer has been received on a customer’s virtual account.
Triggered when: An outgoing transfer initiated via the API has been executed successfully.
Triggered when: An outgoing transfer has failed (invalid recipient account, bank rejection, etc.).

Gift card events

Triggered when: A gift card order has been fulfilled successfully. The gift card code is available.
Triggered when: A gift card order has failed (product unavailable, provider error, etc.).
Triggered when: A failed gift card order has been refunded. The amount has been credited back to your ops balance.

Balance events

Triggered when: One of your ops balances is credited, either by a USDC (Solana) deposit confirmed on its deposit address, or by a KemyCard API gift card redeemed from your dashboard.USDC deposit:
Gift card redemption (same top-up fees as a USDC deposit, no on-chain fields):
The source field is only present for gift card redemptions. A gift card is issued for one specific ops balance and always credits that balance. gross_amount is the gift card value, fee the top-up fee of that balance and amount the net amount credited.
Triggered when: An automatic debit occurs on your ops balance — card creation, card top-up, subscription payment, gift card purchase, etc. Sent for every paid operation.
Possible reason values:Additional fields (e.g. brand, customer_code, product, billing_cycle) vary depending on the reason.
Triggered when: Your ops balance drops below the threshold you configured (lowBalanceThreshold, default $100). This is triggered in two ways:
  • Real-time — immediately after a debit that pushes the balance below the threshold
  • Scheduled — every 4 hours, a CRON job checks all active partners
An alert email is also sent to your registered email address.
Balance types:

Signature verification

Every delivery includes these headers: How to verify:
1

Extract the timestamp and signature

Parse X-KemyCard-Signature: split by ,, extract t= and v1= values.
2

Compute expected signature

Concatenate {timestamp}.{raw_body} and compute HMAC-SHA256 using your webhook secret.
3

Compare signatures

Use constant-time comparison (e.g. crypto.timingSafeEqual in Node.js, hash_equals in PHP).
4

Reject old timestamps

If the timestamp is more than 5 minutes old, reject the request to prevent replay attacks.

Retry policy

If your endpoint doesn’t respond with a 2xx status code, we retry with exponential backoff: After 7 failed attempts, the delivery is marked as failed and no further retries are made.
If your endpoint accumulates 100 consecutive failures, it will be automatically deactivated. You can reactivate it via PATCH /webhooks/{code} with {"is_active": true}.

Managing endpoints

Update an endpoint

If you change the URL, we send a new ping to validate it. The update is rejected if the new URL doesn’t respond with 2xx.

Disable / re-enable an endpoint

Endpoints can never be deleted — only disabled. This preserves the delivery history.

Rotate the secret

If your secret is compromised, generate a new one immediately:
The old secret is invalidated instantly. Update your server code with the new secret before any pending deliveries arrive.

Test your endpoint

Send a manual ping to verify your endpoint is working:

View delivery history

Check the delivery status for a specific endpoint:
Each delivery shows: event type, status (delivered/failed/pending), HTTP response code, attempt number, duration, and idempotency key.

Best practices

Never trust a webhook payload without verifying the HMAC signature. This prevents attackers from sending fake events to your endpoint.
Store the X-KemyCard-Idempotency-Key header value and check it before processing. Due to retries, you may receive the same event multiple times. Idempotency keys let you safely deduplicate.
Process webhook events asynchronously. Return a 200 response immediately, then handle the event in a background job. If your endpoint takes too long (>10 seconds), the request will time out and trigger a retry.
Don’t use "*" unless you truly need every event. Subscribing to specific events reduces noise and unnecessary load on your server.
Check your webhook dashboard regularly. If you see failures, investigate and fix your endpoint before it reaches the 100-failure auto-deactivation threshold.
When rotating secrets, your server should temporarily accept both the old and new secret during the transition window.