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

# Webhooks

> Using webhooks with Simplex

Webhooks allow you to receive real-time notifications when workflow sessions complete. When a session finishes, Simplex sends a POST request to your configured webhook URL with the complete session results.

## Setting up webhooks

You can configure webhooks in two ways:

### Global webhook (Dashboard)

Set a global webhook URL for your organization on the [Dashboard Settings page](https://www.simplex.sh/settings). This webhook will receive notifications for all workflow sessions in your organization.

### Per-request webhook (API)

When running a workflow via the API, you can specify a custom webhook URL in the [Run Workflow](/api-reference/run-workflow) request. This is useful for testing with ngrok or routing different workflows to different endpoints.

```typescript theme={null}
const result = await client.workflows.run(workflowId, {
  webhookUrl: 'https://your-domain.com/webhook'
});
```

## Webhook payload structure

When a session completes, Simplex will send a POST request to your webhook URL with the following payload:

<ResponseField name="workflow_id" type="string">
  Workflow ID of the session
</ResponseField>

<ResponseField name="session_id" type="string" required>
  Session ID of the session
</ResponseField>

<ResponseField name="workflow_metadata" type="object">
  Metadata set in the workflow definition
</ResponseField>

<ResponseField name="session_metadata" type="object">
  Custom metadata you provided when starting the session
</ResponseField>

<ResponseField name="success" type="boolean" required>
  Indicates if the session completed successfully
</ResponseField>

<ResponseField name="final_message" type="string">
  Text description of what the agent accomplished
</ResponseField>

<ResponseField name="structured_output" type="object">
  Structured output fields set during workflow execution
</ResponseField>

<ResponseField name="screenshot_url" type="string">
  Presigned URL to download a screenshot of the final browser state
</ResponseField>

<ResponseField name="scraper_outputs" type="array">
  Outputs of any scrapers ran during workflow execution
</ResponseField>

<ResponseField name="file_metadata" type="array">
  Metadata for files downloaded during the session

  <Expandable title="properties">
    <ResponseField name="filename" type="string">
      Name of the downloaded file
    </ResponseField>

    <ResponseField name="download_url" type="string">
      Presigned URL to download the file
    </ResponseField>

    <ResponseField name="file_size" type="number">
      Size of the file in bytes
    </ResponseField>

    <ResponseField name="download_timestamp" type="string">
      ISO 8601 timestamp when the file was downloaded
    </ResponseField>
  </Expandable>
</ResponseField>

### Structured Output

The `structured_output` field contains custom data fields that can be defined in [your workflow configuration](/core-concepts/structured-outputs). When you define structured output fields in your workflow, the agent will extract and return these specific pieces of information in addition to the standard response.

For example, if your workflow defines structured output fields for extracting form submission results:

```json theme={null}
"structured_output": {
  "authorization_number": "AUTH-2024-78901",
  "status": "APPROVED",
  "effective_date": "2024-06-01",
  "expiration_date": "2024-12-01"
}
```

This allows you to programmatically access specific data extracted during the workflow execution without having to parse the `agent_response` text. The fields available in `structured_output` depend on what you've defined in your workflow configuration.

## Testing Locally

### Testing with ngrok

When developing locally, you can use [ngrok](https://ngrok.com/) to create a public URL that forwards to your local development server:

1. Install ngrok if you haven't already
2. Start your local webhook server (e.g., on port 3000)
3. Run ngrok to expose your local server:
   ```bash theme={null}
   ngrok http 3000
   ```
4. Use the generated ngrok URL in your webhook configuration:
   ```typescript theme={null}
   const result = await client.workflows.run("your-workflow-id", {
     webhookUrl: "https://abc123.ngrok.io/api/webhook"
   });
   ```

Your local server will now receive webhook notifications when the workflow completes.

## Webhook security

Simplex signs all webhook requests using **HMAC-SHA256** to ensure authenticity. You should always verify the signature before processing webhook data.

### Finding your webhook secret

Your webhook secret is available on the [Dashboard Settings page](https://www.simplex.sh/settings). Keep this secret secure and never commit it to version control.

### How signing works

Each webhook request includes an `X-Simplex-Signature` header containing an HMAC-SHA256 signature of the request body:

```
X-Simplex-Signature: 5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
```

The signature is computed as:

```
HMAC-SHA256(webhook_secret, request_body)
```

## Verifying webhooks

<Tabs>
  <Tab title="TypeScript SDK">
    The Simplex TypeScript SDK includes a built-in `verifySimplexWebhook()` function that handles signature verification for you.

    ### Installation

    ```bash theme={null}
    npm install simplex-ts
    ```

    <Tabs>
      <Tab title="Next.js App Router">
        ```typescript theme={null}
        // app/api/webhook/route.ts
        import { NextRequest, NextResponse } from 'next/server';
        import { verifySimplexWebhook, WebhookVerificationError, WebhookPayload } from 'simplex-ts';

        export async function POST(request: NextRequest) {
          const webhookSecret = process.env.SIMPLEX_WEBHOOK_SECRET!;

          try {
            // Get raw body as text
            const body = await request.text();

            // Convert headers to plain object
            const headers: Record<string, string> = {};
            request.headers.forEach((value, key) => {
              headers[key] = value;
            });

            // Verify signature
            verifySimplexWebhook(body, headers, webhookSecret);

            // ✅ Verified! Parse and process
            const payload: WebhookPayload = JSON.parse(body);
            console.log('Session completed:', payload.session_id);

            // Access structured output if present
            if (payload.structured_output) {
              console.log('Structured data:', payload.structured_output);
              // Process custom fields from workflow
            }

            return NextResponse.json({ received: true });
          } catch (error) {
            if (error instanceof WebhookVerificationError) {
              return NextResponse.json(
                { error: 'Invalid signature' },
                { status: 401 }
              );
            }
            return NextResponse.json(
              { error: 'Internal server error' },
              { status: 500 }
            );
          }
        }
        ```
      </Tab>

      <Tab title="Next.js Pages Router">
        ```typescript theme={null}
        // pages/api/webhook.ts
        import type { NextApiRequest, NextApiResponse } from 'next';
        import { verifySimplexWebhook, WebhookVerificationError, WebhookPayload } from 'simplex-ts';

        // Disable Next.js body parsing
        export const config = {
          api: {
            bodyParser: false,
          },
        };

        // Helper to read raw body
        async function getRawBody(req: NextApiRequest): Promise<string> {
          const chunks: Buffer[] = [];
          return new Promise((resolve, reject) => {
            req.on('data', (chunk: Buffer) => chunks.push(chunk));
            req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
            req.on('error', reject);
          });
        }

        export default async function handler(
          req: NextApiRequest,
          res: NextApiResponse
        ) {
          if (req.method !== 'POST') {
            return res.status(405).json({ error: 'Method not allowed' });
          }

          const webhookSecret = process.env.SIMPLEX_WEBHOOK_SECRET!;

          try {
            const body = await getRawBody(req);

            verifySimplexWebhook(body, req.headers, webhookSecret);

            const payload: WebhookPayload = JSON.parse(body);
            console.log('Session completed:', payload.session_id);

            // Access structured output if present
            if (payload.structured_output) {
              console.log('Structured data:', payload.structured_output);
              // Process custom fields from workflow
            }

            res.json({ received: true });
          } catch (error) {
            if (error instanceof WebhookVerificationError) {
              return res.status(401).json({ error: 'Invalid signature' });
            }
            res.status(500).json({ error: 'Internal server error' });
          }
        }
        ```
      </Tab>
    </Tabs>
  </Tab>

  <Tab title="Manual verification">
    If you're not using our SDKs, you can manually verify webhook signatures:

    ### TypeScript/Node.js

    ```typescript theme={null}
    import crypto from 'crypto';

    function verifyWebhook(body: string, signature: string, secret: string): boolean {
      const expectedSignature = crypto
        .createHmac('sha256', secret)
        .update(body, 'utf8')
        .digest('hex');

      // Use constant-time comparison to prevent timing attacks
      return crypto.timingSafeEqual(
        Buffer.from(expectedSignature),
        Buffer.from(signature)
      );
    }

    // Usage
    app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
      const body = req.body.toString('utf8');
      const signature = req.headers['x-simplex-signature'];
      const secret = process.env.SIMPLEX_WEBHOOK_SECRET;

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

      // Process webhook...
      const payload = JSON.parse(body);
      res.json({ received: true });
    });
    ```

    ### Important security notes

    1. **Always use the raw request body** for signature verification (don't parse it as JSON first)
    2. **Use constant-time comparison** functions to prevent timing attacks (`crypto.timingSafeEqual` in Node.js)
    3. **Never log or expose** your webhook secret
    4. **Always use HTTPS** for your webhook endpoints
    5. **Respond quickly** to acknowledge receipt
  </Tab>
</Tabs>

## Best practices

### 1. Always verify signatures

Never process webhook data without first verifying the signature. This ensures the request actually came from Simplex and hasn't been tampered with.

```typescript theme={null}
// ✅ Good - verify first
verifySimplexWebhook(body, headers, secret);
const payload = JSON.parse(body);

// ❌ Bad - processing without verification
const payload = JSON.parse(body);
processWebhook(payload);
```

### 2. Respond quickly

Your webhook endpoint should acknowledge receipt within 30 seconds to prevent retries from the Simplex server. If you need to perform long-running operations (like processing large files or making external API calls), respond with a `200 OK` immediately and process the data asynchronously.

```typescript theme={null}
// ✅ Good - respond quickly, process later
app.post('/webhook', async (req, res) => {
  verifyWebhook(/* ... */);

  // Respond immediately
  res.json({ received: true });

  // Process asynchronously (e.g., using a job queue)
  processWebhookAsync(payload);
});

// ❌ Bad - long processing blocks response
app.post('/webhook', async (req, res) => {
  verifyWebhook(/* ... */);
  await longRunningTask(payload);  // Don't make Simplex wait!
  res.json({ received: true });
});
```

### 3. Handle errors gracefully

Return appropriate HTTP status codes:

* `200` - Webhook received and verified successfully
* `401` - Signature verification failed
* `500` - Server error

### 4. Use environment variables

Store your webhook secret in environment variables, never in code:

```bash theme={null}
# .env
SIMPLEX_WEBHOOK_SECRET=your-webhook-secret-here
```

### 5. Test with ngrok

Use ngrok to test webhooks locally before deploying to production:

```bash theme={null}
ngrok http 3000
```

Then update your webhook URL in the Simplex dashboard to the ngrok URL.

## Troubleshooting

### "Invalid signature" errors

**Cause**: The most common cause is parsing the request body as JSON before verification.

**Solution**: Always use the raw request body:

* Express: Use `express.raw({ type: 'application/json' })`
* Next.js: Disable `bodyParser` in API route config

### Webhook not receiving requests

1. Check that your webhook URL is correct in the [Dashboard Settings](https://www.simplex.sh/settings)
2. Ensure your endpoint is publicly accessible (use ngrok for local testing)
3. Verify your server is running and responding to POST requests

### Missing X-Simplex-Signature header

Ensure you're reading headers correctly. Header names are case-insensitive in HTTP, but some frameworks may normalize them:

* `X-Simplex-Signature`
* `x-simplex-signature`

## Need help?

If you have questions about webhooks or need help with integration, reach out to us on Slack or at [support@simplex.sh](mailto:support@simplex.sh).
