Send Webhooks from Your Automations

The webhook action sends your order and customer data to any URL you choose, as a JSON POST request. Use it to feed your own backend, a fulfillment system, a data warehouse, or any tool Funnelish doesn’t integrate with directly.

Webhooks are available in both global and funnel workflow automations.

Add a webhook to an automation

  1. Open your automation (global: Automations in the top navigation; funnel: the funnel’s Automations section)

  2. Add the Webhook action where you want it in the flow

  3. Paste your endpoint URL and save

When the automation reaches the webhook step, Funnelish sends the request and immediately continues to the next action. Your endpoint’s speed never slows the flow down.

How delivery works

  • Method: POST with Content-Type: application/json

  • Timeout: 120 seconds

  • Retries: none. Each event is delivered once. If your endpoint is down or returns an error status (400+), the delivery is logged on our side but not retried

  • Any 2xx response counts as success

Because there are no retries, keep your endpoint fast and reliable: respond with 200 first, process the data after (queue it, write it, then work on it).

Securing your endpoint

Requests are plain JSON with no signature headers, so authenticate with the URL itself:

  • Use HTTPS

  • Put a secret token in the URL, e.g. https://api.example.com/hooks/funnelish?token=YOUR-SECRET, and reject requests that don’t carry it

  • Don’t reuse that URL anywhere else

The payload

{
  "event": "buy",
  "id": 18234455,
  "user_id": 41871,
  "optin_email": "[email protected]",
  "payer_email": "[email protected]",
  "first_name": "Jane",
  "last_name": "Doe",
  "phone": "+15551234567",
  "created_time": "2026-08-09T14:03:22Z",
  "optin_time": "2026-08-09T14:01:10Z",
  "shipping_address": "123 Main St",
  "shipping_city": "Austin",
  "shipping_state": "TX",
  "shipping_zip": "78701",
  "shipping_country": "US",
  "address": null,
  "city": null,
  "state": null,
  "zip": null,
  "country": null,
  "billing_name": "Jane Doe",
  "avatar": null,
  "ip": "203.0.113.7",
  "extra_data": null,
  "is_auto_optin": false,
  "updated_at": "2026-08-09T14:03:22Z",
  "meta": {
    "utm_source": "facebook",
    "utm_campaign": "summer-launch"
  },
  "products": [
    {
      "id": 88231,
      "order_id": 5512034,
      "name": "Hair Growth Serum",
      "amount": 49.0,
      "qty": 1,
      "variant_name": "3-pack",
      "source": "Funnelish Pay • Stripe",
      "currency": "USD",
      "transaction_reference": "pi_3PxYz...",
      "test_mode": false
    }
  ]
}

Field reference:

Field Type Notes
event string What happened. See event values below
id number Customer ID (not the order ID)
optin_email / payer_email string The email captured at opt-in / at payment. Prefer these over the email field, which only appears on funnel-scoped automations
first_name, last_name, phone string or null Customer contact fields
shipping_*, addresscountry, billing_name string or null Addresses as captured at checkout. Fields the customer never filled are null
meta object All custom metadata stored on the customer, as key/value strings (UTMs, custom fields). Omitted when empty
products array One entry per product in the triggering order
products[].order_id number The Funnelish order ID. Same for all products of one order
products[].amount number Unit price in currency
products[].source string Payment source: "Funnelish Pay • <Gateway>", "PayPal Plugin", or "Clickfunnels"
products[].transaction_reference string The payment gateway’s transaction reference
products[].test_mode boolean true for test-mode orders. Filter these out in production

Common event values: buy (purchase), optin, subscr_signup, subscr_cancel, subscr_eot (subscription completed), subscr_failed, sub_charge (recurring charge), refund, new_case (dispute), fulfilled, tracking_info, canceled. Which one you receive depends on the trigger that started the automation.

Receiving webhooks: code samples

Each sample checks the secret token, responds 200 immediately, and hands the payload to your own logic.

Node.js (Express)

const express = require("express");
const app = express();
app.use(express.json());

app.post("/hooks/funnelish", (req, res) => {
  if (req.query.token !== process.env.FUNNELISH_HOOK_SECRET) {
    return res.sendStatus(401);
  }
  res.sendStatus(200); // respond first — there are no retries

  const { event, optin_email, products = [] } = req.body;
  if (event === "buy" && !products.some((p) => p.test_mode)) {
    const total = products.reduce((sum, p) => sum + p.amount * p.qty, 0);
    console.log(`New order from ${optin_email}: ${total} ${products[0]?.currency}`);
    // queue fulfillment, update your DB, etc.
  }
});

app.listen(3000);

Go

package main

import (
	"encoding/json"
	"log"
	"net/http"
	"os"
)

type Product struct {
	OrderID  uint64  `json:"order_id"`
	Name     string  `json:"name"`
	Amount   float64 `json:"amount"`
	Qty      int     `json:"qty"`
	Currency string  `json:"currency"`
	TestMode bool    `json:"test_mode"`
}

type Payload struct {
	Event      string            `json:"event"`
	OptinEmail string            `json:"optin_email"`
	Meta       map[string]string `json:"meta"`
	Products   []Product         `json:"products"`
}

func main() {
	http.HandleFunc("/hooks/funnelish", func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Query().Get("token") != os.Getenv("FUNNELISH_HOOK_SECRET") {
			http.Error(w, "unauthorized", http.StatusUnauthorized)
			return
		}
		var p Payload
		if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
			http.Error(w, "bad payload", http.StatusBadRequest)
			return
		}
		w.WriteHeader(http.StatusOK) // respond first — there are no retries

		go func() {
			if p.Event != "buy" {
				return
			}
			for _, prod := range p.Products {
				if prod.TestMode {
					continue
				}
				log.Printf("order %d: %s x%d — %.2f %s (from %s)",
					prod.OrderID, prod.Name, prod.Qty, prod.Amount, prod.Currency, p.OptinEmail)
			}
		}()
	})
	log.Fatal(http.ListenAndServe(":3000", nil))
}

PHP

<?php
if (($_GET['token'] ?? '') !== getenv('FUNNELISH_HOOK_SECRET')) {
    http_response_code(401);
    exit;
}

$payload = json_decode(file_get_contents('php://input'), true);
if (!$payload) {
    http_response_code(400);
    exit;
}

// Respond first — there are no retries.
http_response_code(200);
if (function_exists('fastcgi_finish_request')) {
    fastcgi_finish_request();
}

if (($payload['event'] ?? '') === 'buy') {
    foreach ($payload['products'] ?? [] as $p) {
        if (!empty($p['test_mode'])) {
            continue;
        }
        error_log(sprintf(
            'Order %d: %s x%d — %.2f %s',
            $p['order_id'], $p['name'], $p['qty'], $p['amount'], $p['currency']
        ));
    }
}

Practical tips

  • Test before going live. Point the action at a request inspector like webhook.site, run a test-mode order through your funnel, and look at the real payload for your setup. Test orders arrive with "test_mode": true on each product.

  • Dedupe by order_id + event. One triggering event produces one webhook, but if the same customer’s order can reach your endpoint through more than one automation, use those two fields as your idempotency key.

  • Don’t parse email. It exists only on funnel-scoped automations. optin_email and payer_email are always there.

  • Null means never captured. Address and name fields are null when the customer didn’t reach the step that collects them (e.g. an opt-in event before checkout).

  • Need retries or transforms? Put a queue between Funnelish and fragile downstream systems: receive, store, return 200, process from the queue. Or send the webhook to Zapier/Make and let them handle delivery, see the Zapier integration.

Keep learning