How to Capture Webhook Events into MySQL for Analytics
Receive external HTTP POST events on a Spojit webhook trigger, flatten the JSON payload, and write each event as a row in a MySQL events table for downstream analytics.
What This Integration Does
Many of the tools your business runs on can push events to a URL: a payment provider firing on a charge, a form tool firing on a submission, or an internal service emitting custom signals. Rather than letting those events disappear, this workflow captures each one and lands it as a durable row in your own MySQL database. Once the events are in MySQL, your BI tool, a scheduled report, or another Spojit workflow can aggregate, join, and chart them however you like.
The run model is event-driven. An external system sends an HTTP POST to your workflow's webhook URL, Spojit verifies the request and parses the JSON body, and the workflow flattens the nested payload into a single flat object before inserting it as one row. Each POST produces exactly one run and one row, so the database grows as an append-only event log. Re-runs are safe to reason about because every event is its own record, and you can opt into event-id deduplication on the trigger so retried deliveries from the sender do not create duplicate rows.
Prerequisites
- A MySQL connection added under Connections, with permission to insert into the target database.
- A target table to receive events. This tutorial uses a table named
eventswith columns such asevent_id,event_type,source,received_at, andpayload_json. - A signing connection for the webhook trigger so Spojit can verify inbound requests (schemes: Spojit, Shopify, GitHub, Slack, or Custom). See Setting Up a Webhook Connection.
- The external system that will send events, with the ability to configure an outbound webhook URL.
Step 1: Create the events table in MySQL
Add a Connector node in Direct mode, choose your MySQL connection, and select the execute-query tool to create the table once (you can delete this node after the first successful run, or run it ahead of time from your own database client). A simple schema that holds both typed columns and the full original payload looks like this:
CREATE TABLE IF NOT EXISTS events (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
event_id VARCHAR(128) UNIQUE,
event_type VARCHAR(128),
source VARCHAR(128),
received_at DATETIME,
payload_json JSON
);
The event_id UNIQUE constraint is your safety net against duplicate inserts if the sender retries delivery. To confirm the columns before you map them, you can add a second Direct-mode MySQL node with the describe-table tool and set its table field to events.
Step 2: Add the Webhook trigger
On the canvas, set the Trigger node's type to Webhook. Pick the signing connection that matches your sender's HMAC scheme, then copy the generated workflow URL. Spojit parses the inbound JSON body and exposes it as the trigger output; if the body is not valid JSON it arrives as { raw: "..." }. The webhook responds 202 with an executionId to the caller, so this is an asynchronous capture pattern, not a synchronous request/response. If your sender includes a unique event-id header, enable the opt-in deduplication so retried deliveries are ignored. For a deeper walkthrough see Setting Up a Webhook Trigger.
Step 3: Flatten the JSON payload
Real-world event payloads are usually nested. To turn them into clean column values, add a Connector node in Direct mode, choose the built-in json connector, and select the flatten tool. Map its input to the trigger body, for example {{ trigger.body }}. Flattening collapses nested keys into dotted paths, so a payload like this:
{
"id": "evt_8842",
"type": "order.created",
"data": { "order": { "total": 129.5, "currency": "USD" } }
}
becomes a flat object whose keys are id, type, data.order.total, and data.order.currency. This makes each value addressable by a single path when you build the row. If you only need a few specific fields rather than the whole object, you can instead use the json connector's get tool to pull one value at a time, or pick to keep a named subset.
Step 4: Build the row with a Transform node
Add a Transform node to shape one tidy object whose keys line up with your MySQL columns. Pull values from the flattened output and the trigger, and keep the full original body as a JSON string so nothing is lost:
{
"event_id": "{{ flatten_result.id }}",
"event_type": "{{ flatten_result.type }}",
"source": "checkout-service",
"received_at": "{{ now }}",
"payload_json": "{{ trigger.body }}"
}
Use the variable names that match your earlier nodes (the json flatten node's output and the trigger). For a reliable received_at timestamp you can add a Connector node in Direct mode using the date connector's now tool (with a format like YYYY-MM-DD HH:mm:ss) and reference its output here instead of a placeholder. See Working with Variables and Templates for the templating syntax.
Step 5: Insert the row into MySQL
Add a Connector node in Direct mode, choose your MySQL connection, and select the insert-rows tool. Set the table field to events and map the rows field to a single-element array containing the object you built in the Transform node:
{
"table": "events",
"rows": [ {{ transform_result }} ]
}
The rows field accepts a list of objects, so this pattern inserts exactly one event per run. The object keys must match the table's column names; any column you do not provide uses its database default (for example the auto-increment id). If you prefer full control over the SQL, you can instead use the execute-query tool with a parameterized INSERT statement.
Step 6: Acknowledge the caller (optional)
The webhook trigger already returns 202 automatically, so no Response node is required for a pure capture workflow. If your sender expects a specific body back, add a Response node as the final step and return a small confirmation such as { "stored": true, "event_id": "{{ flatten_result.id }}" }. Keep the response lightweight: heavy work after this point delays the sender. To learn more, read Using Response Nodes.
Tips
- Store the full payload in a
JSONcolumn (herepayload_json) alongside your typed columns. You get fast analytics on the common fields and never lose the original event shape when a sender adds new fields. - If you expect bursts of events, batch them: collect several into an array first, then pass all of them to
insert-rowsin one call. Fewer, larger inserts are easier on the database than one insert per event. - Normalize timestamps to UTC at write time using the date connector so your analytics queries do not have to reconcile mixed timezones later.
- Let Miraxa, the intelligent layer across your automation, scaffold the flatten and insert steps for you. A prompt like "Add a json flatten node after the webhook trigger and a Direct-mode MySQL insert-rows node into the events table" wires the nodes so you can fine-tune the field mapping in the properties panel.
Common Pitfalls
- Senders retry on timeout. Without an event-id header and a
UNIQUEconstraint onevent_id, a single real event can land as several rows. Enable dedup on the trigger and keep the unique index. - Column and key mismatch:
insert-rowsmaps object keys to columns by name, so a typo likeeventTypeversusevent_typesilently drops that value. Confirm names withdescribe-tablefirst. - Payloads that are not valid JSON arrive as
{ raw: "..." }. Add a Condition node to branch on whether{{ trigger.body.raw }}exists, and store raw bodies in a text column rather than the JSON column. - Schema drift: when a sender starts emitting a new nested field, your typed columns will not capture it. Because you also keep
payload_json, you can backfill new columns later without losing data.
Testing
Before pointing live traffic at the workflow, test against a throwaway table. Send a single sample POST to the webhook URL with a small known payload (you can use the http connector's http-post tool from a second scratch workflow, or any HTTP client), then open the run in execution history and confirm the json flatten output, the Transform object, and the MySQL insert-rows result each look right. Query the table with a Direct-mode MySQL execute-query node running SELECT * FROM events ORDER BY id DESC LIMIT 5 to verify the row landed with the expected columns. Once one event flows cleanly end to end, switch the sender to your production URL.