How to Acknowledge Synchronous Webhooks Instantly Then Process Work Asynchronously
Return a 200 to a webhook caller in milliseconds with a Response node, persist the raw payload to MongoDB, then hand the heavy lifting to a Subworkflow so the caller never waits on your processing.
What This Integration Does
Many external systems that send webhooks expect a fast reply. If your workflow runs a long chain of connector calls, enrichment, and database writes before answering, the caller can time out, mark the delivery as failed, and retry, which floods you with duplicate events. The acknowledge-then-process pattern fixes this: the moment a payload arrives you persist it, send the caller a clean acknowledgement, and only then do the real work. The caller sees a quick, healthy response while the slow part runs on its own.
In Spojit you build this with a Webhook trigger that receives the POST, a Connector node calling the MongoDB insert-documents tool to store the raw body, a Response node that returns the acknowledgement to the caller, and a Subworkflow node that invokes a second workflow to do the heavy processing. The parent workflow finishes quickly; the child workflow carries the long-running steps. Each event leaves a stored record in MongoDB, so even if downstream processing fails you still have the original payload to replay. Re-runs are safe because the stored document carries the event identifier you can deduplicate on.
Prerequisites
- A MongoDB connection added in Spojit under Connections -> Add connection, with write access to a database and a collection (for example a
webhook_eventscollection) for storing raw payloads. - A signing connection for the Webhook trigger (scheme Spojit, Shopify, GitHub, Slack, or Custom) so incoming requests are verified by HMAC. See the connections guides if you have not set one up yet.
- A second workflow already created (even an empty shell) that will act as the asynchronous processor. You will point the Subworkflow node at it. You can build it first or stub it and fill it in later.
- Knowing the shape of the payload the caller sends, so you can map fields into MongoDB and into the Subworkflow input.
Step 1: Receive the event with a Webhook trigger
Create the parent workflow and set its Trigger node type to Webhook. Attach your signing connection so requests are verified by HMAC before the workflow runs. The trigger exposes the parsed JSON body as {{ trigger }} (or {{ trigger.raw }} when the body is not JSON). If your caller sends a unique event identifier in a header, enable the opt-in deduplication option keyed on that header so a replayed delivery does not start a second run. A typical inbound body looks like this:
{
"event": "order.created",
"id": "evt_8a1f29",
"order": { "id": 5012, "total": 149.99, "email": "buyer@example.com" }
}
Step 2: Persist the raw payload to MongoDB
Add a Connector node in Direct mode, choose the MongoDB connector, and select the insert-documents tool. Set the database and collection to your event store (for example webhook_events). In the documents field, write the raw event plus a couple of fields you will want for deduplication and auditing. Direct mode keeps this step deterministic and costs no AI credits. Map the document like this:
[
{
"eventId": "{{ trigger.id }}",
"type": "{{ trigger.event }}",
"payload": {{ trigger }},
"status": "received"
}
]
Capture the node output (for example {{ store_event }}) so the inserted record identifier is available downstream if you need to reference it.
Step 3: Acknowledge the caller with a Response node
Add a Response node directly after the MongoDB insert. The Response node returns a value to the synchronous Webhook caller and lets you answer before the rest of the workflow runs. Return a small, predictable acknowledgement that confirms receipt and echoes the event identifier so the caller can correlate it:
{
"received": true,
"eventId": "{{ trigger.id }}"
}
Because you persist first and respond second, the caller only gets a success acknowledgement once the payload is safely stored. Keep this body lean: the caller does not need processing results, only confirmation that you have the event.
Step 4: Hand the heavy work to a Subworkflow
Add a Subworkflow node after the Response node. In its Workflow field, pick the second workflow you prepared as the asynchronous processor. In the Input field, pass the stored event so the child has everything it needs without re-reading anything:
{
"eventId": "{{ trigger.id }}",
"order": {{ trigger.order }}
}
The child workflow runs through its own trigger and nodes, and its final output returns to this parent. Because the caller was already answered in Step 3, the time the Subworkflow takes does not hold up the webhook response. Children appear as their own entries in execution history, which keeps the slow processing easy to inspect in isolation.
Step 5: Do the real processing in the child workflow
Open the child workflow. Give it a Manual trigger so its input is the object the parent passes in, available as {{ trigger }}. Build the long-running logic here: call your e-commerce or CRM connector in Direct mode, enrich data, branch with Condition nodes, or use a Loop for line items. For example, fetch the full order, then run further connector calls. Keep each step's output mapped to a named variable so later steps can read it. This is where minutes of work are allowed to happen, safely off the caller's request.
Step 6: Update the stored record when processing finishes
At the end of the child workflow, add a Connector node in Direct mode using the MongoDB update-documents tool to mark the event complete. Set the database and collection to the same event store, then provide a filter matching the original record and an update setting the new status:
{
"filter": { "eventId": "{{ trigger.eventId }}" },
"update": { "$set": { "status": "processed", "processedAt": "..." } }
}
Now your MongoDB collection is an audit trail: received rows that never reach processed point straight at events that failed downstream, and you can replay them by re-running the child workflow with the stored payload.
Tips
- Keep the Response node body small and fast. The whole point of the pattern is a quick acknowledgement, so do not run connector calls or AI steps before it.
- Use the MongoDB
find-documentstool with a filter oneventIdbefore inserting if you want a hard guard against duplicate processing, in addition to the Webhook trigger's event-id deduplication. - If you need branching logic per event type, add a Condition node in the child workflow rather than the parent, so the parent stays a thin acknowledge-and-store shell.
- Ask Miraxa, the intelligent layer across your automation, to scaffold the parent for you: "Build a webhook workflow that inserts the body into MongoDB, returns a 200 with a Response node, then calls a Subworkflow." Then fine-tune field mappings in the properties panel.
Common Pitfalls
- Putting the Subworkflow before the Response node defeats the pattern: the caller then waits for the entire child run. Always Response first, Subworkflow after.
- Forgetting the signing connection on the Webhook trigger. Without HMAC verification, anyone who learns the URL can start runs. Attach a signing connection before going live.
- Not storing an event identifier. Without
eventIdon the document, you cannot deduplicate replays or correlate the laterupdate-documentscall back to the right record. - Returning processing results in the Response body. The slow work has not run yet at that point, so any results would be empty. Return only a receipt acknowledgement.
Testing
Before going live, point a test caller (or your own HTTP client) at the workflow's webhook URL with a small sample payload and confirm you get the acknowledgement back quickly. Open the MongoDB collection and check a received document landed with the correct eventId and full payload. Then open execution history and confirm the child Subworkflow ran as its own entry and updated the record to processed. Send the same payload twice to verify deduplication keeps you to a single processed event. Once a handful of test events flow cleanly end to end, switch the caller over to the real endpoint.