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

# Handling Webhook Deliveries

> End-to-end guide to receiving, verifying, and processing AutosnapAI webhooks

This guide walks you through everything you need to set up a production-grade webhook receiver — creating a webhook, verifying signatures, processing events idempotently, and handling failures.

## Step 1 — Build a receiver endpoint

Your endpoint needs to:

1. Accept POST requests with a JSON body
2. Read the `X-Autosnap-Signature` header
3. Verify the signature using the `secret` you provided when creating the webhook
4. Respond with `2xx` within **10 seconds**
5. Process events **idempotently** (the same event may arrive more than once)

### Example: FastAPI

```python theme={null}
import hmac, hashlib, os
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks

app = FastAPI()
WEBHOOK_SECRET = os.environ["AUTOSNAP_WEBHOOK_SECRET"]

@app.post("/webhooks/autosnap")
async def receive(request: Request, background_tasks: BackgroundTasks):
    raw_body = await request.body()

    # 1. Verify signature
    signature = request.headers.get("X-Autosnap-Signature", "")
    if not verify_signature(WEBHOOK_SECRET, signature, raw_body):
        raise HTTPException(status_code=401, detail="Invalid signature")

    # 2. Parse the event
    event = await request.json()

    # 3. Hand off to background processing — return 2xx fast
    background_tasks.add_task(process_event, event)

    return {"received": True}


def verify_signature(secret: str, signature_header: str, body: bytes) -> bool:
    expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature_header)


async def process_event(event: dict):
    event_type = event["event"]
    data = event["data"]

    if event_type == "vehicle.created":
        await handle_vehicle_created(data)
    elif event_type == "vehicle.updated":
        await handle_vehicle_updated(data)
    elif event_type == "vehicle.removed":
        await handle_vehicle_removed(data)
    elif event_type == "import.complete":
        await handle_import_complete(event["dealership_id"], data)
    elif event_type == "setup.complete":
        await handle_setup_complete(data)
    elif event_type == "setup.failed":
        await handle_setup_failed(data)
    else:
        # Unknown event type — log and move on
        print(f"Unknown webhook event: {event_type}")
```

<Note>
  For vehicle events (`vehicle.created`, `vehicle.updated`, `vehicle.removed`), the `data` field contains the vehicle object directly. For `import.complete`, `data` contains `website_url` and `stats`. The `dealership_id` is always at the top level of the payload, not inside `data`.
</Note>

### Example: Node/Express

```javascript theme={null}
const express = require('express');
const crypto = require('crypto');

const app = express();
const WEBHOOK_SECRET = process.env.AUTOSNAP_WEBHOOK_SECRET;

// Important: capture raw body for signature verification
app.post(
  '/webhooks/autosnap',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    const signature = req.header('X-Autosnap-Signature');

    if (!verifySignature(WEBHOOK_SECRET, signature, req.body)) {
      return res.status(401).json({ error: 'Invalid signature' });
    }

    const event = JSON.parse(req.body.toString());

    // Hand off to background queue, return 2xx fast
    queue.add('process-autosnap-event', { event });

    res.json({ received: true });
  }
);

function verifySignature(secret, signatureHeader, rawBody) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signatureHeader)
  );
}
```

## Step 2 — Create the webhook

Once your endpoint is live and reachable:

```bash theme={null}
curl -X POST "https://api.autosnap.com/v1/webhooks" \
  -H "Content-Type: application/json" \
  -d '{
    "api_key": "'$AUTOSNAP_API_KEY'",
    "url": "https://yourapp.com/webhooks/autosnap",
    "event_types": ["vehicle.created", "vehicle.updated", "vehicle.removed"],
    "secret": "your_secret_at_least_16_chars",
    "dealership_id": "dlr_8cfc0b00a98b"
  }'
```

The response confirms which webhooks were created:

