TessoloTessoloDocs
Integrations

Push submissions to Slack, Feishu, Zapier and your own server

Outgoing integrations: one submission, several targets, with retries and a signature.

Every form submission can be pushed, the moment it arrives, to as many targets as you like: a Slack or Feishu group, a Discord channel, a Zapier or Make scenario, or an address on your own server.

Starter and above

Add a target

Get the receiving address

  • Slack — create an Incoming Webhook in your Slack app and copy its URL.
  • Feishu (Lark) — add a custom bot to the group and copy the webhook URL.
  • Discord — channel settings → Integrations → Webhooks → copy the URL.
  • Zapier / Make — start a Zap / scenario with a Webhook → Catch hook trigger and copy the URL it gives you.
  • Your own server — any https:// address that accepts a JSON POST.

Paste it under Settings → Outgoing integrations

Settings → Outgoing integrations → Add a target. Pick the type, paste the address, give it a name. Leave Only these forms empty to push every form on the site, or list the form names you want.

The Integrations and notifications group in site settings
Outgoing integrations sit next to form notifications in site settings.

Test it

Save, then click Test. A sample submission goes out immediately and you see the status the other side returned. If it fails, the error is shown on the row.

What gets sent

Slack, Feishu and Discord receive a readable message: the site and form name, then one line per field. Long answers are shortened.

Webhook, Zapier and Make receive JSON:

{
  "event": "form.submitted",
  "siteId": "…",
  "siteName": "Acme",
  "formId": "contact",
  "submissionId": "…",
  "pageId": "…",
  "data": { "Name": "Ada", "Email": "ada@example.com" },
  "createdAt": "2026-09-12T08:00:00.000Z"
}

Field names in data are the labels you gave the form fields. createdAt is when the visitor submitted, so retries carry the exact same body.

Each target chooses which events it wants. Besides Form submitted, a target can subscribe to Order paid (sites that sell with Stripe):

{
  "event": "order.paid",
  "siteId": "…",
  "siteName": "Acme",
  "orderId": "…",
  "status": "paid",
  "currency": "usd",
  "amountTotal": 12900,
  "amount": 129,
  "customer": { "email": "ada@example.com", "name": "Ada" },
  "items": [
    { "itemId": "…", "slug": "headphones", "title": "Headphones",
      "quantity": 1, "unitAmount": 12900, "unitPrice": 129 }
  ],
  "paidAt": "2026-09-12T08:00:00.000Z",
  "createdAt": "2026-09-12T07:59:00.000Z"
}

amountTotal and unitAmount are in the currency's smallest unit (cents); amount and unitPrice are the same numbers in whole currency. Chat targets get a short message with the amount, buyer, items and order number instead.

Two more events cover content and members:

  • Entry published (cms.item.published) — fires every time a collection entry goes live: publishing from the console, importing a CSV with publish on, a sheet sync with auto-publish, or the write API with publish: true. The body carries collectionSlug, collectionName, itemId, slug, title, the published data and publishedAt. A bulk import sends at most 50 of these per batch.
  • Member joined (member.joined) — fires the first time an email becomes a member of the site, whether they signed in with a link or bought something. The body carries memberId, email, name and source (signup or order).

Every request also carries these headers:

HeaderMeaning
x-tessolo-eventform.submitted, order.paid, cms.item.published or member.joined
x-tessolo-delivery-idSame value on every retry of one submission — use it to ignore duplicates
x-tessolo-attempt1, 2, …
x-tessolo-timestampWhen this attempt was sent (milliseconds)
x-tessolo-signatureOnly when a signing secret is set — see below

Retries

If the target doesn't answer with a 2xx, we try again after 5 minutes, 15 minutes, 1 hour, 6 hours and 24 hours. A 4xx (other than 408 and 429) is not retried — the address is wrong, retrying won't fix it.

After three failures in a row the workspace owner gets an email. Retries always use the address currently saved, so fixing a typo fixes the pending retries too. Disabling or deleting the target cancels them.

Your server should reply 200 as soon as it has stored the submission, and treat a repeated x-tessolo-delivery-id as already handled. A slow reply looks like a timeout and gets retried.

Verifying the signature

For Webhook, Zapier and Make targets you can set a signing secret. Every request then carries

x-tessolo-signature: t=1789000000,v1=5f1a…

where v1 is HMAC-SHA256(secret, t + "." + rawBody) in hex. To verify: split the header, recompute the HMAC over t + "." + body using the raw request body, compare in constant time, and reject if t is more than five minutes old.

The secret is shown once when you type it. After saving you only see its last four characters; type a new one to replace it.

Which plan

Outgoing integrations are on Starter and above and up. Starter allows three targets per site, Pro ten, Business as many as you need. If your plan later drops below the feature, pushes stop but nothing is deleted.

On this page