← Help Center
Developers12 min read

Webhooks: event notifications, inbound triggers, Zapier, and Shopify

Get a signed POST on your server when someone subscribes, unsubscribes, signs up, or buys. Event types, payloads, verifying signatures, retries. Plus inbound triggers from Zapier and Shopify.

Webhooks go two ways. Outbound: InfluencersKit posts an event to your server when something happens on your account. Inbound: Zapier, Shopify, or your app posts to InfluencersKit to start a trigger.

Before you start (outbound)

  • Your workspace is on Pro or Custom, and you're a workspace admin.
  • Your endpoint is reachable on the public internet over HTTPS, on port 443 or 8443, with a valid certificate.
  • It answers with any 2xx status within 15 seconds. Do slow work after you answer.

Add an endpoint

  1. 1Go to Settings → API keys & webhooks.
  2. 2Under Webhooks, enter the Endpoint URL, such as https://example.com/ik-webhooks.
  3. 3Tick the events you want.
  4. 4Select Add endpoint.
  5. 5Copy the signing secret from the dialog, starting whsec_, and store it with your server's other secrets. It's shown once.
  6. 6Select Send test event, then Show delivery log, and check the ping delivery succeeded.
  • URLs must use https://, have no user name or password, and point to a public address. Addresses such as localhost, 10.x.x.x or 192.168.x.x are refused, both when you save and before every send.
  • Redirects aren't followed. Use the final URL.
  • You can add up to 10 endpoints.

Event types

EventSent when
subscriber.createdA new address is added to your list: in the dashboard, by the API, or through a form, landing page or creator page. CSV imports don't send it.
subscriber.unsubscribedSomeone unsubscribes from everything (unsubscribe page or one-click), marks your email as spam, or is set to unsubscribed in the dashboard or by the API. data.reason is unsubscribe_page, one_click, complaint, edited or api.
form.submittedSomeone submits one of your live forms, new to your list or not. Sent at once, before any email confirmation: subscriber_status is pending_confirmation until they confirm. Not sent for an address you deleted after it opted out.
landing_page.submittedSomeone signs up on one of your live landing pages, new to your list or not. Same rules as form.submitted.
purchase.completedA Stripe payment is recorded for a digital good, course, coaching booking, or membership renewal.
purchase.refundedA sale is fully refunded in Stripe.
pingYou selected Send test event. You can't subscribe to it.
automation.rule_firedA behavior rule with a Send a webhook action fired. It goes only to the endpoint the rule names.
integration.forwardedAn integration trigger with a Forward to webhook action ran. It goes only to the endpoint the trigger names.

What we send

A POST with a JSON body. Every event has the same envelope: id (unique per event, the same on every retry), type, created (Unix seconds), and data.

{
  "id": "evt_6c1f0e2a9b7d4c3e8f1a2b3c",
  "type": "subscriber.created",
  "created": 1762164000,
  "data": {
    "id": "6727f0c2a1b2c3d4e5f60718",
    "email": "reader@example.com",
    "first_name": "Ada",
    "last_name": null,
    "status": "active",
    "tags": ["newsletter"],
    "source": "form:your-handle/your-form",
    "created_at": "2026-11-03T10:00:00Z"
  }
}

Other events' data:

Eventdata fields
subscriber.unsubscribedThe subscriber fields above, plus reason.
form.submittedform_id, form_slug, email, first_name, last_name, subscriber_id, subscriber_status
landing_page.submittedlanding_page_id, landing_page_slug, and the same subscriber fields as forms
purchase.completed, purchase.refundedid, product_type (digital_good, course_enrollment, coaching_booking, course_renewal), product_id, amount (in currency units, such as 19.99), currency, customer_email, payment_provider, created_at
pingmessage, endpoint_id

Headers

HeaderValue
IK-Signaturet=1762164000,v1=5257a869…: the timestamp and the signature. See below.
IK-Webhook-IdThe event id.
IK-Event-TypeThe event type.
IK-Webhook-TimestampUnix seconds when this attempt was sent.
IK-Webhook-Attempt1 for the first try, 2 for the first retry, and so on.
User-AgentInfluencersKit-Webhooks/1.0

Verify the signature

Check every request before you trust it. The signature is an HMAC-SHA256, in hex, of the timestamp, a full stop, and the raw request body, keyed with your whole secret including whsec_. Use the body exactly as received: parsing and re-serialising the JSON changes it.

Refuse a request whose timestamp is more than five minutes from your clock. Each attempt is signed with a fresh timestamp, so a captured request can't be replayed later.

After you roll the secret, the header carries two v1 values for 24 hours, one per secret. Accept the request if any one matches.

// Node.js with Express
import crypto from "node:crypto";
import express from "express";

const SECRET = process.env.IK_WEBHOOK_SECRET; // whsec_...

function verifyIkSignature(rawBody, header, secret, toleranceSeconds = 300) {
  if (!header) return false;
  let t = null;
  const signatures = [];
  for (const part of header.split(",")) {
    const [key, value] = part.trim().split("=");
    if (key === "t") t = Number(value);
    if (key === "v1") signatures.push(value);
  }
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest("hex");
  return signatures.some(
    (s) => s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected))
  );
}

const app = express();
app.post("/ik-webhooks", express.raw({ type: "application/json" }), (req, res) => {
  if (!verifyIkSignature(req.body, req.get("IK-Signature"), SECRET)) return res.sendStatus(400);
  const event = JSON.parse(req.body.toString("utf8"));
  // Skip event.id if you've handled it already, then queue your work.
  res.sendStatus(200);
});
app.listen(3000);

