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 retries up to 3 times with exponential backoff.
Setup
Contact Cardda support to enable this feature. 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.
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",
"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 the feature was enabled.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 may redeliver the same event after a deploy or network blip - 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?
- Verify the feature is enabled (contact support)
- Check your webhook is publicly accessible
- Ensure your webhook responds within 10 seconds
Updated 14 days ago
