Blog

Pipedream6 min read

Send form submissions to Pipedream

Trigger a Pipedream workflow from every Ikiform submission, verify the signature in a Node.js step, and pass the fields to any of Pipedream's apps.

Preet SutharFounder, Ikiform

Pipedream sits between no-code and code: each workflow can mix prebuilt app actions with short Node.js steps. That makes it easy to verify Ikiform's signature properly and then hand the fields to Slack, a database or an API.

How a submission gets from your form to Pipedreamyour formany html form,framework orserverIkiformstores it,signs a webhookPipedreamhttp trigger,then a node.jsverify stepany appSlack,Postgres, anAPI, emailpostsignedstepsHow a submission gets from your form to Pipedreamyour formany html form, framework orserverIkiformstores it, signs a webhookPipedreamhttp trigger, then a node.jsverify stepany appSlack, Postgres, an API, emailpostsignedsteps
  1. A visitor submits your form, and it posts to Ikiform.
  2. Ikiform stores the submission and posts a signed webhook to the workflow's HTTP trigger.
  3. A Node.js step checks the signature and ends the run for anything else.
  4. Later steps send the fields to Slack, Postgres, an API or email.
Ikiform posts to the workflow's HTTP trigger. A Node.js step checks the signature and stops anything else.

Before you start

  • An Ikiform form that already receives submissions.
  • A Pipedream account.

What Ikiform sends

Every delivery is a POST with a JSON body. The body has the same shape for every endpoint, and you can't change it. A submission looks like this:

submission.created
{
  "id": "dlv_01k7v2d4n8r2p6s0w3y5b7c9e1",
  "type": "submission.created",
  "timestamp": "2026-10-23T12:00:00.000Z",
  "api_version": "2026-10-17",
  "data": {
    "submission_id": "sub_01k7v2c9t3e8m5q4x6z0a1b2c3",
    "form": { "id": "frm_xxxxxxxxxxxxxxxx", "name": "Contact" },
    "fields": { "name": "Ada", "email": "ada@example.com", "message": "Hello" },
    "meta": {
      "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
      "origin": "https://example.com",
      "referrer": "https://example.com/contact",
      "country": "GB",
      "subject": null,
      "reply_to": "ada@example.com"
    },
    "created_at": "2026-10-23T12:00:00.000Z",
    "truncated": false
  }
}

The form's values are in data.fields, keyed by the input name. A test from the dashboard sends an endpoint.test event instead, with data.endpoint_id and data.message and no fields. The full reference is on Webhooks.

Ikiform caps a delivery at 256 KB and empties fields above that, so every delivery fits inside Pipedream's 512 KB request limit.

Set it up

  1. Add an HTTP trigger

    1. Create a workflow and pick the HTTP / Webhook trigger.
    2. Keep the default response, which answers 200 as soon as the request arrives.
    3. Copy the endpoint URL. It looks like https://eo1a2b3c4d5e6f.m.pipedream.net.
  2. Add the endpoint in Ikiform

    • In the Ikiform dashboard, open Webhooks and choose Add endpoint.
    • Paste the Pipedream endpoint URL into URL.
    • Under Forms, pick one form, or All forms in the workspace.
    • Add a Description if you like, such as "Pipedream", then choose Add endpoint.
    • Copy the signing secret. It starts with whsec_ and is shown in full only once.

    There's no event picker. An endpoint gets submission.created for every accepted submission on the forms you chose, and endpoint.test when you send a test. A workspace on Pro can have up to 3 endpoints.

  3. Store the secret

    In Pipedream, open Settings > Environment Variables, add IKIFORM_WEBHOOK_SECRET and paste the whsec_ secret. Code steps read it from process.env.

  4. Verify the signature in a Node.js step

    Send one submission from your form so the trigger has an event to test with. Then add a Node.js code step, name it verify, and paste this:

    verify
    import crypto from "crypto";
    
    export default defineComponent({
      async run({ steps, $ }) {
        const { headers, body } = steps.trigger.event;
        const raw = JSON.stringify(body);
        const id = headers["webhook-id"];
        const timestamp = headers["webhook-timestamp"];
        const signatures = headers["webhook-signature"] ?? "";
        const age = Math.abs(Date.now() / 1000 - Number(timestamp));
        if (!id || !timestamp || !(age <= 300)) return $.flow.exit("Missing or stale Ikiform webhook headers");
    
        const key = Buffer.from(process.env.IKIFORM_WEBHOOK_SECRET.replace(/^whsec_/, ""), "base64");
        const expected = crypto.createHmac("sha256", key).update(`${id}.${timestamp}.${raw}`).digest();
        const valid = signatures.split(" ").some((entry) => {
          const [version, value] = entry.split(",");
          const received = Buffer.from(value ?? "", "base64");
          return version === "v1" && received.length === expected.length && crypto.timingSafeEqual(received, expected);
        });
        if (!valid) return $.flow.exit("Invalid Ikiform webhook signature");
        if (body.type !== "submission.created") return $.flow.exit(`Skipped ${body.type}`);
    
        return {
          deliveryId: body.id,
          form: body.data.form.name,
          submissionId: body.data.submission_id,
          fields: body.data.fields,
        };
      },
    });
    How the verify step checks an Ikiform signaturewebhook-idwebhook-timestampwithin 5 minraw bodyexact bytessecretbase64 after whsec_HMAC-SHA256of id.timestamp.bodywebhook-signaturev1,… one per secretany v1 match?returnfields for latersteps$.flow.exitno later steprunskeyexpectedyesnofrom the requestyours, never sentHow the verify step checks an Ikiform signaturewebhook-idwebhook-timestampwithin 5 minraw bodyexact bytessecretbase64 after whsec_HMAC-SHA256ofid.timestamp.bodywebhook-signaturev1,… one per secretany v1match?returnfields for latersteps$.flow.exitno later step runs
    1. Read webhook-id, webhook-timestamp and the raw body exactly as received.
    2. Reject the request if the timestamp is more than 5 minutes from now.
    3. Base64-decode the part of the secret after whsec_ and use it as the HMAC key.
    4. Compute HMAC-SHA256 of id.timestamp.body and base64-encode it.
    5. Compare it, in constant time, with each v1 value in webhook-signature. There are two while a secret rotates.
    6. If one matches: return, fields for later steps.
    7. If none match: $.flow.exit, no later step runs.

    Pipedream parses JSON bodies before your code runs and doesn't keep the original bytes. JSON.stringify of the parsed body gives the same bytes back, because Ikiform writes its body as compact JSON with JSON.stringify too. Header names arrive in lowercase.

    • A missing, stale or wrong signature ends the run with $.flow.exit, so no later step runs.
    • Test events end the run too, after the signature check, so a dashboard test confirms the secret without touching your apps.
    • The step returns the form name, ids and fields for later steps.
  5. Use the fields

    Add the actions you want and reference the step's return value:

    References
    {{steps.verify.$return_value.fields.email}}
    {{steps.verify.$return_value.fields.message}}
    {{steps.verify.$return_value.form}}

    Deploy the workflow so it runs on every delivery.

