Documentation Index
Fetch the complete documentation index at: https://docs.spojit.com/llms.txt
Use this file to discover all available pages before exploring further.
Every workflow starts with a Trigger node. It defines how and when the workflow runs. Select the trigger type in the properties panel when you click on the trigger node.
Trigger types
| Type | Description |
|---|
| Manual | Run the workflow on demand by clicking Run in the toolbar |
| Webhook | Run when an external HTTP request hits the workflow’s webhook URL — supports Spojit’s own scheme plus Shopify, GitHub, Slack, and custom HMAC verification |
| Schedule | Run on a cron schedule (e.g., every hour, daily at 9 AM) |
| Email | Run when a new email arrives in a connected mailbox (Gmail or Outlook) |
Manual
No additional configuration needed. Click Run in the designer toolbar to execute the workflow. The request body you pass (if any) is available as the trigger output for downstream nodes.
Webhook
A webhook trigger fires when an external HTTP service POSTs to its unique URL. Spojit verifies the request’s HMAC signature against a webhook signing connection that you create once per upstream app and reuse across as many workflows as you like.
How signing connections work
Each webhook trigger references a Connection that owns:
- A scheme — one of
Spojit, Shopify, GitHub, Slack, or Custom. The scheme determines which header carries the signature, what gets signed, and how it’s encoded.
- One or two signing secrets. Two enables zero-downtime rotation; both verify simultaneously during the rotation window.
Multiple workflows can share the same connection. This matches how providers like Shopify, GitHub, and Slack issue one signing secret per app that signs every webhook from that app — rotating the secret upstream means updating exactly one connection in Spojit, and every workflow keeps working.
Setup
- Select the trigger node, set Trigger Type to Webhook.
- Click + next to the Signing connection picker.
- Pick a connector tile:
- Spojit — we generate the secret. Pick this when you control both sides of the integration (your own backend, scripts, internal tools).
- Shopify / GitHub / Slack — paste the signing secret from your provider’s app settings.
- Custom — for any provider we don’t have a preset for; you configure header, encoding, algorithm, and payload shape.
- Name the connection (e.g. “Shopify production”), provide the secret if applicable, and Create.
- The connection is auto-selected. Copy the Webhook URL and paste it into your provider’s webhook configuration.
Manage existing connections (rotate secrets, see which workflows use them, delete) from Connections → My Connections — click Configure on a webhook row to open its detail page.
Choosing a scheme
| Scheme | Header | Signed payload | Encoding | Pick when |
|---|
| Spojit | X-Spojit-Signature: t=<ts>,v1=<hex> | <unix-ts>.<body> | hex | Your own backend / scripts / tools |
| Shopify | X-Shopify-Hmac-Sha256 | raw body | base64 | Inbound from a Shopify app |
| GitHub | X-Hub-Signature-256: sha256=<hex> | raw body | hex | Inbound from a GitHub App or repo webhook |
| Slack | X-Slack-Signature: v0=<hex> (+ X-Slack-Request-Timestamp) | v0:<ts>:<body> | hex | Inbound from a Slack app’s Events API |
| Custom | configurable | configurable | configurable | Anything we don’t have a preset for |
All schemes use HMAC-SHA256 by default. The Custom scheme additionally allows SHA-1 (weak — only for legacy providers that require it) and SHA-512.
Provider presets
Spojit
We generate a whsec_… secret on connection create and show it once. Sign every request like this:
- Compute Unix timestamp in seconds
- HMAC-SHA256 of
<timestamp>.<raw-body> using your secret
- Hex-encode the result
- Send as header:
X-Spojit-Signature: t=<timestamp>,v1=<hex>
- Optional:
X-Spojit-Event-Id: <unique-id> for deduplication.
Timestamp tolerance: 5 minutes either direction. Older requests are rejected as replays.
Shopify
In your Shopify admin: Settings → Notifications → Webhooks. Each webhook subscription has a signing secret you can reveal — paste it when creating the connection.
Spojit verifies X-Shopify-Hmac-Sha256 (base64-encoded HMAC-SHA256 of the raw body). Deduplication uses Shopify’s X-Shopify-Webhook-Id header.
Provider docs: Shopify webhooks configuration.
GitHub
In GitHub: when creating the webhook on a repository or GitHub App, set a Secret value. Paste the same value when creating the Spojit connection.
Spojit verifies X-Hub-Signature-256: sha256=<hex>. Deduplication uses GitHub’s X-GitHub-Delivery header.
Provider docs: Validating webhook deliveries.
Slack
In your Slack app: Basic Information → App Credentials → Signing Secret. Paste it when creating the Spojit connection.
Spojit verifies X-Slack-Signature: v0=<hex> over v0:<ts>:<body>, with the timestamp in X-Slack-Request-Timestamp (5-minute skew). Deduplication uses Slack’s X-Slack-Event-Id header.
URL verification: Slack sends a one-time url_verification event when you save the webhook URL. Spojit handles this automatically — verifies the HMAC, then echoes the challenge field as text/plain. No workflow run is dispatched for verification events.
Provider docs: Verifying requests from Slack.
Custom
For providers without a preset, configure each axis explicitly:
| Field | Description |
|---|
| Signature header name | The HTTP header carrying the HMAC (e.g. X-Vendor-Signature). |
| Algorithm | SHA-256 (default), SHA-1 (weak — legacy only), SHA-512. |
| Encoding | hex or base64. |
| Value prefix | Optional literal string stripped before comparison (e.g. sha256=). |
| Signed payload | Raw body (sign body verbatim) or Timestamp.body (Stripe-style — requires a timestamp header). |
| Timestamp header / skew | Required when payload includes a timestamp. |
| Event ID header | Optional — used for deduplication. |
Example: Postman
To test your webhook from Postman:
Request:
- Method:
POST
- URL: your webhook URL (from the trigger properties panel)
- Headers:
Content-Type: application/json
X-Spojit-Signature: generated by the pre-request script below
X-Spojit-Event-Id: any unique string (optional — used for deduplication)
- Body (raw JSON):
{
"event": "order.created",
"data": {
"orderId": "ORD-12345",
"amount": 99.99
}
}
Pre-request Script (paste in Postman’s Scripts → Pre-request tab):
const secret = "whsec_YOUR_SECRET_HERE";
const timestamp = Math.floor(Date.now() / 1000).toString();
const body = pm.request.body.raw;
const payload = timestamp + "." + body;
const signature = CryptoJS.HmacSHA256(payload, secret)
.toString(CryptoJS.enc.Hex);
pm.request.headers.upsert({
key: "X-Spojit-Signature",
value: "t=" + timestamp + ",v1=" + signature
});
Replace whsec_YOUR_SECRET_HERE with your actual signing secret.
Example: Node.js
import crypto from "node:crypto";
const secret = "whsec_YOUR_SECRET_HERE";
const body = JSON.stringify({ event: "order.created", data: { orderId: "ORD-12345" } });
const timestamp = Math.floor(Date.now() / 1000);
const signature = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${body}`)
.digest("hex");
const response = await fetch("https://your-webhook-url", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Spojit-Signature": `t=${timestamp},v1=${signature}`,
"X-Spojit-Event-Id": "evt_unique_123",
},
body,
});
console.log(response.status); // 202
Example: Python
import hmac, hashlib, time, json, requests
secret = "whsec_YOUR_SECRET_HERE"
body = json.dumps({"event": "order.created", "data": {"orderId": "ORD-12345"}})
timestamp = str(int(time.time()))
signature = hmac.new(
secret.encode(), f"{timestamp}.{body}".encode(), hashlib.sha256
).hexdigest()
response = requests.post(
"https://your-webhook-url",
headers={
"Content-Type": "application/json",
"X-Spojit-Signature": f"t={timestamp},v1={signature}",
"X-Spojit-Event-Id": "evt_unique_456",
},
data=body,
)
print(response.status_code) # 202
Response codes
| Code | Meaning |
|---|
202 | Accepted — workflow run dispatched. Response includes executionId and status. |
401 | Signature missing, invalid, or timestamp outside the 5-minute tolerance window. |
404 | Trigger not found or inactive. |
409 | Duplicate event — a request with the same event-id header was already processed. Response includes the original executionId. |
402 | Insufficient credits. |
403 | Not authorized to run this workflow. |
500 | Internal error. |
Deduplication
Deduplication is opt-in via an event-id header. When the header is present, a second request with the same id on the same webhook returns 409 with the original executionId instead of running the workflow again. When it’s absent, every request fires a fresh execution.
The header depends on the connection type:
| Connection type | Event-id header |
|---|
| Spojit Webhook | X-Spojit-Event-Id (optional — set per logical event if you want retries deduped) |
| Shopify Webhook | X-Shopify-Webhook-Id (Shopify sends automatically) |
| GitHub Webhook | X-GitHub-Delivery (GitHub sends automatically) |
| Slack Webhook | event_id field in the request body (Slack sends automatically) |
| Custom Webhook | The header name you configured under Event ID header when creating the connection |
Secret rotation
Each connection can hold up to 2 active signing secrets at a time, enabling zero-downtime rotation. Because rotation lives on the connection, all triggers using that connection rotate atomically — there’s never a window where some workflows accept the old secret and others don’t.
Open the connection’s detail page (Connections → My Connections → Configure on the row), then:
- Click Rotate / add secret to create a second secret.
- Spojit scheme: copy the newly generated secret (shown once). Other schemes: rotate the secret upstream (in Shopify / GitHub / Slack admin, etc.), then paste the new value here.
- Both secrets verify simultaneously. Check the Last used timestamp on each — once the new secret shows recent activity, you’ve fully cut over.
- Delete the old secret.
The detail page also shows every workflow whose webhook trigger references this connection — handy for confirming what’ll be affected before you rotate. Spojit verifies incoming requests against all active secrets on the connection; the first match wins.
Webhook payload
The full HTTP request body is passed as the trigger output to downstream nodes. If the body is valid JSON, it’s parsed automatically. Otherwise, it’s available as a raw string under { raw: "..." }.
Polling execution status
When your webhook returns 202 Accepted (either async mode, or sync mode that hit its timeout), you’ll get back an executionId. You can poll the status endpoint to find out where the run is and retrieve the output once it’s done.
Endpoint:
GET /api/executions/{executionId}/status
Authentication: same HMAC scheme as webhook ingress — reuse your webhook’s signing secret. No separate credentials. The signed payload is {timestamp}.{executionId}.
X-Spojit-Signature: t=<unix-timestamp>,v1=<hex-hmac-sha256>
Response (200 OK):
{
"executionId": "cc74c932-59a8-4838-aef7-744b7a53637f",
"status": "RUNNING",
"currentStep": {
"id": "connector-abc123",
"label": "AI Agent",
"status": "RUNNING"
},
"stepsCompleted": 1,
"totalSteps": 3,
"output": null,
"error": null
}
Fields:
| Field | Description |
|---|
status | PENDING, RUNNING, WAITING_APPROVAL, COMPLETED, FAILED, or CANCELLED |
currentStep | The step currently executing (or waiting, e.g. for approval). null when the run has finished. |
stepsCompleted | Count of steps that have reached COMPLETED. Grows as the run progresses. |
totalSteps | Total work-producing steps in the workflow. Only populated for linear workflows. Returns null if the graph contains any Condition, Loop, or Parallel node (actual path length is dynamic). |
output | The workflow’s output, populated once status === 'COMPLETED'. When the workflow ends with a Response node, this mirrors its { statusCode, headers, body } shape. |
error | Error message if status === 'FAILED', otherwise null. |
Response codes:
| Code | Meaning |
|---|
200 | Status retrieved |
401 | Signature missing, invalid, expired (>5 min), or the originating trigger has no signing secrets |
404 | Execution not found |
Example: Node.js polling
import crypto from "node:crypto";
const secret = "whsec_YOUR_SECRET";
const executionId = "cc74c932-59a8-4838-aef7-744b7a53637f";
async function pollStatus() {
const timestamp = Math.floor(Date.now() / 1000);
const signature = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${executionId}`)
.digest("hex");
const response = await fetch(
`https://your-spojit-host/api/executions/${executionId}/status`,
{
headers: {
"X-Spojit-Signature": `t=${timestamp},v1=${signature}`,
},
},
);
return response.json();
}
// Poll until terminal status
while (true) {
const status = await pollStatus();
console.log(status.status, status.currentStep?.label ?? "—");
if (["COMPLETED", "FAILED", "CANCELLED"].includes(status.status)) {
console.log("Output:", status.output);
break;
}
await new Promise((r) => setTimeout(r, 2000));
}
Example: Python polling
import hmac, hashlib, time, requests
secret = "whsec_YOUR_SECRET"
execution_id = "cc74c932-59a8-4838-aef7-744b7a53637f"
def poll_status():
timestamp = str(int(time.time()))
signature = hmac.new(
secret.encode(),
f"{timestamp}.{execution_id}".encode(),
hashlib.sha256,
).hexdigest()
response = requests.get(
f"https://your-spojit-host/api/executions/{execution_id}/status",
headers={
"X-Spojit-Signature": f"t={timestamp},v1={signature}",
},
)
return response.json()
while True:
status = poll_status()
print(status["status"], (status.get("currentStep") or {}).get("label", "—"))
if status["status"] in ("COMPLETED", "FAILED", "CANCELLED"):
print("Output:", status["output"])
break
time.sleep(2)
Example: Postman
Same pre-request script as the webhook call, but with the payload changed to sign {timestamp}.{executionId}:
const secret = "whsec_YOUR_SECRET";
const url = pm.request.url.toString();
const match = url.match(/\/executions\/([^/]+)\/status/);
const executionId = match ? match[1] : "";
const timestamp = Math.floor(Date.now() / 1000).toString();
const payload = timestamp + "." + executionId;
const signature = CryptoJS.HmacSHA256(payload, secret)
.toString(CryptoJS.enc.Hex);
pm.request.headers.upsert({
key: "X-Spojit-Signature",
value: "t=" + timestamp + ",v1=" + signature
});
Recommended polling cadence
- Start with 1–2 second intervals for short-running workflows
- Back off to 5–10 seconds for workflows that routinely take minutes
- Always stop polling once
status is COMPLETED, FAILED, or CANCELLED — these are terminal
Schedule
Run a workflow automatically on a cron schedule. Each firing produces an independent workflow execution, identical in every way to a manually triggered run.
Setup
- Select the trigger node in the designer
- Set Trigger Type to Schedule
- Click Add Schedule
- Edit the Cron expression (5-field Unix cron — e.g.,
0 9 * * 1-5 for 9 AM weekdays)
- Pick a Timezone (IANA, e.g.
Australia/Sydney, America/New_York)
- Leave Active on to have it fire; toggle off to pause
- Click Save workflow — the schedule begins firing on the next matching time
Changes only apply on Save. Editing the cron or timezone field does not affect the running schedule until you save. The Next fires preview shows the three upcoming firings based on the currently saved configuration, so you can always see what’s actually going to run.
Multiple schedules
A single trigger node can hold any number of schedules. Use this to model real-world timing needs such as:
- Business hours vs. weekends — one schedule for
0 9 * * 1-5 (weekdays 9 AM) and another for 0 12 * * 0,6 (weekends noon)
- Peaks and off-peaks — run every 15 minutes during business hours, every hour overnight
- Multi-region timing — one schedule in
America/New_York, another in Europe/London, both hitting the same workflow
Each schedule is independent — you can pause one and leave the other active, or delete one without affecting the others.
Schedules use standard 5-field Unix cron:
┌───────────── minute (0-59)
│ ┌─────────── hour (0-23)
│ │ ┌───────── day of month (1-31)
│ │ │ ┌─────── month (1-12)
│ │ │ │ ┌───── day of week (0-6, Sun=0)
│ │ │ │ │
* * * * *
| Expression | Meaning |
|---|
0 * * * * | Every hour on the hour |
*/15 * * * * | Every 15 minutes |
0 9 * * * | Every day at 9 AM |
0 9 * * 1-5 | 9 AM on weekdays |
0 0 1 * * | Midnight on the 1st of every month |
0 9 * * 1 | 9 AM every Monday |
The properties panel shows a human-readable description under the cron input (e.g. “At 09:00 AM, Monday through Friday”) so you can verify your expression before saving.
Timezones
Every schedule has an IANA timezone (e.g. Australia/Sydney, Europe/London, UTC). The cron expression is evaluated in that timezone, which means daylight savings transitions are handled correctly — a 0 9 * * * schedule in America/New_York fires at 9 AM local time year-round, not at a fixed UTC offset.
Pausing a schedule
Toggle the Active switch off and save the workflow. The schedule is retained in the workflow’s configuration (so you can re-enable it later) but won’t fire while paused. This is reversible without losing the cron/timezone configuration.
Tips
- The Next fires preview shows the saved schedule, not your draft edits — trustworthy readout of what’s actually going to run
- You can combine a Schedule trigger with a Manual or Webhook trigger on the same workflow. Each is an independent entrypoint that converges on the same downstream logic.
- Keep the cron on conservative intervals during development (e.g. every 10 min) to validate behavior before switching to production cadence
Email
Run a workflow automatically when a new email arrives in a connected Gmail or Outlook mailbox. The workflow receives the parsed message — sender, subject, body, attachment metadata — and can act on it (extract data from a PDF order form, write to your ERP, notify a Slack channel, etc.).
How it works
You connect a mailbox to Spojit via OAuth (no password sharing — the OAuth grant is read-only by default). Spojit polls that mailbox on a schedule you choose and fires a workflow execution for every new message that matches your filters. The mail itself never travels through any third party — Spojit reads it directly from your provider (Google or Microsoft) using the OAuth token you granted.
Recommended pattern: dedicated mailbox
Instead of connecting your everyday inbox, create a dedicated mailbox in your workspace just for the workflow — for example, orders-automation@acme.com. Then set up a forwarding rule (or share the address with senders) so only the relevant emails land there. Benefits:
- Clear scope — Spojit only ever sees the dedicated mailbox, not your personal inbox
- Easy to revoke — disconnect the mailbox in Spojit or delete the mailbox in your workspace; either kills access
- Easy to audit — IT can see exactly what’s flowing through the dedicated address
Setup
- Connect the mailbox in the Connections page:
- Click Add connection → pick Gmail (trigger) or Outlook (trigger)
- Choose a permission level:
- Read only — Spojit can only read messages
- Read + modify — Spojit can also mark messages as read or move them after processing (needed for the optional “After processing” actions below)
- Sign in with the account that owns the mailbox
- The connection appears in your list with the granted scopes visible
- Add the trigger in the workflow designer:
- Drop a Trigger node, set Trigger Type to Email
- Click Add mailbox
- Pick the connection from the dropdown
- Pick the folder/label to watch (e.g.,
INBOX, or a custom label like Spojit/Orders)
- Set the poll interval (1–60 minutes; default 2)
- Optional: filter by sender domain or subject regex
- Optional: pick what to do with the message after Spojit processes it
- Click Save workflow — Spojit starts polling on the next cycle
Filters
Two optional filters narrow what triggers a run:
- From allowlist — comma-separated patterns.
@acme.com matches any sender at acme.com; orders@vendor.com matches the exact address. Multiple patterns are OR’d.
- Subject regex — a JavaScript-flavor regular expression. The message fires only if the subject matches.
If both filters are set, both must pass.
After processing
By default Spojit doesn’t touch the message — it stays in your folder, in whatever read state it was. If you want, you can ask Spojit to:
- Mark as read — useful for visual confirmation that Spojit picked it up
- Move to label / folder — pick a destination; in Gmail the label is added (other labels stay), in Outlook the message moves out of the source folder
Both options require a Read + modify connection — pick one when you create the connection if you plan to use these. Read-only connections will have the modify options disabled in the trigger panel.
Multiple mailboxes per workflow
A single workflow can watch multiple mailboxes — useful for things like “watch our US orders mailbox AND our EU orders mailbox, both run the same processing logic.” Add as many as you need; each fires the workflow independently.
Multiple workflows watching the same mailbox
Different workflows can watch the same mailbox + folder. Every matching message fires every matching workflow — independently and in parallel. Common pattern: one workflow processes the order into your ERP, another notifies sales on Slack — both fire on each incoming email.
A few important behavior notes for this pattern:
- No “first one wins” — each workflow gets its own copy of the email, runs its own logic
After processing actions don’t hide messages from siblings — even if Workflow A moves the message to a Processed folder, Workflow B still sees and processes it within the same poll cycle. You won’t miss a workflow because of another workflow’s cleanup action.
- Conflicting after-processing actions apply in order — if A says “mark read” and B says “move to folder X”, both happen. In Gmail, labels accumulate; in Outlook, the move wins.
The one limit: within a single workflow, you can’t add two EMAIL triggers with identical settings (same connection + folder + filters) — that would just fire the workflow twice for every message, which is never what you want. Cross-workflow duplicates are fine and intentional.
What Spojit can and can’t see
- Can see: messages in the folder you select. Spojit’s code is committed to only querying that folder, even though the OAuth scope technically grants whole-mailbox access (this is how Google/Microsoft OAuth scopes work — folder-level scoping isn’t an option from the provider).
- Can’t do (read-only connections): mark messages as read, move messages, delete messages
- Can do with Read + modify (only if you picked it at connection setup): mark as read, move/label — never delete
- Doesn’t store: full message bodies. Bodies and attachments are passed into the workflow execution and retained per the workflow execution’s normal retention policy. They are not stored in any separate Spojit-owned mailbox.
Disconnecting
In the Connections page, click Remove on the email connection. Spojit deletes the OAuth token and stops polling immediately. To revoke at the provider side as well (defense in depth):
Tips
- Start with a dedicated mailbox — much easier story for IT and for limiting blast radius
- Use Read only by default; only upgrade to Read + modify if you actually want Spojit to mark or move messages
- The poll interval doesn’t need to be aggressive — 2–5 minutes is fine for almost every order-processing use case; 1-minute polls aren’t dramatically faster end-to-end given queue + provider latency
- Pair a dedicated mailbox with a forwarding rule from your main inbox to filter just the messages you want Spojit to act on
- For testing, send yourself an email matching your filters and watch the Recent activity panel to confirm it landed
Output
The trigger node produces an output that downstream nodes can reference:
- Manual: The input data passed when clicking Run (if any)
- Webhook: The parsed HTTP request body
- Schedule:
{ scheduledAt } — the ISO-8601 timestamp of the firing that started the run
- Email:
{ from, to, cc, subject, textBody, htmlBody, headers, receivedAt, conversationId, attachments[] }. Each attachment is a reference ({ provider, providerMessageId, attachmentId, filename, mimeType, size }) — the bytes are fetched on demand by downstream nodes that need them.
Tips
- Start with Manual triggers during development, then switch to Webhook, Schedule, or Email when you’re ready for production
- A workflow can have multiple triggers (e.g., Manual + Webhook, or Schedule + Email) — each is an independent entrypoint
- Use the Active toggle to temporarily pause any trigger without deleting it or losing the configuration