> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fastcac.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Ready-made handlers

> Complete webhook receivers for Express, Laravel and Django.

Each handler:

1. verifies the signature with the open-source `svix` library, which also rejects timestamps more
   than five minutes off;
2. parses the raw body itself, because `verify` returns nothing;
3. skips a `webhook-id` it has already handled;
4. handles the event, or queues it, and only then records the id;
5. answers `204`, or `500` if handling failed so the event is retried.

Set `FASTCAC_WEBHOOK_SECRET` to your endpoint's `whsec_…` secret.

## Express

Mount it with `express.raw` so the body stays unparsed. `npm install express svix`

```javascript server.js theme={null}
import express from "express";
import { Webhook } from "svix";

const wh = new Webhook(process.env.FASTCAC_WEBHOOK_SECRET);
const handled = new Set(); // use your database in production, with a unique index on the id
const app = express();

app.post("/fastcac/webhooks", express.raw({ type: "application/json" }), async (req, res) => {
  try {
    wh.verify(req.body, req.headers); // throws if the signature or timestamp is wrong
  } catch {
    return res.status(400).send("invalid signature");
  }
  const event = JSON.parse(req.body);

  const id = req.headers["webhook-id"];
  if (handled.has(id)) return res.sendStatus(204); // already handled

  try {
    await handle(event); // keep it quick, or push to your job queue here
  } catch (err) {
    console.error(err);
    return res.sendStatus(500); // not recorded, so the retry runs it again
  }
  handled.add(id);
  res.sendStatus(204);
});

async function handle(event) {
  const { data } = event;
  switch (event.type) {
    case "registration.approved":
      // Fetch the registration for its registration_number; wait for certificate_ready for PDFs.
      break;
    case "registration.queried":
      // Fetch the registration, show the customer its `query`, then resubmit.
      break;
    case "registration.rejected":
      // CAC refused it. Tell the customer.
      break;
    case "registration.verification_failed":
      // Fetch the registration, fix the fields in `failure.fields`, resubmit at no charge.
      break;
    case "document.rejected":
      // Ask for a new upload; data.reason says why.
      break;
    case "balance.low":
      // data.available fell below data.low_balance_threshold. Top up.
      break;
    case "balance.funded":
      // data.amount was credited; data.available is the new balance.
      break;
    default:
      // Ignore types you don't handle.
  }
}

app.listen(3000);
```

## Laravel

Add the route in `routes/api.php` (run `php artisan install:api` first on Laravel 11+), which skips
CSRF checks. `composer require svix/svix`

```php app/Http/Controllers/FastcacWebhookController.php theme={null}
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Svix\Webhook;

class FastcacWebhookController extends Controller
{
    public function __invoke(Request $request)
    {
        $headers = [
            'webhook-id' => $request->header('webhook-id'),
            'webhook-timestamp' => $request->header('webhook-timestamp'),
            'webhook-signature' => $request->header('webhook-signature'),
        ];

        try {
            // Throws if the signature or timestamp is wrong; returns nothing.
            // config/services.php: 'fastcac' => ['webhook_secret' => env('FASTCAC_WEBHOOK_SECRET')]
            $secret = config('services.fastcac.webhook_secret');
            (new Webhook($secret))->verify($request->getContent(), $headers);
        } catch (\Exception $e) {
            return response('invalid signature', 400);
        }
        $event = json_decode($request->getContent(), true);

        // Use a shared cache store (Redis, database).
        $key = 'fastcac-webhook:'.$headers['webhook-id'];
        if (Cache::has($key)) {
            return response()->noContent(); // already handled
        }

        $this->handle($event); // keep it quick, or dispatch a job here
        // An exception above answers 500 and isn't recorded, so the retry runs it again.
        Cache::put($key, true, now()->addDays(3));
        return response()->noContent();
    }

    private function handle(array $event): void
    {
        $data = $event['data'];
        switch ($event['type']) {
            case 'registration.approved':
                // Fetch the registration for its registration_number; wait for certificate_ready for PDFs.
                break;
            case 'registration.queried':
                // Fetch the registration, show the customer its `query`, then resubmit.
                break;
            case 'registration.rejected':
                // CAC refused it. Tell the customer.
                break;
            case 'registration.verification_failed':
                // Fetch the registration, fix the fields in `failure.fields`, resubmit at no charge.
                break;
            case 'document.rejected':
                // Ask for a new upload; $data['reason'] says why.
                break;
            case 'balance.low':
                // $data['available'] fell below $data['low_balance_threshold']. Top up.
                break;
            case 'balance.funded':
                // $data['amount'] was credited; $data['available'] is the new balance.
                break;
            default:
                // Ignore types you don't handle.
        }
    }
}
```

```php routes/api.php theme={null}
use App\Http\Controllers\FastcacWebhookController;

Route::post('/fastcac/webhooks', FastcacWebhookController::class);
```

## Django

A plain view, exempt from CSRF. `pip install svix`

```python webhooks/views.py theme={null}
import json
import os

from django.core.cache import cache
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from svix.webhooks import Webhook, WebhookVerificationError

wh = Webhook(os.environ["FASTCAC_WEBHOOK_SECRET"])


@csrf_exempt
@require_POST
def fastcac_webhook(request):
    try:
        wh.verify(request.body, dict(request.headers))  # raises if the signature or timestamp is wrong
    except WebhookVerificationError:
        return HttpResponse("invalid signature", status=400)
    event = json.loads(request.body)

    # Use a shared cache (Redis, database).
    key = f"fastcac-webhook:{request.headers['webhook-id']}"
    if cache.get(key):
        return HttpResponse(status=204)  # already handled

    handle(event)  # keep it quick, or hand it to Celery or RQ here
    # An exception above answers 500 and isn't recorded, so the retry runs it again.
    cache.set(key, True, timeout=3 * 24 * 3600)
    return HttpResponse(status=204)


def handle(event):
    data = event["data"]
    match event["type"]:
        case "registration.approved":
            pass  # Fetch the registration for its registration_number; wait for certificate_ready for PDFs.
        case "registration.queried":
            pass  # Fetch the registration, show the customer its `query`, then resubmit.
        case "registration.rejected":
            pass  # CAC refused it. Tell the customer.
        case "registration.verification_failed":
            pass  # Fetch the registration, fix the fields in `failure.fields`, resubmit at no charge.
        case "document.rejected":
            pass  # Ask for a new upload; data["reason"] says why.
        case "balance.low":
            pass  # data["available"] fell below data["low_balance_threshold"]. Top up.
        case "balance.funded":
            pass  # data["amount"] was credited; data["available"] is the new balance.
        case _:
            pass  # Ignore types you don't handle.
```

```python urls.py theme={null}
from django.urls import path

from webhooks.views import fastcac_webhook

urlpatterns = [path("fastcac/webhooks", fastcac_webhook)]
```

Send a [test event](https://docs.fastcac.com/webhooks/overview.md#add-an-endpoint) to check your handler end to end.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.