Ikiform only delivers to public https:// URLs on port 443. IP addresses, localhost, internal hostnames, workers.dev and pages.dev are refused when you save the endpoint.

Answer only after the check

With the default response, Pipedream answers 200 before your steps run, so Ikiform counts every delivery as delivered. If you want failed checks to show in Ikiform's delivery log and be retried, set the trigger to return a custom response from the workflow, and call this right after the signature check:

verify
await $.respond({ immediate: true, status: 204 });
When Pipedream answers Ikiform, with the default and a custom responsedefault responsecustom responseIkiformHTTP triggeranswers 200 at onceverify stepruns after the answerIkiformHTTP triggerwaits for $.respondverify step$.respond 204 after thecheckrun ends400, Ikiform retriespostthenpostthencheck failsWhen Pipedream answers Ikiform, with the default and a custom responsedefault responsecustom responseIkiformHTTP triggeranswers 200 at onceverify stepruns after theanswerIkiformHTTP triggerwaits for $.respondverify step$.respond 204 afterthe checkrun ends400,Ikiformretriespostthenpostthencheck fails
  1. With the default response, the HTTP trigger answers 200 as soon as the request arrives, and the verify step runs after that. Ikiform counts every delivery as delivered.
  2. With a custom response, the trigger waits. The verify step calls $.respond with 204 right after the signature check.
  3. If the check fails, the run ends before $.respond, Pipedream answers 400, and Ikiform retries.
In that mode, a run that ends before $.respond answers 400, which Ikiform retries. Keep the $.respond call before any slow step so Ikiform gets its answer within 15 seconds.

Test it

Open the endpoint in Ikiform and choose Send test. Ikiform sends one endpoint.test event with no retries, and the result shows up in the endpoint's delivery log within a few seconds. Then send a real submission from your form, or from a terminal:

Terminal
curl https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx \
  -H "Content-Type: application/json" \
  -d '{"name":"Ada","email":"ada@example.com","message":"Hello from curl"}'

A 201 with "ok": true means Ikiform stored it. The delivery appears on the endpoint page, and the message lands in Pipedream's event inspector.

If the form has a human check or allowed domains turned on, a post from curl is refused or marked as spam, and spam never sends a webhook. Test from your real site instead.

The test event ends at the verify step with Skipped endpoint.test. A real submission runs every step.

How delivery works

The receiver answersIkiform
Any 2xx within 15 secondsMarks the delivery succeeded
A redirectDoesn't follow it, and counts a failure
429 or 503 with Retry-AfterWaits that long, up to the next step of the schedule
410 GoneFails the delivery and disables the endpoint
Anything else, or no answer in 15 secondsRetries

Failed deliveries are retried 8 times over about 3 d 6 h, with the same webhook-id each time. If every delivery to an endpoint fails for 5 days, Ikiform disables it and emails the workspace owners.

Troubleshooting

ProblemWhat to do
Run ends with Invalid Ikiform webhook signatureIKIFORM_WEBHOOK_SECRET doesn't belong to this endpoint. Copy it again, or use Roll secret and update the variable.
Run ends with Missing or stale Ikiform webhook headersThe request didn't come from Ikiform, or you replayed an old event by hand. Send a new submission, or Retry now the delivery in Ikiform.
429 in the delivery logPipedream throttles very high request rates. Ikiform waits and retries.
400 timestamp_too_oldThe signature is more than 5 minutes old. Check the server clock, and don't replay old deliveries by hand; use Retry now on the delivery instead, which signs it again.
Endpoint is disabledEvery delivery failed for 5 days, the URL answered 410, or its hostname started resolving to a private address. Fix the cause, then turn Enabled back on and Retry now the failed deliveries.
No delivery at allCheck that the endpoint covers this form, that it's enabled, and that the submission wasn't marked as spam or held because the workspace is over its monthly limit.

Avoid duplicate records

Retries reuse the same delivery id. The verify step returns it as deliveryId; check it against a data store or a unique column before you create records.

Next steps

Want toRead
Post each submission to a channelSend form submissions to Slack
Run the check on your own serverReceiving webhooks
See every header and retry ruleWebhooks

Pipedream's own reference: triggers, Node.js steps and limits.

Stuck on a step, or want a guide for another tool? Email hello@ikiform.com.