Skip to main content
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. 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 request. This is useful for testing with ngrok or routing different workflows to different endpoints.
  • TypeScript
  • Python
const result = await client.workflows.run(workflowId, {
  webhook_url: '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:
{
  "success": true,                    // boolean indicating if the session completed successfully
  "agent_response": "string",         // text description of what the agent accomplished
  "session_id": "string",             // unique identifier for the session
  "agentic_steps": 0,                 // number of agentic actions taken during the session
  "browser_seconds": 0,               // duration of the session in seconds
  "session_store": {},                // all data stored in the session store during execution
  "metadata": {},                     // custom metadata you provided when starting the session
  "workflow_id": "string",            // workflow ID if this session was running a workflow
  "workflow_result": {},              // structured workflow result if available
  "screenshot": "base64_string"       // base64-encoded PNG screenshot of the final browser state
}

Testing Locally

  • TypeScript
  • Python

Testing with ngrok

When developing locally, you can use ngrok 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:
    ngrok http 3000
    
  4. Use the generated ngrok URL in your webhook configuration:
    const result = await client.workflows.run({
      workflow_id: "your-workflow-id",
      webhook_url: "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. 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

  • TypeScript SDK
  • Python SDK
  • Manual verification
The Simplex TypeScript SDK includes a built-in verifySimplexWebhook() function that handles signature verification for you.

Installation

npm install simplex-ts
  • Next.js App Router
  • Next.js Pages Router
// app/api/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { verifySimplexWebhook, WebhookVerificationError } 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 = JSON.parse(body);
    console.log('Session completed:', payload.session_id);

    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 }
    );
  }
}

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.
// ✅ 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.
// ✅ 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:
# .env
SIMPLEX_WEBHOOK_SECRET=your-webhook-secret-here

5. Test with ngrok

Use ngrok to test webhooks locally before deploying to production:
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
  • Flask: Use request.get_data(as_text=True) or request.body
  • FastAPI: Use await request.body()

Webhook not receiving requests

  1. Check that your webhook URL is correct in the Dashboard Settings
  2. Ensure your endpoint is publicly accessible (use ngrok for local testing)
  3. Verify your server is running and responding to POST requests
  4. Check your firewall settings

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
  • HTTP_X_SIMPLEX_SIGNATURE (Django/WSGI)

Need help?

If you have questions about webhooks or need help with integration, reach out to us on Slack or at support@simplex.sh.
I