```json theme={null}
{
  "success": true,
  "created": [
    { "webhook_id": 42, "event_type": "vehicle.created" },
    { "webhook_id": 43, "event_type": "vehicle.updated" },
    { "webhook_id": 44, "event_type": "vehicle.removed" }
  ],
  "skipped": [],
  "url": "https://yourapp.com/webhooks/autosnap",
  "dealership_id": "dlr_8cfc0b00a98b"
}
```

## Step 3 — Test the webhook

Send a test delivery from the dashboard or programmatically by triggering an inventory refresh on one of the dealerships. Inspect the delivery results:

```bash theme={null}
curl "https://api.autosnap.com/v1/webhooks/42/deliveries?api_key=$AUTOSNAP_API_KEY&limit=50"
```

Returns delivery attempts (default 50, max 100) with status codes, response times, and error messages.

## Idempotency is critical

Webhooks are delivered **at-least-once**, not exactly-once. Your handler must produce the same end state regardless of how many times it sees the same event. A simple pattern:

1. Use a combination of event type + VIN + timestamp as a deduplication key
2. At the start of handling, check if you've already processed this event
3. If yes, return early. If no, process it.

## Always respond fast

The receiver endpoint must return `2xx` within **10 seconds**. If you do heavy work synchronously (writing to your database, calling other APIs, sending emails), you'll time out and we'll retry — flooding your endpoint with duplicates.

The pattern:

1. **Verify the signature** (\~1ms)
2. **Reject if invalid** (\~1ms)
3. **Parse the JSON** (\~1ms)
4. **Push to a background queue** (\~10ms)
5. **Return 2xx**

Background workers do the actual data writes, calls to other services, etc.

## Retry behavior

Failed deliveries are retried with exponential backoff. Each webhook delivery gets up to **3 total attempts**:

| Attempt | Delay after previous         |
| ------- | ---------------------------- |
| 1       | Immediate (initial delivery) |
| 2       | \~4 seconds                  |
| 3       | \~8 seconds                  |

Only **server errors (5xx)** trigger retries. Client errors (4xx) are not retried — fix your endpoint and the next event will succeed.

After reaching the configured failure threshold (default: **10 consecutive failures**) across all deliveries (not just retries for one event), the webhook subscription is automatically deactivated. Use `PATCH /v1/webhooks/{id}/reactivate` to re-enable it after fixing the issue — this resets the failure count without losing your subscription configuration.

## Security best practices

<Steps>
  <Step title="Always verify signatures">
    Never act on webhook payloads without first verifying the HMAC signature. Anyone can POST to a public URL.
  </Step>

  <Step title="Use HTTPS">
    Webhook URLs must be `https://`. Plain HTTP is rejected at webhook creation time.
  </Step>

  <Step title="Use a unique URL path">
    Use a unique, hard-to-guess path for your webhook endpoint (e.g. `/webhooks/autosnap-a3f2c8b4`) so it's not easily discoverable.
  </Step>

  <Step title="Change secrets after a leak">
    If your webhook `secret` is exposed, delete the webhook and create a new one with a new secret.
  </Step>
</Steps>

## Common pitfalls

| Mistake                                 | Fix                                                         |
| --------------------------------------- | ----------------------------------------------------------- |
| Parsing JSON before verifying signature | Verify on the **raw bytes** first, then parse               |
| Doing database writes synchronously     | Push to a background queue, return 2xx immediately          |
| Not handling duplicate deliveries       | Add an idempotency check                                    |
| Crashing on unknown event types         | Log and skip — new event types may be added without warning |
| Returning 200 for invalid signatures    | Return 401 — we'll stop retrying after a few failures       |

## Related

<CardGroup cols={2}>
  <Card title="Webhooks concept" icon="webhook" href="/concepts/webhooks">
    Event types, payload format, signature verification
  </Card>

  <Card title="Webhook API endpoints" icon="code" href="/api-reference/endpoints/create-webhook">
    Full endpoint reference
  </Card>
</CardGroup>
