Send events from your site to Slack
- Trigger
- POST /relay
- Lands in
- The relay log, plus any channel you route to
- Setup
- 3 min
- Needs
- Anything that can send an HTTP POST

New signup from Dunder Mifflin
[email protected] · via My Site, Signup Page
- Type
- signups
- Plan
- pro
- Campaign
- spring-launch
- Submitted at
- 11:04 AM
The same POST shape carries every event type. Where each one lands, and how it batches, is decided per type in the dashboard.
Invite @Via to the Slack channel where you want webhooks to appear. Without this, Viably accepts your webhook but cannot post to the channel.
In your Viably dashboard, create a relay endpoint and assign it to one or more Slack channels. You receive a URL and a secret token.
From your site or service, send a POST request with your event data. Include a type field to route it to the right Slack message template.
Viably formats the payload into a clean Block Kit message and posts it to your channel, immediately or batched on a schedule you choose. Stripe charge and subscription events are combined into a single summary card.
Environment variables
After creating an endpoint in the dashboard, add these to your project's environment.
Both values are shown in the dashboard immediately after creating an endpoint. Your token is only displayed once, so store it somewhere safe.
TypeScript types
Drop this into your project as viablyTypes.ts. Each event type carries its own recommended fields, and Viably surfaces a hint in Slack when a recommended field is missing.
Sending a payload
A minimal helper and three common usage examples. Any additional fields you include beyond the recognised ones appear in the Slack message as extra context.
Event types
The type field routes the event to the matching Slack message template. If omitted or unrecognised it falls back to others. Viably adds an inline hint to the Slack message when a recommended field is absent.
No payload can declare itself one of these four, on any endpoint, no matter how the field is spelled. Each one means something specific enough that letting any sender claim it would be a problem: transaction counts toward your revenue figures, ocr is Viably’s own document pipeline, and inbox and reply decide which side of a conversation an email sits on. Send one of these four as your type to a relay endpoint that was not built for it, and Viably treats it exactly as it treats any other unrecognised value. The event still reaches Slack under others, quietly, with no error returned and no amount or contact captured from it.
A leadgen, signups, contactform, or transaction event does more than post to Slack. It creates or updates a contact, keyed on the email address, so the same person’s enquiry, signup, and payment gather on one record you can work, answer, and attribute revenue to. See the Lightweight CRM guide for what that record does for you.
All three are optional on leadgen, signups, contactform, and transaction, and all three are worth sending. email is the identity key your Contacts list is built on, so a lead captured in June and a payment in July resolve to the same person, and your periodic report can tell you how much revenue that lead eventually brought in. name and company are what turn that row into something a human can act on. Without them you still get the Slack message, but the contact arrives as a bare address with nobody to greet.
A human-readable label for the origin of the event, such as "My Site — Contact Page". Viably uses this for footer attribution on every message. If omitted, it falls back to the HTTP Referer or Origin header, then to the domain you set on the endpoint in the dashboard.
An ISO 8601 timestamp for when the event occurred, such as new Date().toISOString(). Viably formats it using your workspace’s default timezone and displays it as a Submitted at field in the Slack message. If omitted, no timestamp is shown.
Duplicate events
event_id is optional, and it is the single most useful optional field on this page. It is your own identifier for the event a payload describes, and sending it is how you tell Viably that two deliveries are one event.
A webhook retried after a timeout, a proxy that replays a request, or a form somebody submitted twice all arrive as two deliveries of one thing. Without an id to match them on, that becomes two lines on the Slack card, two touches on the contact record, and two captures billed against your plan. It also moves numbers you make decisions from: a contact with two events reads as somebody who came back, so the same person shows up under engaged rather than dormant in your acquisition figures. Send event_id and Viably remembers it for 24 hours, so the second delivery is dropped before any of that happens.
Nothing breaks, and payments are already covered: Viably collapses them on order_id, and Stripe events on Stripe’s own event id. For everything else it falls back to comparing the payload, and treats an identical body carrying the same email address inside ten minutes as one event. That window is deliberately short, because two identical bodies a week apart are usually one person genuinely signing up again, and a fallback cannot tell the difference. An event_id can, which is the whole reason to send one.
The one rule: if the same event were relayed twice, both calls have to produce the same string. Derive it from the row the event created, from the event’s own timestamp, or from a hash of the submission. A database id is ideal, so signup:8814 is a good id and needs no more thought than that.
Calling crypto.randomUUID() as you post the webhook is worse than sending nothing at all. A supplied event_id is taken as your word on which deliveries are the same event, so a fresh one per request means no two deliveries ever match and the payload comparison above never runs. A constant or a value shared between event types fails in the other direction and makes an endpoint discard its own traffic for 24 hours. Both are silent, though every collapsed delivery is written to your relay log as a dropped row naming the key it matched, so nothing disappears without a trace.
Some events have nothing durable behind them. A newsletter form that posts straight to Viably has no subscription record to borrow an id from, and there the honest key is the person and the day: newsletter:[email protected]:2026-07-30. Subscribing twice in an afternoon is one subscription, coming back in March is a second, and that is exactly what the id then says. Avoid making it stable forever in this case, because Viably stores your id on the contact event under a unique index: somebody genuinely resubscribing next month would post to Slack and then be dropped from your CRM without a word.
Custom event types
The six types above are the ones Viably ships with, and they are not the limit. Send type as any lowercase value that starts with a letter and uses only letters, digits, and underscores, up to 64 characters, and it works the first time you send it. support_ticket, plan_upgraded, and nps_response are all valid without asking anyone for permission first.
The first delivery of a type nobody has sent before reaches Slack immediately, formatted the same generic way others is, and it registers itself in your workspace’s event catalog at the same time. What it does not do on its own is create or update a contact. An admin has to open that catalog and decide what the type means, whether it should capture a lead, a signup, or a customer, and whether your team is expected to reply, before it does anything beyond posting a message. A string a payload happens to carry should never be able to start writing into your Contacts or your revenue figures before a person on your team agreed that it should.
There is rarely a reason to create a second endpoint just because you want to track a second kind of event. A contact form, a signup flow, and a handful of custom milestones your product wants tracked, such as an onboarding step finished or a referral made, can all post through the same token, and Viably tells them apart by type alone. See Routing multiple event types for how to send different types to different channels without touching your integration code.
That is a generous ceiling for how many kinds of event one product realistically emits, and it mainly exists to stop a runaway sender, such as a bug that mints a fresh type name on every request, from filling the catalog and the batching queue behind it. Once a workspace reaches that ceiling, a further novel type name is treated as unrecognised and falls back to others, the same as a malformed one, so an integration never breaks outright over it.
Reporting
Every event you deliver is recorded for reporting. In the Month in Review area of your dashboard, Viably charts your webhook volume by category, so you can follow how signups, transactions, and other event types move through the month.
Available on Viably+. Your relay history is kept for several months, so the trend chart in Month in Review can reach back across earlier months when you want to compare periods or follow a longer pattern. Free workspaces keep the current billing period in their charts.
Channel override
By default, events are posted to the Slack channels you configured on the endpoint. You can override this per event by including a channels array in your payload. If Viably cannot access any of the override channels, it falls back to the endpoint defaults automatically.
Channel IDs can be found by right-clicking a channel in Slack, choosing View channel details, and copying the ID at the bottom.
Routing multiple event types
One endpoint token is not limited to one kind of event. Send signups and a custom type like support_ticket through the same token, and both still reach Slack, because type alone decides which template renders an event. What differs, if you want it to, is not in your code at all.
Open your endpoint and you will find a panel by that name. Once a type has arrived at least once, an admin can point it at its own Slack channels, its own batching interval, or its own task-assignment rule, independent of the endpoint’s own defaults and independent of every other type sharing the token. support_ticket can post immediately to a support channel while signups keeps batching every six hours into a growth channel, and changing one never touches the other, even though both arrived through the same POST shape and the same bearer token.
channels is something your code decides, request by request, and it always wins over everything else. Where each event lands is something an admin decides once, per type, from the dashboard, and it is what a type does by default when your payload does not name a channel override itself. Most integrations never need channels at all once routing is set up this way, since the decision already lives on the type rather than needing to be repeated on every send.
A contact form, a signup flow, a payments rail that needs its own adapter, and a handful of custom milestones can coexist as separate tokens if that is simpler for your team to reason about, though they do not have to. If the only reason you were about to create a second endpoint was to send a new kind of event somewhere else in Slack, routing that type from the dashboard usually gets you there with one fewer secret to carry in your codebase.
Auto-create tasks
Every endpoint has an optional Spawn a task toggle in the dashboard. When it is on, each delivered event also creates a task, posted as a claimable card threaded under the Slack notification, so events that need follow-up always have an owner.
- Assign it, or leave it up for grabs. Route the task to a specific teammate, or leave it unassigned so anyone can claim it in Slack.
- It is a real task. The card carries Claim and Mark Done buttons, and the task shows up on your board, in the Weekly Digest, and in follow-ups, identically to a task created by a clipboard reaction.
- Non-Stripe endpoints spawn a task on every single non-batch delivery, one per lead, contact-form fill, signup, deploy, and so on.
Point a Stripe relay at your billing events, turn on Set to cancel, and assign your customer success manager. Every time someone schedules a cancellation, a recovery task lands in Slack with their name on it, and churn becomes a worklist rather than a surprise.
Stripe Integration
Viably can receive events directly from Stripe and post them to Slack. Because Stripe webhook destinations post to a plain URL, you must use the query-param token format rather than the Bearer authorization header.
- 1Create a relay endpoint
In the Viably dashboard, create a new relay endpoint with Stripe Integration enabled. Once created, open the token dialog and copy the Stripe or query-param URL. It looks like
https://hq.viably.app/relay?token=…. - 2Create a webhook destination in Stripe
In your Stripe Dashboard, go to Developers → Webhooks → Add destination. Paste the Viably query-param URL as the Endpoint URL. Select these two events:
charge.succeededandcustomer.subscription.updated.charge.succeededin particular captures all your successful transactions in Slack, whether they originated from Stripe Checkout or from your own custom integration using Stripe's APIs. Viably looks for this event's payload to extract the correctamount,currency, anddescriptionto build your transaction messages in Slack.
You should not send any other Stripe events to this Webhook Relay endpoint, since that can cause an unexpectedly heavy load of messages in your workspace, and even double notifications or double-counting in revenue tracking. - 3Copy the signing secret
After saving, Stripe displays a signing secret beginning with
whsec_. Copy it to complete your setup on Viably. If you miss it, you can reveal or roll it from the destination details page. - 4Complete setup in Viably
Back in the Viably dashboard, click Edit on the relay endpoint. Paste the
whsec_…value into the Stripe webhook secret field and save. Viably will now verify every incoming Stripe request and reject anything unsigned. - 5Test it
Use Stripe's Send test event button on the destination page to fire a sample event. Check your configured Slack channel, where you should see a formatted message within a few seconds.
Stripe webhook destinations send a plain POST and do not support custom Authorization headers. The ?token=… format is the Viably-supported alternative for these integrations, and both URLs are shown in the token dialog after endpoint creation.
Campaign attribution
Most teams can tell you how many people a campaign reached. Far fewer can tell you what those people went on to spend, because the campaign is recorded in the ad platform and the revenue is recorded somewhere else entirely, and nothing joins the two without somebody exporting both and reconciling them by hand. Viably closes that gap as a side effect of capturing the contact, and it asks nothing of you beyond the page URL your form already knows.
Include current_page_url in your payload and Viably reads utm_source, utm_medium, utm_campaign, utm_content, utm_term, utm_id, and utm_author off the query string your ad platform appended when the visitor clicked. If you post your own JSON and the URL is not to hand, send those fields directly and they are read the same way. Nothing else changes about your integration.
The click and network ids an ad platform adds, such as fbclid, gclid, and the hsa_* cluster, are captured alongside the campaign rather than dropped. Each one shows up as its own entry under Captured data on the contact, ready to promote into a field the moment you want to match it against your ad platform’s own reporting, so a new parameter from a new platform needs nothing from us to be waiting for you.
Attribution is recorded on first touch and stays there for the life of the contact. Someone who arrives through a spring promotion and buys again in November is still counted towards the spring campaign, so a campaign that brings loyal customers goes on earning its reputation long after the spend stopped. That is usually the difference between a channel that looks expensive on the day and one that turns out to have been the best money you spent all year.
Open anyone on the Contacts page and the campaign that brought them sits directly beneath the page that found them, so an enquiry landing in Slack this morning becomes an enquiry you can trace back to the ad that paid for it. For the wider view, every contact export carries UTM source, UTM medium, and UTM campaign as columns beside the source, which is enough to pivot a spreadsheet by campaign and see what each one went on to earn without touching another tool. Contacts captured before you started tagging links carry empty cells, because the columns are fixed so that a file you built a pivot on keeps working next month.
One landing page can run a dozen campaigns over a year, and one campaign can drive traffic to several pages. Folding the two together would file every campaign as its own channel and scatter a page’s revenue across a dozen rows that each look too small to matter. Keeping them as separate fields means you can ask which page converts and which campaign paid off, and get an honest answer to both.
Beyond the recognised fields, you can include anything in the payload, whether that is city, plan, or your own custom metadata. Viably renders them as additional context in the Slack message so nothing is lost. One of them earns special treatment: current_page_url is also read for campaign tags, which is covered above.
Nothing. The endpoint is hosted by Viably and live the moment you create it, so a single POST from your site or backend is the whole integration. There is no server to deploy, monitor, or keep patched.
Yes, that is the design. The type field in your payload picks the Slack template and decides how the event is treated, so one endpoint serves your contact form, your signups, and your deploys without you managing three integrations.
Person events build contacts. A lead, signup, contact form, or transaction carrying an email address creates or updates a contact record, which is how the same person's enquiry, signup, and payment end up on one timeline with revenue attributed.
Yes. Each endpoint has default channels, and any individual payload can override them with a channels array. If Viably cannot reach an override channel it falls back to the defaults rather than dropping the event.
Create your first endpoint from the Webhook Relay page. It takes one POST to see an event land in Slack.
Add Viably to Slack