PrompteryxBeta
    ⌘K
    PlatformSupport
    Menu
    IntroductionInstall Desktop & extensionQuick StartPlatform Overview
    OverviewWhat you can makeAI Browser Agent in the chatModels and AutoConnectorsSchedulesProjectsExplore and publishingShare linksMemory and SkillsPersonal agent (beta)Welcome, settings and skinsPlans, credits and top-upsPrompteryx DesktopGroups (pilot)Phone app (early access)
    OverviewCreating WorkflowsTesting & DebuggingScheduling
    OverviewGetting StartedCommandsRecordingSettingsStructured OutputParallel AgentsTips & Best Practices
    OverviewInstallationSidebar AgentUsing Attached FilesExecute on Current TabExporting to Visual Studio
    Overview
    OverviewOpen PageClickClick CoordinatesFillExtract TextScreenshotWaitPress KeyFocus ElementBlur ElementSelect TextHighlight ElementSet ViewportGet Bounding BoxScrollHoverSelect OptionAssertionsCookiesEvaluate JavaScriptExample Workflows
    OverviewPrompteryx Browser AgentOpenAI
    OverviewSchedule TriggerWebhook Trigger
    OverviewGoogle SheetsGoogle CalendarConnect Hub (3,000+ apps)Example Workflows
    AI Vision SettingsChrome Automation ProfilesCloud BrowserBrowser Profiles & Persistent ContextVideo RecordingExecution LogsSharing & CollaborationEdit History
    Selectors GuideVariables
    Start hereGet an API keyRun the AI browser agentDrive a cloud browserRun workflows from anywhereUse connected apps (Connect Hub)SDK (TypeScript)CLIMCP for AI assistants
    REST APIWebhooksMCP toolsCloud Browser APICloud Browser quickstartMigrate from Browserbasen8nZapierMake.com

    Need help? Contact Support

    1. Documentation
    2. Developers
    3. Webhooks

    Webhooks

    Trigger workflows from external events, and receive a signed callback when a run finishes

    Webhook Triggers
    Give a workflow its own URL that any service can call
    POST
    /api/v1/webhooks/:workflowId
    curl -X POST https://www.prompteryx.ai/api/v1/webhooks/YOUR_WORKFLOW_ID \
      -H "X-Webhook-Secret: your_workflow_secret" \
      -H "Content-Type: application/json" \
      -d '{"email": "user@example.com", "action": "process"}'

    Turn it on: add a Webhook Trigger node to the workflow in Visual Studio and save. Until then the URL answers 403 WEBHOOK_DISABLED.

    Authentication is set on the trigger node: none, a shared secret in the X-Webhook-Secret header, HTTP Basic, a Bearer token, or a custom header. You can also require an HMAC signature from the sender (GitHub, Stripe, Slack presets or custom). The HTTP method is enforced too (POST by default).

    Payload: the trigger's output is { body, headers, query, params, method, receivedAt }; later steps reference the JSON you sent through body (non-JSON bodies arrive as text).

    Limits: 30 calls a minute per caller IP; each call counts as a workflow run against your plan (429 QUOTA_EXHAUSTED when the monthly allowance is used up).

    Completion Callbacks
    Receive a POST request when a workflow execution finishes

    Pass webhookUrl in POST /api/v1/workflows/:id/execute (or webhookUrl in the SDK's workflows.run). When the run ends Prompteryx POSTs the result there. The URL must be public http(s); private and internal addresses are refused.

    What arrives

    POST https://your-server.com/callback
    Content-Type: application/json
    X-Prompteryx-Event: workflow.completed
    X-Prompteryx-Timestamp: 1790909344
    X-Prompteryx-Signature-256: v1=4fed51e60e8bcdcae67c1f08a1f83b3059773198696cf568806509728097a7c9
    
    {
      "event": "workflow.completed",          // or "workflow.failed"
      "timestamp": "2026-10-02T02:49:04.006Z",
      "data": {
        "executionId": "api_1790909291145_o3dbvg",
        "workflowId": "0QDHW2HTH3zRjHOtybJz",
        "workflowName": "Daily price check",
        "status": "completed",                // or "failed"
        "error": null,
        "outputs": { "Extract Price": { "extractedText": "$99.99" } },
        "duration": 204,                      // milliseconds
        "startedAt": "2026-10-02T02:49:03.594Z",
        "completedAt": "2026-10-02T02:49:04.006Z"
      }
    }

    outputs is the same node-name map as GET /api/v1/executions/:id. For the full per-node detail, call that endpoint with the executionId.

    Signature Verification
    Check that a callback really came from Prompteryx, and is fresh
    1. Create your signing secret on the API Keys page. It starts with whsec_ and is shown once. Rotating it there replaces it immediately.
    2. Every callback carries X-Prompteryx-Timestamp (unix seconds) and X-Prompteryx-Signature-256: v1=<hex>, where the hex is HMAC-SHA256 of "<timestamp>.<raw body>" keyed with your secret.
    3. Recompute it over the raw body, compare in constant time, and refuse timestamps more than 5 minutes from your clock.

    With the SDK

    import { constructWebhookEvent } from '@prompteryx/sdk'   // 0.5.0+
    
    app.post('/prompteryx-callback', express.raw({ type: 'application/json' }), async (req, res) => {
      try {
        const event = await constructWebhookEvent({
          secret: process.env.PROMPTERYX_WEBHOOK_SECRET,   // whsec_…
          body: req.body.toString('utf8'),
          headers: req.headers,
        })
        console.log(event.event, event.data.executionId, event.data.outputs)
        res.sendStatus(204)
      } catch {
        res.sendStatus(401)
      }
    })

    Plain Node.js

    const crypto = require('crypto')
    
    // rawBody: the request body as a string, BEFORE JSON parsing.
    function verifyPrompteryxCallback(rawBody, headers, secret /* whsec_… */) {
      const ts = headers['x-prompteryx-timestamp']
      const sig = headers['x-prompteryx-signature-256'] || ''
      if (!ts || !/^\d+$/.test(ts)) return false
      // Refuse anything older (or newer) than 5 minutes: replay protection.
      if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false
      const expected = crypto.createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex')
      // During a secret rotation the header can hold several v1= values.
      return sig.split(',').map((s) => s.trim()).filter((s) => s.startsWith('v1=')).some((s) => {
        const got = Buffer.from(s.slice(3))
        const want = Buffer.from(expected)
        return got.length === want.length && crypto.timingSafeEqual(got, want)
      })
    }
    Old header is going away
    Callbacks still carry the old X-Prompteryx-Signature header (HMAC keyed with the execution id) plus X-Prompteryx-Signature-Deprecated: true for one more release. It is not secret, because the id is in the body, so don't rely on it; switch to X-Prompteryx-Signature-256. Until you create a signing secret, callbacks carry only the old header. Callbacks sent by the desktop app (local runs) also carry only the old header for now.
    Always verify
    Verify the signature before acting on a callback, and use executionId to make your handler idempotent. A delivery is attempted once and is not retried.