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 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 the webhook_secret securely. 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 shared webhook_secret Cardda 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.
⚠️

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 retries failed deliveries with the same id
  • 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?

  • 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

Did this page help you?