Flow triggers and inputs
Start flows from signed webhooks, provider-signed webhooks from Shopify, Stripe, GitHub, HubSpot, Adobe Commerce and VTEX, approved file uploads, and two-step chained actions.
Updated 09/10/2026
On this page
Ways records reach a flow
Besides schedules and manual runs, a flow can take records from outside Nexra:
| Input | Use it when |
|---|---|
| Signed webhook | Your own system can sign a request with Nexra's nexra_v1 scheme. |
| Provider-signed webhook | Shopify, Stripe, GitHub, HubSpot, Adobe Commerce or VTEX sends events in its own signed format. |
| Approved file upload | Someone exports an approved CSV or JSON file and uploads it to start a run. |
Every input is checked in full against the flow's record schema before anything is kept, stored as a workspace file for 7 days by default (up to 30 for webhook triggers) so a run can be retried, and recorded on the evidence chain by its SHA-256 only. Each accepted input starts an ordinary run, so every write still goes through the governed-action gate.
The flow's How records arrive panel shows what it accepts and lists recent inputs and deliveries.
Signed webhooks
Create a trigger
Publish the flow, then choose Create a webhook trigger, or call the API. Creating triggers needs connect.schedule.manage.
POST /api/console/connect/flows/<flow id>/triggers
Content-Type: application/json
{ "name": "Rostering system", "environment": "sandbox" }{
"triggerId": "<trigger id>",
"flowId": "<flow id>",
"kind": "webhook",
"name": "Rostering system",
"status": "active",
"environment": "sandbox",
"url": "/api/connect/triggers/ftp_Qm3v…",
"secretLast4": "x9Ka",
"maxBodyBytes": 65536,
"maxDeliveriesPerMinute": 60,
"timestampToleranceSeconds": 300,
"inputRetentionDays": 7,
"verification": "nexra_v1",
"signing": {
"headers": ["X-Nexra-Event-Id", "X-Nexra-Timestamp", "X-Nexra-Signature"],
"algorithm": "HMAC-SHA256",
"signedString": "v1:{timestamp}:{eventId}:{raw body}",
"signatureFormat": "v1={hex}"
},
"secret": "nxwhsec_…",
"secretShownOnce": true
}The secret is shown once. Store it in the sending system's secret store; afterwards Nexra shows only its last four characters. It is held in a secure vault, never in the database or the URL. The URL carries only an unguessable trigger ID.
You can set maxBodyBytes (up to 100 KiB), maxDeliveriesPerMinute (up to 600), timestampToleranceSeconds (30 to 900) and inputRetentionDays (up to 30) when you create the trigger.
Send a delivery
POST /api/connect/triggers/ftp_Qm3v…
Content-Type: application/json
X-Nexra-Event-Id: evt_visits_2026-10-09_0001
X-Nexra-Timestamp: 1791532800
X-Nexra-Signature: v1=29eade3aea26da9593c48a5fc620d1f036d780fd8406c5324c1540fa12bd39e1
{"records":[{"id":"V-1042","clientId":"C-311","serviceDate":"2026-10-08","description":"Home visit, 1 hour","amount":42.5,"approvalStatus":"approved"}]}| Header | Value |
|---|---|
X-Nexra-Event-Id | Unique per event: 8 to 128 characters of A-Z a-z 0-9 . _ : - |
X-Nexra-Timestamp | Unix seconds when you signed |
X-Nexra-Signature | v1= followed by the hex HMAC-SHA256 of the signed string |
The signed string is v1: + timestamp + : + event ID + : + the raw body bytes, keyed with the trigger secret. The example above was signed with the placeholder secret nxwhsec_REPLACE_WITH_YOUR_TRIGGER_SECRET; use it to check your own code produces the same signature.
import crypto from "node:crypto";
export function signNexraDelivery(secret, eventId, rawBody) {
const timestamp = Math.floor(Date.now() / 1000);
const signature = crypto
.createHmac("sha256", secret)
.update(`v1:${timestamp}:${eventId}:`)
.update(rawBody)
.digest("hex");
return {
"Content-Type": "application/json",
"X-Nexra-Event-Id": eventId,
"X-Nexra-Timestamp": String(timestamp),
"X-Nexra-Signature": `v1=${signature}`,
};
}import hashlib, hmac, time
def sign_nexra_delivery(secret: str, event_id: str, raw_body: bytes) -> dict:
timestamp = int(time.time())
signed = f"v1:{timestamp}:{event_id}:".encode() + raw_body
signature = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return {
"Content-Type": "application/json",
"X-Nexra-Event-Id": event_id,
"X-Nexra-Timestamp": str(timestamp),
"X-Nexra-Signature": f"v1={signature}",
}Sign the exact bytes you send. The body can be one record, a list of records, or an object whose records field (set on the flow, for example records) holds the list, with up to 500 records per delivery.
Responses
| Status | Meaning |
|---|---|
202 | Accepted. A run is queued: {"accepted": true, "deliveryId": "…", "executionId": "…", "payloadSha256": "…", "records": 1} |
200 | The same event ID and body were already accepted. It is not run again. |
401 | Missing or invalid signature, or the timestamp is outside the window (300 seconds by default). |
404 | Unknown or disabled trigger. |
409 | The event ID was reused with a different body, or the flow has no published webhook version. |
413 | The body is larger than the trigger's limit (64 KiB by default). |
415 | The body is not application/json. |
422 | The records do not match the flow's schema. The response names the record and field, never the value. |
429 | Over the trigger's rate limit (60 deliveries a minute by default), or too many refused requests from your address. |
503 | Temporarily unavailable. Retry with the same event ID. |
Retrying with the same event ID is always safe: each event ID is single use per trigger and remembered for 7 days. The rate limit counts only correctly signed deliveries, so knowing a trigger URL is not enough to use up its quota.
Rotate and disable
Rotate issues a new secret. Pass {"graceMinutes": 60} (up to 1,440) to keep the old secret valid while senders switch; during that window, either signature is accepted. Disable stops the trigger. Deliveries shows recent attempts with their status and payload SHA-256 only.
| Method | Path |
|---|---|
POST | /api/console/connect/flows/:flowId/triggers |
GET | /api/console/connect/flows/:flowId/triggers |
POST | /api/console/connect/triggers/:triggerId/rotate |
POST | /api/console/connect/triggers/:triggerId/disable |
GET | /api/console/connect/triggers/:triggerId/deliveries |
Provider-signed webhooks
Set verification when you create the trigger to accept a provider's own signature, with no relay in between. Send the secret the provider signs with once, as providerSecret (for Adobe, the clientId). It goes straight to a secure vault and is never returned, logged or placed in a URL. The scheme cannot change after creation.
POST /api/console/connect/flows/<flow id>/triggers
Content-Type: application/json
{
"name": "Shopify orders",
"verification": "shopify",
"providerSecret": "<your Shopify app client secret>",
"shopDomain": "example-store.myshopify.com",
"eventTypes": ["orders/create"]
}eventTypes (optional, up to 20) keeps only those topics; other events are answered 200 and ignored, so the provider does not retry them.
verification | Proof of origin | Replay protection | Records come from |
|---|---|---|---|
shopify | X-Shopify-Hmac-Sha256: base64 HMAC-SHA256 of the raw body with your app's client secret. Optional shopDomain must match. | Single-use X-Shopify-Webhook-Id; X-Shopify-Triggered-At at most 24 hours old | The body |
stripe | Stripe-Signature t=…,v1=…: hex HMAC-SHA256 of t.body with your whsec_ endpoint secret. | Signed timestamp within the trigger's window; single-use event ID | data.object |
github | X-Hub-Signature-256: sha256= hex HMAC-SHA256 of the body. | Single-use X-GitHub-Delivery, kept 30 days | The body; ping is acknowledged without a run |
hubspot_v3 | X-HubSpot-Signature-v3: base64 HMAC-SHA256 of method, full trigger URL, body and timestamp with your app's client secret. | X-HubSpot-Request-Timestamp within 5 minutes; single-use event IDs | The list of events |
adobe_io_events | Two RSA-SHA256 digital signatures, checked against Adobe's published public keys; the recipient client ID must match. | Single-use eventid, kept 30 days | data.value for Adobe Commerce, otherwise data |
vtex | VTEX hooks are unsigned, so Nexra issues a secret that the hook sends in X-Nexra-Hook-Token through hook.headers. | Single-use order ID, state and last change, kept 30 days | The body |
For Adobe Commerce, use Adobe I/O Events (for example observer.sales_order_place_after); Nexra answers the registration challenge for you. For VTEX, Nexra generates the secret and shows it once; the token proves the sender holds it. GitHub and VTEX send no signed timestamp, so their replay protection rests on the single-use delivery key, kept for 30 days.
Accepted provider deliveries are answered 200, as Shopify and VTEX require. Repeats are answered 200 without running again, even when the provider changed delivery details in the body. Secrets in URLs are never accepted. For a sender that cannot sign in one of these ways, place a small relay in front that re-signs with nexra_v1.
Rotate works for provider schemes too: send the provider's new providerSecret with an optional grace period, matching the rolling secrets Stripe and others offer.
Approved file uploads
When a person exports an approved file, upload it and start a run with it. In the console, open the flow and choose Upload approved export. Uploading and running needs connect.flow.execute (Owner, Admin, Developer and Connect operator).
POST /api/console/connect/flows/<flow id>/inputs?fileName=visits.csv
Content-Type: text/csv
id,clientId,serviceDate,description,amount,approvalStatus
V-1042,C-311,2026-10-08,"Home visit, 1 hour",42.50,approved
V-1043,C-207,2026-10-08,Medication check,18.00,approved{
"inputId": "<input id>",
"flowId": "<flow id>",
"channel": "file",
"format": "csv",
"fileName": "visits.csv",
"sizeBytes": 141,
"sha256": "e4b0…",
"recordCount": 2,
"executionId": null,
"createdAt": "2026-10-09T09:02:17.310Z",
"expiresAt": "2026-10-16T09:02:17.310Z"
}Then start the run with the returned inputId. One upload starts one run.
POST /api/console/connect/flows/<flow id>/executions
Content-Type: application/json
{
"environment": "sandbox",
"inputId": "<input id>",
"idempotencyKey": "visits-2026-10-09",
"correlationId": "visits-2026-10-09"
}CSV rules
CSV is parsed strictly, so a file is either read exactly as written or refused with a clear reason:
UTF-8, with or without a byte-order mark.
A header row of unique field names that includes every required field.
Comma delimiters, double-quote quoting and "" for a quote inside a quoted cell.
CRLF or LF line endings; one trailing line break is fine, blank lines elsewhere are not.
The same number of cells in every row.
Cells convert to the field's type (number, integer, boolean or date). An empty cell is treated as no value.
No spreadsheet formulas: by default a text cell starting with =, +, -, @ or a tab is refused, so nothing reaches the destination as a live formula.
Up to 10,000 rows per file, and 5 MiB by default (a flow can allow up to 20 MiB).
Errors name the line and column, never the cell's content. JSON uploads follow the same schema and can be one record, a list, or an object holding the list.
Two-step chained actions
A destination step can use the result of an earlier destination step, for example a purchase-order line written to the header just created:
{ "chain": { "fromStep": "po_header", "bind": [{ "from": "id", "to": "orderId" }] } }The second step runs after the first, and only for records the first step completed.
Each write has its own approval. An approval binds the exact action, and the second write includes the first write's result, which only exists once the first write has run.
When the first write waits for approval, approving it writes it and then requests the second write's approval automatically, bound to the real result. Nobody has to retry the record, and this survives a restart.
If the second write fails after the first succeeded, the record waits in the dead-letter queue on the second step with a note naming what the first step created. Retrying writes against the same result. Nexra never reverses the first write automatically.
Chains are two steps deep. Use separate flows for longer sequences.
Next
Governed workflow templates: templates that use uploads, signed webhooks and chained steps.
Runs, monitoring and recovery: follow the run each input starts.