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: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:We validate your URL
https://. HTTP URLs are rejected.We send a ping
2xx status code.Endpoint is created
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 detaileddata payload for each event, along with when it is triggered.
KYC events
kyc.approved
kyc.approved
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.kyc.identity_mismatch
kyc.identity_mismatch
restricted by KemyCard and cannot be used until you call POST /kyc/{customerCode}/synchronize. The activate endpoint does not lift this restriction.kyc.identity_synchronized
kyc.identity_synchronized
POST /kyc/{customerCode}/synchronize. The declared identity now matches the verified identity and the restriction is lifted.kyc.rejected
kyc.rejected
POST /kyc/{customerCode}/link to let the customer retry.kyc.expired
kyc.expired
kyc.resubmission_requested
kyc.resubmission_requested
kyc.pending_review
kyc.pending_review
kyc.approved or kyc.rejected event follows.POA events
poa.approved
poa.approved
full level. The verification is billed at this moment (see balance.debited, reason: poa_verification).poa.rejected
poa.rejected
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:card.activated
card.activated
POST /cards has been issued and is ready to use. Call GET /cards/{code}/sensitive to get its number, CVV and expiry.card.creation_failed
card.creation_failed
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.card.topup.completed
card.topup.completed
POST /cards/{code}/topup has been applied. balance is the new card balance.card.topup.failed
card.topup.failed
cards_visa or cards_mastercard balance.card.transaction
card.transaction
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.card.3ds_request
card.3ds_request
card.frozen
card.frozen
POST /cards/{code}/freeze. Payments are declined until it is unfrozen. The payload is the base card payload, with status: frozen.card.unfrozen
card.unfrozen
POST /cards/{code}/unfreeze. The payload is the base card payload, with status: active.card.terminated
card.terminated
cards_visa or cards_mastercard balance, and any top-up still in progress is refunded (see card.topup.failed).Wallet events
wallet.deposit_received
wallet.deposit_received
wallet.withdrawal_completed
wallet.withdrawal_completed
wallet.withdrawal_failed
wallet.withdrawal_failed
Virtual account events
virtual_account.deposit_received
virtual_account.deposit_received
virtual_account.transfer_completed
virtual_account.transfer_completed
virtual_account.transfer_failed
virtual_account.transfer_failed
Gift card events
gift_card.delivered
gift_card.delivered
gift_card.failed
gift_card.failed
gift_card.refunded
gift_card.refunded
Balance events
balance.deposit_confirmed
balance.deposit_confirmed
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.balance.debited
balance.debited
reason values:brand, customer_code, product, billing_cycle) vary depending on the reason.balance.low_balance
balance.low_balance
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
Signature verification
Every delivery includes these headers:Extract the timestamp and signature
X-KemyCard-Signature: split by ,, extract t= and v1= values.Compute expected signature
{timestamp}.{raw_body} and compute HMAC-SHA256 using your webhook secret.Compare signatures
crypto.timingSafeEqual in Node.js, hash_equals in PHP).Reject old timestamps
Retry policy
If your endpoint doesn’t respond with a2xx status code, we retry with exponential backoff:
failed and no further retries are made.
Managing endpoints
Update an endpoint
2xx.Disable / re-enable an endpoint
Rotate the secret
If your secret is compromised, generate a new one immediately:Test your endpoint
Send a manual ping to verify your endpoint is working:View delivery history
Check the delivery status for a specific endpoint:delivered/failed/pending), HTTP response code, attempt number, duration, and idempotency key.
Best practices
Always verify signatures
Always verify signatures
Use idempotency keys
Use idempotency keys
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.Return 200 quickly
Return 200 quickly
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.Subscribe only to events you need
Subscribe only to events you need
"*" unless you truly need every event. Subscribing to specific events reduces noise and unnecessary load on your server.Monitor your delivery history
Monitor your delivery history
Handle secret rotation gracefully
Handle secret rotation gracefully