# Webhook Triggers ## Create And Test A Webhook 1. Open **Settings → Automations → Webhooks**. 2. Select **New webhook**. 3. Choose the event and agent or company destination. 4. Copy the endpoint and signing secret into the sending service. 5. Send a test event and confirm it in the events log. ![Webhook subscriptions with copy, test, verify, edit, and enable controls](https://neotask-marketing-assets-417007889150.s3.us-east-1.amazonaws.com/docs/product/2026-08-13-r2/webhook-subscriptions-1196.webp) Webhook triggers let an external system kick off work in your autonomous company by sending it an HTTP request. When something happens elsewhere, such as a GitHub push, a support ticket, or an event from your own service, that system POSTs to a URL that belongs to your company, and Neotask turns it into a prompt for your agent to act on. ## What a Webhook Trigger Is Each webhook trigger is a **route**: a company-scoped URL, a signature scheme used to verify who's allowed to send to it, an optional filter on which event types to accept, and a prompt template that turns the incoming payload into instructions for your agent. When a verified delivery arrives, Neotask renders the prompt and queues it; the desktop app picks it up and fires an agent run from it. ## Creating a Route Open your company's **Settings**, then go to **Webhooks** under Integrations, and create a new route. You'll provide: - **Name**: a label for the route (a route slug is derived from it automatically, and you can edit the slug directly). - **Signature verifier**: which signing scheme the sender uses (see below). - **Event filter**: optional, see [Filtering by Event Type](#filtering-by-event-type). - **Prompt template**: the text sent to your agent, with placeholders filled in from the incoming payload. ### Provider Presets Instead of picking a signature scheme and building a prompt template from scratch, you can start from a curated preset for a specific provider, GitHub, Stripe, Resend, Clerk, and JotForm among them. A preset pre-selects the right signature scheme and pre-fills a starter prompt template with placeholders that match that provider's real event shape. Neotask remembers which preset a route was created from so **Send test event** (see [Testing Your Webhook](#testing-your-webhook)) can use realistic sample data for that exact provider rather than a generic one for the underlying scheme. Two presets are worth calling out because they don't map to a new signature scheme of their own: - **Resend** and **Clerk** both use the **Svix-style** scheme under the hood, no dedicated scheme was needed for either, since both providers deliver their webhooks the same way any Svix-based sender does. The secret direction for both is **the provider mints it, you paste it back into Neotask**: add your route's URL as an endpoint in the Resend or Clerk dashboard, copy the signing secret it shows you, and paste it into your route's settings. - **JotForm** has no webhook signing capability of any kind, confirmed directly on JotForm's own support forum, where JotForm's staff recommend a shared token in the webhook URL as the workaround. A JotForm route therefore uses the **URL token** scheme instead of a signature scheme, and the JotForm preset also handles a format difference: JotForm posts your form submission as `multipart/form-data`, not JSON, with the submission answers riding inside a `rawRequest` field. Neotask parses that automatically for any URL-token route receiving a multipart delivery, so your prompt template's placeholders resolve against the submission's parsed fields exactly like they would for a JSON provider. See the URL token row below and the JotForm note under [Which direction the secret travels](#which-direction-the-secret-travels) for the full detail. Choosing a preset is a convenience, every field it fills in is still a normal, editable route setting afterward, and you can always build a route from scratch by picking a signature scheme directly instead of a preset. ## The Webhook URL Each route gets its own public URL in this form: ``` https://neotask.ai/api/hooks// ``` Give this URL to the external system you want to trigger your agent from. It only accepts `POST` requests, and every request must be correctly signed. There is no unsigned/open mode. The route also answers a bodyless `HEAD` probe of the same URL, some providers, such as Intercom, send a `HEAD` request to validate the URL before they let you save the webhook, so the endpoint replies `200` when the URL resolves to an enabled route and `404` otherwise, without ever verifying, storing, or rate-limiting anything (a `HEAD` carries no payload). ## Signature Verification Every route picks one signature scheme when it's created, and the endpoint verifies every delivery against it. Neotask supports sixteen schemes: | Scheme | How it verifies | |---|---| | **GitHub-style** | Reads the `X-Hub-Signature-256` header (`sha256=`), an HMAC-SHA256 of the raw request body keyed with your route's secret. This is the same scheme GitHub itself uses for repository webhooks. | | **GitLab** | Reads the `X-Gitlab-Token` header and compares it against your route's secret. This is GitLab's own webhook scheme: a shared secret token sent verbatim, not a computed signature. There is no timestamp in this scheme, so no replay window applies; a redelivered event is instead deduplicated by its delivery id. | | **Stripe** | Reads the `Stripe-Signature` header (`t=,v1=`), an HMAC-SHA256 of the timestamp and raw body together, keyed with your Stripe signing secret (the whole `whsec_...` string). Deliveries older than five minutes are rejected as a replay-protection measure. During a Stripe secret rotation the header can carry more than one `v1` entry, and any matching one is accepted. | | **Shopify** | Reads the `X-Shopify-Hmac-Sha256` header, a base64 HMAC-SHA256 of the raw request body keyed with your route's secret. No timestamp exists in this scheme, so replay protection comes from delivery id deduplication. | | **Jira (token in URL)** | Standard Jira webhooks are unsigned, so this scheme uses a shared token carried in the webhook URL itself: append `?token=` to your route's URL when you register it in Jira. The endpoint compares the token against your route's secret and rejects anything else. | | **Svix-style** | Reads `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers, verifying an HMAC-SHA256 signature over the id, timestamp, and raw body together. Deliveries older than five minutes are rejected as a replay-protection measure. Use this for any sender built on the Svix webhook standard. | | **Generic timestamped HMAC** | For senders that don't speak any of the formats above. Reads an `X-Webhook-Timestamp` header and an `X-Webhook-Signature` header (a hex HMAC-SHA256 of the timestamp and raw body together), with the same five-minute replay window. | | **Calendly** | Reads the `Calendly-Webhook-Signature` header, in the same `t=,v1=` shape as Stripe, an HMAC-SHA256 of the timestamp and raw body together, keyed with your route's secret. Deliveries older than five minutes are rejected. | | **Cal.com** | Reads the `X-Cal-Signature-256` header, a hex HMAC-SHA256 of the raw request body keyed with your route's secret. No timestamp exists in this scheme, so replay protection comes from delivery id deduplication. | | **Typeform** | Reads the `Typeform-Signature` header (`sha256=`), a base64 HMAC-SHA256 of the raw request body keyed with your route's secret. No timestamp exists in this scheme. Typeform sends no signature at all if the webhook has no secret configured, so an unsigned request is rejected the same as a wrongly-signed one. | | **Tally** | Reads the `Tally-Signature` header, a raw base64 HMAC-SHA256 digest of the raw request body (no prefix) keyed with your route's secret. No timestamp exists in this scheme. | | **Vercel** | Reads the `x-vercel-signature` header, a hex HMAC-SHA1 of the raw request body keyed with your route's secret. No timestamp exists in this scheme. (Vercel's own scheme intentionally uses SHA1, not SHA256.) | | **HubSpot** | Reads the `X-HubSpot-Signature-v3` header (a base64 HMAC-SHA256) together with the `X-HubSpot-Request-Timestamp` header, over a signed string that concatenates the HTTP method, the exact public request URL, the raw request body, and the timestamp, keyed with your route's secret. Deliveries whose timestamp is more than five minutes old are rejected. Because the signature covers the exact URL HubSpot called, this scheme depends on your route's public URL matching what's registered in HubSpot's app settings byte-for-byte, if you ever front this endpoint with an additional proxy that rewrites the host or path, verification will fail closed rather than silently accept a mismatched request. | | **Intercom** | Reads the `X-Hub-Signature` header (`sha1=`), an HMAC-SHA1 of the raw request body keyed with your route's secret. No timestamp exists in this scheme. | | **Linear** | Reads the `Linear-Signature` header, a hex HMAC-SHA256 of the raw request body keyed with your route's secret. Replay protection here works differently from every other scheme: Linear puts a `webhookTimestamp` field inside the JSON body itself rather than a header, and Neotask rejects anything more than one minute old (a tighter window than the five minutes used elsewhere). | | **URL token (generic)** | Reads the `?token=` query-string parameter and compares it against your route's secret, the same mechanism as the Jira scheme above, generalized for any provider that offers no signing of its own. JotForm is the motivating example: JotForm has no webhook signing capability at all, so a JotForm route uses this scheme instead. Because there is no body or header signature to check, this scheme's security rests entirely on keeping the full URL, including the token, secret, and only ever sending it over HTTPS. Unlike the "Generic timestamped HMAC" scheme above (for senders that CAN sign but don't match a named provider), this scheme is for senders that CAN'T sign at all. | ## The Signing Secret When you create a route, Neotask generates a secret and shows it to you exactly once, with a clear warning that it will not be shown again. Copy it into your sending system right away. If you lose it, you can rotate the secret from the route's settings, which generates a new one and shows it once in the same way; the old secret immediately stops working. The secret is never displayed again after that moment, in the UI or in any export. Only whether a route has one is shown afterward. ### Which direction the secret travels Who mints the secret depends on the provider, and the route settings support all three directions: - **Neotask mints it, you paste it into the provider**: GitHub, GitLab, Calendly, Cal.com, Typeform, Tally, and custom senders (Svix-style and generic HMAC). Create the route, copy the secret Neotask shows you once, and paste it into the provider's webhook configuration as its signing secret or secret token. (Calendly's standard integration path works this way; a separate OAuth-app integration style has Calendly generate a signing key per application instead, which doesn't apply to a single route's secret.) - **The provider mints it, you paste it back into Neotask**: Stripe, Shopify, Vercel, HubSpot, Intercom, Linear, **Resend**, and **Clerk**. Each of these generates its own secret rather than letting you choose one, Stripe's signing secret (`whsec_...`) when you register the endpoint, Shopify's shared secret shown below the webhooks list in Settings > Notifications, Vercel's shown once when the webhook is created, HubSpot's app Client Secret from its Auth settings, Intercom's app Client Secret from the Developer Hub, Linear's shown on the webhook's detail page, and Resend's and Clerk's own Svix-backed signing secrets (also `whsec_...`) shown once when you add your route's URL as an endpoint in their dashboards. Register your route's URL with the provider first, copy the secret it shows you, then paste it into your route's settings in Neotask. Setting a secret this way replaces the previous one immediately, and Neotask never displays it back to you afterward. - **The secret rides in the URL**: Jira, and any route using the generic **URL token** scheme, most notably **JotForm**, which has no webhook signing of any kind. Because these providers carry no signature, the route's secret instead acts as an access token appended to the URL you give the provider (`?token=`). Treat that full URL as sensitive, exactly as you would the secret itself, anyone who has the URL can trigger the route. For a provider-mints-it route, the route's secret is set to a Neotask-generated placeholder the moment you create it, so verification against a real delivery fails closed, every delivery is rejected with `401`, until you paste the provider's own secret back in. Neotask tracks whether that paste-back has happened, and the desktop app warns on a route created from a provider-mints-it preset (or manually pointed at one of those verifiers) until it detects a successful paste-back, so it's obvious a route is silently rejecting every delivery rather than discovering it only after the provider reports failed deliveries. Rotating a route's secret (even for a provider-mints-it route) always generates a new Neotask placeholder and re-arms this warning, since the newly generated secret is not the provider's real signing secret either, paste the provider's secret back in again after a rotation. ## Testing Your Webhook Every route has a **Send test event** action in its settings. It runs your prompt template against a small, realistic sample payload for the route's provider (for example, a pull request event for a GitHub route or a successful payment event for a Stripe route) and queues the result as a pending event, marked as a test so you can tell it apart in the events log. The response shows you the exact rendered prompt your agent would receive, which makes it the quickest way to check that your placeholders resolve the way you expect before pointing the real sender at the route. If your route was created from a specific [provider preset](#provider-presets) such as Resend, Clerk, or JotForm, the test event uses a sample shaped for that exact provider, a JotForm route's test, for example, renders against a realistic parsed submission rather than the generic URL-token sample. A route created without a preset (or from an unrecognized one) falls back to a sample for its underlying signature scheme, exactly as before. Test events go through the same template rendering, the same queue, and the same rate limit as real deliveries, but they skip signature verification, because you trigger them from inside Neotask rather than from the external sender. A disabled route refuses to send test events until you re-enable it. ## Filtering by Event Type By default a route accepts every delivery that passes signature verification. If you only want certain event types to reach your agent, add them to the route's event filter as a comma-separated list. Neotask determines the event type of each delivery the same way GitHub-style clients do: from an `X-GitHub-Event` header if present, otherwise from an `event_type` or `type` field in the payload itself. A filtered-out delivery is acknowledged but never queued for your agent. ## Prompt Templates and Placeholders The prompt template is what your agent actually receives, built from the verified payload. Use `{dot.path}` placeholders to pull values out of the incoming JSON, for example `{action}` or `{repository.full_name}`. A few rules to keep in mind: - Placeholders resolve against the parsed payload only; a value you insert is never re-scanned for more placeholders, so payload content can't inject additional substitutions. - If a path doesn't exist in the payload, the placeholder is left in the prompt as literal text rather than silently disappearing, so a broken template is easy to spot. - Long values are trimmed (each inserted value up to 2,000 characters), and the whole rendered prompt is capped at 16,384 characters. ## Delivery Status and the Events Log After sending a test, open the event list and confirm its delivery state before registering the real sender. ![Recent webhook event with its delivery state](https://neotask-marketing-assets-417007889150.s3.us-east-1.amazonaws.com/docs/product/2026-08-13-r2/webhook-events-1196.webp) The **Webhooks** section also shows a log of recent deliveries for each route, grouped by status: - **Pending**: verified and queued, waiting for the desktop app to run it. - **Consumed**: picked up and run. - **Failed**: picked up but the run did not complete successfully. Not every delivery becomes a logged event. A request can also be turned away or acknowledged without ever reaching the queue: an unsigned or incorrectly signed request is rejected, a redelivery of something already received is acknowledged without creating a second event, a delivery excluded by your event filter is acknowledged but not queued, and a route sending too many requests too quickly is rate-limited. ## Rate Limits and Payload Size Each route accepts up to 30 deliveries per minute by default; requests beyond that are rejected until the window resets. Each delivery is also capped at 1 MiB, and larger payloads are rejected outright. ## Run Policy Each route also carries a run policy that governs the run a verified delivery triggers: whether you're notified when it fires (off by default), an optional pinned model for that run, and an optional daily cap on how many runs the route can trigger. Leaving all of this unset behaves exactly as described above, a verified delivery is queued and picked up by the desktop app with no extra gate. The policy is captured on each event at the moment it's received, so an edit to a route's run policy only affects deliveries from that point forward, never events already sitting in the queue. Enforcement of the run policy (the delivery notification, the model pin, and the daily cap) is handled by the desktop app. There is no per-route pre-run approval setting: a webhook-triggered run is governed in-run by the same approval settings and autonomy tier as the company's autonomous runs, so it asks for approval exactly where any other autonomous action would. ## HIPAA Mode If your company has HIPAA mode enabled, inbound webhook deliveries are acknowledged with a normal success response, but the payload is never processed or stored. This check happens before anything else, including signature verification, because an external sender isn't a party Neotask has a signed agreement with to receive protected health information. In practice this means webhook triggers are effectively inert for a company in HIPAA mode: deliveries won't be rejected outright, but they also won't reach your agent. ## Main Agent Webhooks Webhook triggers aren't only for a company's agent. You can also create a route on your account's main agent, so an external system can kick off work that isn't tied to any specific company. Main agent routes work the same way as company routes: the same signature verifier choices, the same optional event filter, the same prompt template placeholders, the same rate limits and payload cap, and the same once-only secret display. The differences are where you create them and the shape of the URL. Create a main agent route from your account-level **Webhooks** settings, alongside your company's onboarding surfaces. Each route gets its own public URL in this form: ``` https://neotask.ai/api/hooks/main/ ``` The `` is a random, opaque identifier Neotask generates for the route; it doesn't reveal your account or tenant. Deliveries, signature verification, filtering, idempotency, rate limiting, and the events log all work exactly as described above for company routes, just scoped to your main agent instead of a company. ## Related - [Skill Learning](skill-learning) - [Automation & Scheduling](automation) - [Auto - Apps, Integrations & Files](auto-companies-apps-integrations-and-files)