Docs

Inbound webhooks

Start automation runs from third-party events — form submissions, payments, calendar bookings, anything that can POST JSON.

Overview

Inbound webhooks turn third-party events into automation runs. Build a workflow, pick Webhook as the trigger source, and the Studio mints a URL that accepts JSON POSTs. Each accepted payload starts one WorkflowRuntargeted at the contact identified in the payload.

  • URL shape: POST https://api.novusflow.tech/v1/webhooks/in/<token>
  • Body: any JSON object. The full body is available downstream as {{trigger.<path>}}.
  • Content-Type: application/json required.
  • Rate limit: 120 requests / minute per source IP. Bursts beyond that return HTTP 429.

Configuring a webhook trigger

In the Studio, click the TRIGGER node and set Trigger source to Inbound webhook. Save and activate the workflow. The inspector then exposes the four configuration knobs:

FieldPurposeDefault
Webhook URLThe endpoint you give the third-party app. Token is workspace-scoped.auto-minted on activation
Contact lookupMatch by email / phone / contact id.email
JSON path on payloadDot-path to the lookup value. Top-level or nested.(same as lookup kind)
Create the contact if not foundWhen checked, an unknown sender becomes a new Contact.off
Signing secretOptional HMAC shared secret for byte-exact body verification.blank (unsigned)

Signing & security

Two security layers stack on top of each other. Most workflows use just the first; the second is required when you can't fully trust the network path (public form endpoints, partners pushing to multiple tenants, audit-grade ingestion).

  • Token (always on) — the URL contains a 30-character random token. Anyone with the URL can post; anyone without it cannot.
  • HMAC signature (opt-in) — when you set a signing secret in the inspector, the request must carry an X-Novus-Signature header. The value is sha256=<hex> where the hex is HMAC-SHA256(secret, raw_body). Signature checks are constant-time. Requests with no header, a wrong signature, or a stale signature (mismatched secret rotation) return HTTP 401.
HMAC verify (Node)
import crypto from "node:crypto";
const expected = crypto
  .createHmac("sha256", secret)
  .update(rawBody, "utf8")
  .digest("hex");
// header value is "sha256=<expected>"

Payload mapping

Webhook payloads are rarely flat. The JSON path input accepts a dot-path so you can pluck the contact identifier out of nested envelopes (Stripe's data.object wrapper, Shopify's customersub-object, Tally's data.fields[]).

SenderLookup kindPathWhy
Tally formemaildata.emailTally puts the form value at the top of data.
Stripe — payment_intent.succeededemaildata.object.customer_emailCustomer email lives inside the event envelope.
Shopify — orders/createemailcustomer.emailCustomer is a sub-object of the order.
Typeformemailform_response.hidden.emailHidden fields keep PII out of analytics.
Calendlyemailpayload.invitee.emailBooking events carry the invitee email.
Hubspot — contact.creationidobjectIdHubspot ids match a custom metadata field.

Create-on-miss. If no contact matches, the default response is HTTP 404. Toggle Create the contact if not found and the webhook will instead create one with the looked-up email/phone and the configured defaults. The new contact ID is returned in the 202 response so your sender can store it for later.

Real-world examples

Tally form — new lead

POST /v1/webhooks/in/<token>
{
  "eventId": "tally_evt_abc",
  "data": {
    "email": "[email protected]",
    "firstName": "Ayu",
    "fields": [
      { "key": "interest", "value": "Premium plan" }
    ]
  }
}

Inspector: lookup email, path data.email, create-if-missing on, first-name default Ayu. The workflow then sees {{trigger.data.fields[0].value}} as the interest tag.

Stripe — payment_intent.succeeded

POST /v1/webhooks/in/<token> + X-Novus-Signature
{
  "id": "evt_3PaIDx",
  "type": "payment_intent.succeeded",
  "data": {
    "object": {
      "id": "pi_3PaIDx",
      "amount_received": 150000,
      "currency": "idr",
      "customer_email": "[email protected]",
      "metadata": { "invoice_number": "INV-1024" }
    }
  }
}

Inspector: lookup email, path data.object.customer_email, signing secret set to your Stripe webhook signing secret. Downstream nodes pull the invoice number via {{trigger.data.object.metadata.invoice_number}}.

Shopify — orders/create

POST /v1/webhooks/in/<token>
{
  "id": 820982911946154508,
  "name": "#1001",
  "customer": {
    "email": "[email protected]",
    "first_name": "Dewi",
    "phone": "+6281234567890"
  },
  "total_price": "299000.00",
  "currency": "IDR"
}

Inspector: lookup email, path customer.email. Order details flow through to a WhatsApp template via {{trigger.name}} and {{trigger.total_price}}.

Typeform — hidden contact id

POST /v1/webhooks/in/<token>
{
  "event_id": "tf_evt",
  "form_response": {
    "form_id": "abc",
    "hidden": { "contact_id": "cln3xy4z9000…" }
  }
}

Inspector: lookup Contact ID, path form_response.hidden.contact_id. This pattern keeps the form anonymous to Typeform while still routing the run to the right person — useful for in-app surveys.

Response codes

CodeMeaningSender action
202 AcceptedRun started. Response body carries { runId, contactId }.Treat as success.
401 UnauthorizedSigning secret set but signature missing or wrong.Rotate or re-sign.
404 Not FoundToken doesn't match an ACTIVE WEBHOOK workflow, or contact lookup failed (create-on-miss off).Check URL or enable create-on-miss.
422 Unprocessable EntityLookup path resolved to nothing or wrong type.Fix the JSON path or the sender's shape.
429 Too Many RequestsPer-IP rate limit hit (120/min).Backoff + retry.
5xxPlatform error.Retry with exponential backoff.