Verification Codes via Webhook π
Receive verification codes directly in your application via webhook for automated payment flows.
How It Works
When a purchase requires verification, the code is automatically sent to your webhook instead of SMS.
Purchase β Verification Code β Your Webhook β Complete PaymentRetries
If your webhook fails (non-2xx response or timeout), Cardda makes up to 3 attempts (initial + retries after ~3 s and ~18 s). Every attempt has the same payload id.
Setup
Ask your Cardda account team to set up forwarding for your card. You'll need to provide:
- Webhook URL: Your endpoint (e.g.,
https://yourapp.com/api/codes) - Card ID: The card you want to configure
Cardda will provide you with:
- webhook_secret: A secret key to verify webhook requests
Important: Store thewebhook_secretsecurely. It will only be shown once.
There is no switch to pause forwarding. To stop receiving codes on your webhook, ask your Cardda account team to remove the setup for that card.
Webhook Payload
Cardda sends a POST request to your webhook with security headers:
POST https://yourapp.com/api/codes
Content-Type: application/json
X-Cardda-Signature: b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9
X-Cardda-Timestamp: 1644512345
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"from_phone": "+15551234567",
"to_phone": "+56912345678",
"body": "Tu codigo de verificacion es 123456",
"vendor_card_id": "12345",
"created_at": "2026-02-10T14:30:00.000Z"
}Security Headers:
X-Cardda-Signature: hex-encoded HMAC-SHA256 of<X-Cardda-Timestamp>.<raw_request_body>(literal Unix timestamp, dot, then the raw body bytes β not the parsed JSON). Use the sharedwebhook_secretCardda issued you when forwarding was set up.X-Cardda-Timestamp: Unix timestamp (seconds) when the request was sent. Reject deliveries older than 5 minutes to prevent replay attacks.
NoX-Cardda-Event-Idheader today. The PLH SMS webhook is the only Cardda webhook live in production right now, and it ships only the two headers above. The general-purposeX-Cardda-Event-Idheader described on Webhooks is part of a roadmap contract and is not sent today β for idempotency on PLH SMS, dedup on the payload'sidfield (see below). When the general-purpose webhook system ships, the header will become the preferred dedup key and your verifier should fall back topayload.idif it is absent.
Your webhook must:
- Verify the signature before processing
- Deduplicate on the payload's
id(the SMS's UUID) β Cardda retries failed deliveries with the sameid - Respond with
2xxwithin 10 seconds
Implementation
The following example shows how to receive and verify a webhook request using Python and Flask. It validates the timestamp to prevent replay attacks, verifies the HMAC signature to ensure the request comes from Cardda, deduplicates on the SMS's id (the canonical dedup key today β no X-Cardda-Event-Id is sent yet), and extracts the 6-digit verification code from the message body.
import hmac
import hashlib
import json
import time
import re
import os
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.post('/api/codes')
def receive_code():
signature = request.headers.get('X-Cardda-Signature')
timestamp = request.headers.get('X-Cardda-Timestamp')
secret = os.environ['CARDDA_WEBHOOK_SECRET']
raw_body = request.get_data(as_text=True)
# 1. Verify timestamp (prevent replay attacks)
try:
if abs(time.time() - int(timestamp)) > 300:
return jsonify({'error': 'Request too old'}), 401
except (ValueError, TypeError):
return jsonify({'error': 'Invalid timestamp'}), 401
# 2. Verify HMAC signature over `<timestamp>.<raw_body>` β NOT body alone.
signed_payload = f'{timestamp}.{raw_body}'
expected_signature = hmac.new(
secret.encode(), signed_payload.encode(), hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature or '', expected_signature):
return jsonify({'error': 'Invalid signature'}), 401
# 3. Parse the body and deduplicate on payload.id (the SMS UUID).
# No X-Cardda-Event-Id header is sent today β payload.id is the dedup key.
try:
body = json.loads(raw_body)
except ValueError:
return jsonify({'error': 'Invalid JSON'}), 400
sms_id = body.get('id')
if not sms_id:
return jsonify({'error': 'Missing payload id'}), 400
if not insert_if_absent(sms_id): # your dedup store
return jsonify({'status': 'duplicate'}), 200
# 4. Extract and process code
match = re.search(r'\d{6}', body.get('body', ''))
if not match:
return jsonify({'status': 'received'}), 200
# Add your logic here to handle the verification code
process_code(body['vendor_card_id'], match.group())
return jsonify({'status': 'received'}), 200
Always respond 200Respond with 200 OK after successful verification to acknowledge receipt.
Security Notes
- Always use HTTPS endpoints (
https://yourapp.com/api/codes) - Verify the HMAC signature on every request β sign over
<timestamp>.<raw_body>, not the body alone - Check timestamp to prevent replay attacks (< 5 minutes old)
- Deduplicate on
payload.idso a retried delivery does not re-issue the code (noX-Cardda-Event-Idis sent today) - Store your
webhook_secretsecurely (environment variables)
Troubleshooting
Not receiving codes?
- Confirm with your Cardda account team that forwarding is set up for the card
- Check your webhook is publicly accessible
- Ensure your webhook responds within 10 seconds
Updated about 13 hours ago
