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

# Webhooks setup & configuration

> Configure FunnelFox webhooks to receive real-time event notifications for purchases, subscriptions, and funnel interactions.

Webhooks allow your application to receive real-time notifications when specific
events occur in FunnelFox. Instead of polling for changes, webhooks push data
to your endpoint as events happen, enabling instant reactions to user actions,
purchases, and [subscription changes](/docs/develop/webhooks#subscription-access-via-webhooks).

## How Webhooks Work

When an event occurs in your funnel, FunnelFox sends an HTTP POST request to
your configured endpoint with a JSON payload containing event details.

```mermaid theme={null}
sequenceDiagram
    participant User
    participant FunnelFox
    participant Your Server
    
    User->>FunnelFox: Completes funnel
    FunnelFox->>Your Server: POST webhook event
    Your Server->>Your Server: Process event
    Your Server->>FunnelFox: 200 OK
```

## Quick Start

<Steps>
  <Step title="Configure Endpoint">
    Add your webhook URL in [Project Settings](/docs/dashboard/settings#webhooks):

    * **Production URL**: For live events
    * **Sandbox URL**: For test events (optional)
  </Step>

  <Step title="Verify Requests">
    Validate the `Fox-Secret-Key` header matches your project's secret key
  </Step>

  <Step title="Handle Events">
    Process the JSON payload and respond with 2xx status within 10 seconds
  </Step>

  <Step title="Implement Retries">
    Handle potential duplicate events and implement idempotency
  </Step>
</Steps>

## Event Types

FunnelFox sends webhooks for these events:

<CardGroup cols={2}>
  <Card title="Funnel Events" icon="route">
    * `onboarding.started` - User begins funnel
    * `onboarding.completed` - User reaches finish screen
    * `profile.updated` - Email captured or updated
  </Card>

  <Card title="Payment Events" icon="credit-card">
    * `purchase.completed` -  User has completed a purchase or when recurrent/upsell purchase happen
  </Card>

  <Card title="Subscription Events" icon="rotate">
    * `subscription.created` - New subscription
    * `subscription.trialing` - Trial period started
    * `subscription.active` - Subscription activated
    * `subscription.cycle` - Renewal processed
    * `subscription.paused` - Billing paused
    * `subscription.resumed` - Billing resumed
    * `subscription.cancelled` - Subscription cancelled
    * `subscription.auto_renew_off` - Auto-renewal turned off
    * `subscription.unpaid` - Payment failed
  </Card>
</CardGroup>

<Tip>Use `subscription.` webhooks to grant new customers [subscription access](/docs/develop/webhooks#subscription-access-via-webhooks).</Tip>

Go to the [Events Reference](/docs/api-reference/webhook-reference/onboardingstarted) for complete payload schemas.

## Security & Verification

Every webhook request includes a `Fox-Secret-Key` header containing your project's
secret key. Always verify this header to ensure requests are from FunnelFox.

```javascript theme={null}
// Express.js example
app.post('/webhook', (req, res) => {
  const secret = req.headers['fox-secret-key'];
  
  if (secret !== process.env.FUNNELFOX_SECRET) {
    return res.status(401).send('Unauthorized');
  }
  
  // Process webhook
  const event = req.body;
  console.log(`Received ${event.type} event`);
  
  res.status(200).send('OK');
});
```

<Warning>
  Never expose your Fox-Secret-Key in client-side code or public
  repositories. Store it securely in environment variables.
</Warning>

## Payload Structure

All webhook events follow this base structure:

```json theme={null}
{
  "id": "evt_00000000000000000000000000",
  "type": "onboarding.completed",
  "created_at": 1704067200,
  "project_id": "00000000000000000000000000",
  "is_sandbox": false,
  "session_id": "ses_00000000000000000000000000",
  "location": {
    "funnel_id": "00000000000000000000000000",
    "funnel_version": 3,
    "origin": "https://example.fnlfx.com/welcome",
    "query": "?utm_source=google",
    "query_params": {
      "utm_source": "google"
    },
    "sandbox": false,
    "experiment_id": "exp_123",
    "experiment_title": "Pricing Test",
    "experiment_alias": "pricing-test"
  },
  "profile": {
    "id": "pro_00000000000000000000000000",
    "session_id": "ses_00000000000000000000000000",
    "country": "US",
    "ip": "192.0.2.1",
    "city": "New York",
    "time_zone": "America/New_York",
    "user_agent": "Mozilla/5.0...",
    "locale": "🇺🇸 English",
    "locale_code": "en_US"
  },
  "data": {
    // Event-specific data
  }
}
```

### Key Fields

| Field        | Type    | Description                             |
| ------------ | ------- | --------------------------------------- |
| `id`         | string  | Unique event identifier                 |
| `type`       | string  | Event type (e.g., "purchase.completed") |
| `created_at` | integer | Unix timestamp in seconds               |
| `is_sandbox` | boolean | Whether this is a test event            |
| `location`   | object  | Funnel and experiment context           |
| `profile`    | object  | User information and geo data           |
| `data`       | object  | Event-specific payload                  |

## Matching User Profiles

Webhook events carry the same profile and session identifiers that your funnel
exposes as [funnel variables](/docs/editor/variables#system-variables). Match them to
tie an event back to a specific visitor or visit you saw in the funnel.

| Funnel variable       | Webhook field                          | Identifies                     |
| --------------------- | -------------------------------------- | ------------------------------ |
| `{{user.id}}`         | `profile.id`                           | The profile — a unique visitor |
| `{{user.session_id}}` | `session_id` (or `profile.session_id`) | The session — a single visit   |

<Warning>
  Webhook fields and funnel variables return the same identifier in
  different forms. Webhook payloads prefix the value (`profile.id` is
  `pro_...`, `session_id` is `ses_...`), while funnel variables return the bare
  value with no prefix. A direct string comparison fails silently — no error is
  raised, the IDs simply never match.

  ```text theme={null}
  Funnel variable  {{user.id}}   =  01ka0r5w37tsg0rnnjct1km7rr
  Webhook field    profile.id    =  pro_01ka0r5w37tsg0rnnjct1km7rr
  ```
</Warning>

To match reliably, normalize one side before you compare. Strip the prefix from
the webhook value, or add it to the value you stored from the funnel:

```javascript theme={null}
// Value captured in the funnel from {{user.id}}
const funnelUserId = '01ka0r5w37tsg0rnnjct1km7rr';

// Strip the prefix from the webhook value before comparing
const webhookProfileId = event.profile.id.replace(/^pro_/, '');

if (webhookProfileId === funnelUserId) {
  // Same profile
}
```

<Info>
  When you look a profile or session up with the [FunnelFox API](/docs/develop/api), you can pass the ID with or
  without the prefix (the API accepts both forms).
</Info>

## Handling Webhooks

### Response Requirements

Your endpoint must:

* Return a **2xx status code** (200-299)
* Respond within **10 seconds**
* Accept `application/json` content type

```python theme={null}
# Python Flask example
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def handle_webhook():
    # Verify secret
    if request.headers.get('Fox-Secret-Key') != FUNNELFOX_SECRET:
        return jsonify({'error': 'Unauthorized'}), 401
    
    event = request.json
    
    # Process asynchronously (recommended)
    queue_for_processing(event)
    
    # Respond immediately
    return jsonify({'status': 'received'}), 200
```

### Idempotency

Events may be delivered multiple times. Use the event `id` to ensure
idempotent processing:

```javascript theme={null}
const processedEvents = new Set();

function handleWebhook(event) {
  if (processedEvents.has(event.id)) {
    console.log(`Duplicate event ${event.id}, skipping`);
    return;
  }
  
  processedEvents.add(event.id);
  // Process event...
}
```

## Retry Logic

FunnelFox implements exponential backoff with up to **30 retry attempts** for
failed webhook deliveries.

### Retry Schedule

The delay between retries follows this formula:

```
delay(n) = n^4 + 15 + random(0-29) × (n+1) seconds
```

This results in approximately:

* Attempt 1: \~15-45 seconds
* Attempt 2: \~30-90 seconds
* Attempt 3: \~1-3 minutes
* Attempt 10: \~3-5 hours
* Attempt 30: \~10-12 days

<Info>
  Failed webhooks are retried for up to 2 weeks. Ensure your endpoint can
  handle events that arrive significantly after they occurred.
</Info>

## Testing Webhooks

### Local Development

Use tools like ngrok to expose your local server:

```bash theme={null}
# Install ngrok
npm install -g ngrok

# Expose local port 3000
ngrok http 3000

# Use the HTTPS URL in FunnelFox settings
# https://abc123.ngrok.io/webhook
```

### Webhook Testing Tools

Popular webhook testing services:

* [Webhookah](https://webhookah.com/) - Test your webhooks in real-time
* [Webhook.site](https://webhook.site) - Instant test endpoints
* [RequestBin](https://requestbin.com) - Inspect webhook payloads
* [Hookdeck](https://hookdeck.com) - Development webhook infrastructure

## Subscription Access via Webhooks

You can grant new customers subscription access using `subscription.` webhooks such as [subscription.trialing](/docs/api-reference/webhook-reference/subscriptiontrialing) or [subscription.active](/docs/api-reference/webhook-reference/subscriptionactive).

These webhooks are optimized for reliability and have the lowest delivery delay, making them ideal for managing subscription access in real time.

<Warning> Avoid using `purchase.completed` for this purpose. It’s optimized for tracking purchases rather than managing subscription states.</Warning>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Webhooks not being received">
    * Verify your endpoint URL is publicly accessible (not localhost)
    * Check the Fox-Secret-Key header is being validated correctly
    * Ensure your server responds within 10 seconds
    * Confirm webhooks are configured in Project Settings
  </Accordion>

  <Accordion title="Receiving duplicate events">
    This is expected behavior due to retry logic. Implement idempotency using
    the event `id` field to handle duplicates gracefully.
  </Accordion>

  <Accordion title="Events arriving out of order">
    Network delays and retries can cause events to arrive out of sequence.
    Use the `created_at` timestamp to determine actual event order.
  </Accordion>

  <Accordion title="Missing data in payload">
    Some fields are only present for specific event types. Check the
    [Events Reference](/docs/api-reference/webhook-reference/onboardingstarted) for complete schemas. The `data` field varies per event.
  </Accordion>

  <Accordion title="Sandbox vs Production events">
    Check the `is_sandbox` field to differentiate test events. Configure
    separate webhook URLs for sandbox and production in Project Settings.
  </Accordion>
</AccordionGroup>

## Rate Limits

FunnelFox doesn't impose rate limits on webhook deliveries. However:

* Events are sent as they occur (no batching)
* High-volume funnels may generate many simultaneous requests
* Ensure your endpoint can handle your expected traffic

## Next Steps

* [Webhook Reference](/docs/api-reference/webhook-reference/onboardingstarted) - Detailed event schemas
* [Project Settings](/docs/dashboard/settings#webhooks) - Configure webhook endpoints