The same check in Python:

import hashlib
import hmac
import time

def verify_ik_signature(raw_body: bytes, header: str, secret: str, tolerance_seconds: int = 300) -> bool:
    if not header:
        return False
    t, signatures = None, []
    for part in header.split(","):
        key, _, value = part.strip().partition("=")
        if key == "t" and value.isdigit():
            t = int(value)
        elif key == "v1":
            signatures.append(value)
    if t is None or abs(time.time() - t) > tolerance_seconds:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, s) for s in signatures)

Retries

A delivery succeeds when your endpoint answers 2xx within 15 seconds. Anything else is a failure: another status, a redirect, a timeout, a TLS error, or a URL that now points to a private address.

A failed delivery is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 24 hours, each a little later at random. That's 8 attempts over about 42 hours. After the last one the delivery is marked failed.

Delivery statusMeaning
pendingWaiting for its first try or its next retry.
succeededYour endpoint answered 2xx.
failedEvery attempt failed. Select Resend to try again.
cancelledNot sent, because the endpoint was turned off or deleted first.

Delivery log

  • Select Show delivery log on an endpoint to see its last 50 deliveries: status, attempts, the last HTTP status and error, the start of your response, and the exact body sent.
  • Resend queues a finished delivery again, with the same event id and body.
  • Deliveries are kept for 30 days.

When an endpoint keeps failing

After 10 failed attempts in a row, spread over at least 24 hours, the endpoint is turned off. Its waiting deliveries are cancelled, the reason is shown on the endpoint, and your workspace admins get an email. Any success resets the count.

To start again, fix the endpoint, switch it back On, select Send test event, and resend the deliveries you need from the log.

Roll the signing secret

  1. 1Select Roll secret on the endpoint and confirm.
  2. 2Copy the new secret from the dialog.
  3. 3Put the new secret in your server within 24 hours. Until then, requests are signed with both.

Inbound triggers

Integration Triggers let other apps start actions in InfluencersKit. Adding an integration needs a plan with Integration Triggers. The people you act on must already be subscribers, matched by email address.

SourceURLHow it's verified
Zapierhttps://influencerskit.com/api/integrations/inbound/zapier?token=YOUR_TOKENWebhook token in the URL
Customhttps://influencerskit.com/api/integrations/inbound/custom?token=YOUR_TOKENWebhook token in the URL
Shopifyhttps://influencerskit.com/api/integrations/inbound/shopifyX-Shopify-Hmac-Sha256 with your signing secret

Set up a trigger

  1. 1Go to Automations → Integration Triggers and open the Integrations tab.
  2. 2Select Add integration. Enter a Name and choose a Platform.
  3. 3Open the Triggers tab and select New trigger. Choose the Integration, enter a Name and set Trigger event.
  4. 4In Actions, enter a JSON array, such as [{"type":"dispatch_event","eventType":"SUBSCRIBE"}].
  5. 5Set Status to Active and save.
  6. 6Open the Settings tab and copy the Zapier or Custom URL. The Webhook token there covers your whole account: keep it out of public code.

Send an inbound event

POST a JSON object. Fields are read from the top level only.

  • A valid token returns 200 with the body ok, whether or not a trigger matched. A wrong token returns 404 {"message":"Invalid token","status":404}.
  • For Zapier and Custom, the event name is the event field, then type. Without either it's zapier.webhook or custom.webhook.
  • For Shopify, the event name is the X-Shopify-Topic header, such as orders/create. Save the Shopify signing secret first: without it every Shopify event is refused with 401 Shopify webhook secret not set.
  • Trigger event must equal the event name, ignoring case. Leave it blank, or use *, to match every event.
curl -X POST "https://influencerskit.com/api/integrations/inbound/custom?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"event":"order.paid","email":"reader@example.com","plan":"annual"}'

Inbound actions

Success in Execution logs means the trigger ran, not that anything happened. A log shows Success when the address isn't a subscriber, and for actions that do nothing. Ignore the inbound URL shown for Stripe on the Integration Triggers page: it doesn't exist. A Stripe trigger runs only for checkout.session.completed, from your own payment webhook. Retry policy, Rate limit / min, and the notify options on that Settings tab save but have no effect.

TemplateJSONWhat happens
Dispatch event{"type":"dispatch_event","eventType":"SUBSCRIBE"}Enrolls the subscriber in email into your active Welcome Series sequences. This is the only action that changes anything today.
Dispatch event{"type":"dispatch_event","eventType":"CUSTOM_EVENT"}No visible effect. This is the default template.
Queue email{"type":"queue_email","templateId":"…"}No email goes out. The job is created, then fails.
Add tag{"type":"add_tag","tag":"…"}Does nothing. No tag is added.
Forward webhook{"type":"webhook_forward","url":"…"}Does nothing. Use an outbound endpoint above instead.

Frequently asked questions

Which plan includes webhooks?

Pro and Custom. On other plans nothing is sent, and endpoints can't be added or turned on.

Can the same event arrive twice?

Yes, occasionally, for example when your server timed out but still processed it. Use the event id to skip ones you've already handled.

Do events arrive in order?

Not guaranteed. Use the created time in each event, and read the current state from the API if order matters.

Does a CSV import send subscriber.created?

No. Imports add subscribers in bulk without events, so an import can't flood your endpoint.

Are failed inbound events retried?

No. An inbound trigger runs once when the request arrives. Only the sender's own retry rules apply. Outbound events are retried, as described above.

Ready to try it?

Open your dashboard and follow along — or create a free account.