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 Payment

Retries

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 the webhook_secret securely. 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 shared webhook_secret Cardda 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.
โš ๏ธ

No X-Cardda-Event-Id header 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-purpose X-Cardda-Event-Id header described on Webhooks is part of a roadmap contract and is not sent today โ€” for idempotency on PLH SMS, dedup on the payload's id field (see below). When the general-purpose webhook system ships, the header will become the preferred dedup key and your verifier should fall back to payload.id if 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 2xx within 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 200

Respond 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.id so a retried delivery does not re-issue the code (no X-Cardda-Event-Id is sent today)
  • Store your webhook_secret securely (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