Lucra POSTs an event to your server when something changes, so you don't have to poll.
Set up
Add a webhook URL when you create a key. Lucra shows a signing secret (whsec_…) once.
Events
Events name what changed and the IDs involved; fetch the details from the API. Ignore types you don't handle.
| Event | When | data |
|---|---|---|
application.created | A creator applies to a program | program, application |
application.approved | An application is approved | program, application |
application.rejected | An application is rejected | program, application |
application.withdrawn | A creator withdraws an application | program, application |
submission.created | A creator submits work | program, submission |
submission.approved | A submission is approved | program, submission |
submission.revision_requested | A reviewer asks for changes | program, submission |
submission.rejected | A submission is rejected | program, submission |
submission.live | A submission goes live | program, submission |
file.ready | An uploaded file finishes processing | file |
file.failed | An uploaded file can't be used | file |
campaign.published | A campaign goes live on its platform | campaign |
campaign.partially_failed | A launch succeeds on some platforms and fails on others | campaign |
campaign.failed | A launch fails | campaign |
campaign.paused | A campaign is paused | campaign |
campaign.archived | A campaign is archived | campaign |
campaign.needs_attention | A campaign needs action on its ad platform | campaign |
payment.succeeded | A payment to a creator succeeds | payment, creator |
payment.failed | A payment to a creator fails | payment |
payment.disputed | A payment is disputed during its hold | payment, creator |
payment.refunded | A payment is refunded in full | payment |
payout.paid | Earnings reach a creator's (or this account's) balance | payout |
payout.failed | Sending earnings to a balance fails | payout |
withdrawal.paid | A withdrawal the account asked for reached the bank | withdrawal |
withdrawal.failed | A withdrawal the account asked for failed | withdrawal |
retainer.accepted | A creator accepts a retainer | retainer |
retainer.rejected | A creator turns a retainer down | retainer |
retainer.canceled | A retainer is canceled | retainer |
retainer.ended | A retainer ends | retainer |
sample.created | A creator requests a sample | sample |
sample.approved | A sample request is approved | sample |
sample.rejected | A sample request is rejected | sample |
message.created | A message arrives in one of the account's threads | message, thread |
brand.created | A partner provisions a brand | brand |
creator.created | A partner adds a creator to its roster | creator |
JSON
{
"id": "evt_u5s9xZIdD99WBwmDjJPYc3e",
"type": "submission.approved",
"account": "acct_04Jm0JWUg20EhYo2lyNIEHo",
"data": { "submission": "sub_14XA4WGiSOmaVI3B4QLm4Fs" }
}
Verify the signature
Lucra-Signature: t=<seconds>,v1=<hex> is an HMAC-SHA256 of <t>.<raw body> with your secret.
TypeScript
import { createHmac, timingSafeEqual } from "node:crypto"
export function verifyLucraWebhook(
body: string,
header: string,
secret: string
) {
const { t, v1 } = Object.fromEntries(
header.split(",").map((part) => part.split("="))
)
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false
const expected = createHmac("sha256", secret)
.update(`${t}.${body}`)
.digest("hex")
return (
v1?.length === expected.length &&
timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
)
}
Retries
Answer 2xx within 10 seconds. Failed deliveries retry for 3 days. Events can repeat or arrive out of order, so deduplicate on id.