Webhooks need a plan with API access, like API keys. Admins and owners manage endpoints in the dashboard, in the Webhooks tab next to API Keys. API keys cannot manage them.
Webhooks
Get a signed HTTP request the moment something changes in your Creator Hub, instead of polling the API.
Webhooks tell your own systems when something changes in your organization: a creator replies in chat, an application arrives, a billing window becomes due, a payout reaches a creator. viral.app sends each event as an HTTPS POST to endpoints you add in the dashboard. Your CRM, helpdesk, finance tooling, AI agent or n8n, Zapier and Make workflows can then act on it right away.
Deliveries follow Standard Webhooks, so any Standard Webhooks library verifies them. The API reference lists every event under Webhook Events, with the full payload schema. API and automation covers API keys and access.
Webhooks tab
Add endpoints, choose their events, send a test event and watch every delivery.
Verify signatures
Check every request in a few lines of TypeScript, Python or plain Node.js.
Event types
Chat, creators, campaigns and briefs, assignments, payouts, jobs and applications.
Add an endpoint
Open Webhooks under Organization → API and choose Add Endpoint. Enter the URL, an optional description, and the events it should receive. Choose All events, including future ones to also receive event types viral.app adds later.
viral.app then shows the endpoint's signing secret (whsec_…) once. Store it in your secrets manager. Admins and owners can reveal it again later with Reveal Secret in the endpoint's menu, and every reveal is recorded in the action log.
- URLs:
https://with a public host name. IP addresses, local and internal names, and viral.app itself are refused. Treat a URL as a credential: anyone who knows it can send you requests, which is why every request is signed. - Limits: 20 endpoints per organization, URLs up to 2,048 characters.
- Automations: to start an n8n, Zapier or Make workflow, add the URL of its webhook trigger as an endpoint.
Endpoints, their secrets and their deliveries are managed only in the dashboard. The API has no routes for them: the API reference documents what viral.app sends, under Webhook Events.
What a delivery looks like
Every event is one POST with a JSON body:
POST /webhooks/viral-app HTTP/1.1
content-type: application/json
user-agent: viral.app-Webhooks/1.0
webhook-id: whevt_A1b2C3d4E5f6
webhook-timestamp: 1790332496
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
{
"id": "whevt_A1b2C3d4E5f6",
"type": "application.submitted",
"timestamp": "2026-09-25T12:34:56.789Z",
"organizationId": "org_A1b2C3d4E5f6",
"actor": { "type": "creator", "surface": "ui", "userId": "user_A1b2C3d4E5f6", "apiKeyId": null, "impersonated": false },
"data": {
"application": {
"id": "orgjapp_A1b2C3d4E5f6",
"jobId": "orgjob_A1b2C3d4E5f6",
"status": "open",
"source": "applied",
…
}
}
}
idis the event id, the same as thewebhook-idheader. It stays the same on every retry, resend and recovery.timestampis when the change happened. Thewebhook-timestampheader is the time of this delivery attempt.actorsays who caused the event: a member of your team (user), an API key (api_key, withapiKeyId), a creator, or viral.app itself (system).surfacesays where the change came in: the dashboard (ui), the API (api), the Copilot (copilot) or viral.app (system).datauses the same field names and money units as the matchingGETendpoint. Issued payouts read likeGET /payouts/paidrows (amountin major units withprecision).payout.duereads like aGET /payouts/duerow (payoutAmountin minor units withpayoutPrecision). Every payout event writes billing-period bounds (billingPeriodStart,billingPeriodEnd) with a+00:00offset, like the Due list (2026-09-01T00:00:00.000+00:00): parse them as timestamps rather than comparing them as strings with bounds from other API routes, which may useZ. Payloads never contain creator email addresses or bank details.
Verify signatures
webhook-signature holds one or more space-separated v1,<signature> entries. Each is a base64 HMAC-SHA256 of <webhook-id>.<webhook-timestamp>.<raw body>, keyed with your secret. A request is genuine when any entry matches. During a secret rotation, both the old and the new secret sign it.
Verify the raw request body, before any JSON parsing: re-serialized JSON no longer matches the signature. Reject requests whose webhook-timestamp is more than five minutes away from your clock; every retry is signed again with a fresh timestamp, so a genuine request is never stale.
With the official standardwebhooks package in TypeScript:
import { Webhook } from "standardwebhooks"
const webhook = new Webhook(process.env.VIRAL_APP_WEBHOOK_SECRET!) // "whsec_…"
export async function POST(request: Request) {
const body = await request.text()
let event: { id: string; type: string; data: unknown }
try {
event = webhook.verify(body, Object.fromEntries(request.headers)) as typeof event
} catch {
return new Response("Invalid signature", { status: 401 })
}
await queue.add(event) // Process it after answering.
return new Response(null, { status: 204 })
}
In Python with standardwebhooks:
from standardwebhooks.webhooks import Webhook
webhook = Webhook(os.environ["VIRAL_APP_WEBHOOK_SECRET"]) # "whsec_…"
@app.post("/webhooks/viral-app")
async def receive(request: Request):
body = await request.body()
try:
event = webhook.verify(body, dict(request.headers))
except Exception:
return Response(status_code=401)
queue.enqueue(event)
return Response(status_code=204)
Without a library, in Node.js:
import { createHmac, timingSafeEqual } from "node:crypto"
export function isGenuineWebhook(
rawBody: string,
headers: Record<string, string | undefined>,
secret: string,
): boolean {
const id = headers["webhook-id"]
const timestamp = headers["webhook-timestamp"]
const signatures = headers["webhook-signature"]
if (!id || !timestamp || !signatures) return false
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64")
const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest()
return signatures.split(" ").some((entry) => {
const [version, signature] = entry.split(",")
if (version !== "v1" || !signature) return false
const received = Buffer.from(signature, "base64")
return received.length === expected.length && timingSafeEqual(received, expected)
})
}
viral.app has no fixed outbound IP addresses, so an IP allowlist cannot tell our requests apart. The signature can.
Respond, retries and ordering
Answer with any 2xx status within 15 seconds. Everything else counts as a failure and is retried: other statuses, timeouts, connection errors, and redirects, which are never followed. Do the work after answering, from a queue of your own: a slow receiver collects timeouts and retries.
A failed delivery is tried again 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours, 14 hours, 20 hours and 24 hours after the previous attempt, each step with up to 20% of random spread. That is ten attempts over about three days. A Retry-After header on a failed response (seconds or an HTTP date) postpones the next attempt, up to 24 hours. A 410 Gone answer disables the endpoint right away.
Deliveries are at least once and unordered:
- Duplicates. A delivery can arrive twice, for example when your answer got lost. Store the
webhook-idof what you processed and skip repeats. - Order. Events can arrive in any order. Order them by
timestamp, or by the resource's own version: a campaign's revision, a payout window'sgeneration, a record'supdatedAt.
When deliveries fail
While an endpoint keeps failing, viral.app holds back its other deliveries and tries one of them every few minutes. The rest follow as soon as one succeeds, so a receiver that comes back isn't flooded at once.
- Emails. Owners and admins get one email a day at most when an endpoint's retries gave up on a delivery, and one when viral.app disables an endpoint.
- Automatic disable. An endpoint that failed for 5 days without a single success, and failed in the last 24 hours, is disabled. So is one that answered
410. - Delivery log. The endpoint's detail page lists every delivery of the last 30 days with each attempt's status, duration and the first 4 KB of your response.
Pause, resume and recover
Pause Endpoint in the endpoint's menu stops its deliveries: pending ones are canceled, and events that happen while it is paused are not recorded for it. A disabled endpoint behaves the same. Choose Resume Endpoint once your receiver works again.
To send what the endpoint missed, choose Recover above its delivery log (or Recover Deliveries in its menu) and pick how far back. This sends every delivery of that endpoint created since then again: the ones that failed, and the ones a pause or disable canceled. The limit is 30 days back, and one recovery re-sends at most 5,000 deliveries, the oldest first. Recovered deliveries keep their original webhook-id, so your duplicate check still works. To send a single delivery again, open it in the delivery log and choose Resend.
Events that happened while no active endpoint subscribed to them were never recorded and cannot be recovered. To catch up after a gap, read the current state from the API, for example GET /payouts/due.
Rotate a secret
Rotate Secret in the endpoint's menu issues a new secret and shows it once:
- Keep the old secret for 24 hours (the default). Every delivery is signed with both secrets meanwhile, so you can switch your receiver without losing a request.
- Retire the old secret now, for a leaked secret. Deliveries fail verification until your receiver uses the new one.
Test an endpoint
Send Test Event in the endpoint's menu sends one webhook.test event to that endpoint only. Use it to check that your receiver verifies the signature and answers with a 2xx. Test events appear in the delivery log but never count toward the endpoint's health.
Event types
Subscribe to a list of types, or to all of them, including types added later. The event picker describes each type, and the API reference documents every payload under Webhook Events. Changes to a payload only ever add fields; a breaking change would be a new event type.
Chat
An AI agent that answers creators subscribes to chat.message.received. An integration that mirrors the chat and also sends messages through the API should skip chat.message.sent events whose apiKeyId is its own key, or it answers itself.
Creators
Campaigns and briefs
Assignments and billing periods
An assignment created with a start in the past reports the billing period it is in and the periods that began or completed today or in the three days before. Earlier periods are not replayed; their unpaid windows arrive as payout.due.
Payouts
Windows that were already due when you subscribed to payout.due are not reported; GET /payouts/due lists them.
Jobs and applications
Plans, holds and privacy
- Plan. Webhooks need a plan with API access. If your plan loses it, viral.app disables your active endpoints, and they resume on their own once API access is back. Events in between are not recorded.
- During a payout hold. Deliveries continue while creator payouts are overdue. During a hard hold you can still pause and delete endpoints, rotate and reveal secrets, and see every endpoint and delivery. Creating, changing, resuming, testing, recovering and resending wait until the hold is lifted.
- Deleted creators. When a creator deletes their viral.app account, stored events that name them are replaced with a redacted placeholder, and their deliveries that have not succeeded end as
skipped: neither retries, Recover nor Resend send the placeholder. Events your endpoints already received cannot be recalled.