Reference

Build on the Lucra API.

Drag a sticker to move it, or use the arrow keys while focused.

Webhooks

Events Lucra sends your server, and verifying them.

Lucra POSTs an event to your server when something changes, so you don't have to poll. Webhooks belong to an API key: each key has at most one webhook URL.

Set up

Add a webhook URL when you create the key in Settings → Developers → API, or have the key set its own:

TypeScript
const { data } = await lucra.keys.current.update({
  body: { webhookUrl: "https://example.com/lucra/webhooks" },
})

The response's webhookSecret (whsec_…) is the signing secret, shown only once; store it to verify signatures. Setting a URL again issues a new secret, and null stops events. GET /v1/keys/current reads the current URL.

Which events a key gets

A key gets events about its own account. A partner's key also gets events about its client brands, and every event about a creator on its roster (in data.creator), wherever it happens: their applications, submissions, messages, retainers, payments, connections and payout setup. An event about a creator alone has account: null.

Missed some? GET /v1/events lists the events sent to the key's webhook, newest first, each with its delivery status. It holds only events from while the key had a webhook URL.

Events

An event names what changed and the IDs involved; fetch the details from the API. Ignore types you don't handle, since new ones are added.

JSON
{
  "id": "evt_0u5s9xZIdD99WBwmDjJPYc3",
  "type": "submission.approved",
  "account": "acct_04Jm0JWUg20EhYo2lyNIEHo",
  "data": { "program": "prog_00lhuAJjQgdtNt5WnqTBc95", "submission": "sub_14XA4WGiSOmaVI3B4QLm4Fs" }
}

Some steps happen on a hosted page: a platform sign-in, payout setup or a card. Send the person to the URL from POST /v1/connections, then wait for connection.connected or creator.payouts_updated.

EventWhendata
application.createdA creator applies to a programprogram, application
application.invitedA creator is invited and must accept the program's agreements (agreementsRequired)program, application, creator
application.approvedAn application is approvedprogram, application
application.rejectedAn application is rejectedprogram, application
application.withdrawnA creator withdraws an applicationprogram, application
agreement.acceptedA creator accepts the account's agreementscreator, program when for one
submission.createdA creator submits workprogram, submission
submission.approvedA submission is approvedprogram, submission
submission.revision_requestedA reviewer asks for changesprogram, submission
submission.rejectedA submission is rejectedprogram, submission
submission.liveA submission goes liveprogram, submission
file.readyAn uploaded file finishes processingfile
file.failedAn uploaded file can't be usedfile
campaign.publishedA campaign goes live on its platformcampaign
campaign.partially_failedA launch succeeds on some platforms and fails on otherscampaign
campaign.failedA launch failscampaign
campaign.pausedA campaign is pausedcampaign
campaign.archivedA campaign is archivedcampaign
campaign.needs_attentionA campaign needs action on its ad platformcampaign
payment.createdA payment to a creator is createdpayment, creator
payment.pending_approvalA payment waits for the brand to approve itpayment, creator
payment.succeededA payment to a creator succeedspayment, creator
payment.failedA payment to a creator failspayment
payment.disputedA payment is disputed before it's sent to the creatorpayment, creator
payment.refundedA payment is refunded in fullpayment
payout.paidEarnings reach a creator's (or this account's) balancepayout
payout.failedSending earnings to a balance failspayout
withdrawal.createdA withdrawal (the account's or a roster creator's) is on its waywithdrawal
withdrawal.paidA withdrawal (the account's or a roster creator's) reached the bankwithdrawal
withdrawal.failedA withdrawal (the account's or a roster creator's) failedwithdrawal
retainer.acceptedA creator accepts a retainerretainer
retainer.rejectedA creator turns a retainer downretainer
retainer.canceledA retainer is canceledretainer
retainer.endedA retainer endsretainer
sample.createdA creator requests a samplesample
sample.approvedA sample request is approvedsample
sample.rejectedA sample request is rejectedsample
sample.shippedThe store's order for a sample is createdsample, creator
pixel.verifiedA pixel receives its first eventpixel
conversion.recordedAn event counts as one of the account's conversionsconversion, pixel_event, submission when credited to a video
message.createdA message arrives in one of the account's threadsmessage, thread
brand.createdA partner provisions a brandbrand
creator.createdA partner adds a creator to its rostercreator
creator.payouts_updatedA creator's payout setup changes: verified, needs action, or readycreator
connection.connectedA platform account is connectedconnection, creator for a creator's
connection.disconnectedA platform account is disconnectedconnection, creator for a creator's
connection.expiredA platform account needs reconnectingconnection, creator for a creator's
ad_authorization.requestedA brand asks a creator to authorize a submission as an adad_authorization, submission, creator
ad_authorization.grantedThe creator grants itad_authorization, submission, creator
ad_authorization.revokedIt is revokedad_authorization, submission, creator

Verify the signature

Every delivery has a Lucra-Signature header: t=<seconds>,v1=<hex>, where v1 is an HMAC-SHA256 of <t>.<raw body> keyed with your signing secret. Check it against the raw request body, before parsing it, and reject old timestamps:

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 with a 2xx within 10 seconds; do slow work after you respond. Failed deliveries retry for 3 days. Events can repeat or arrive out of order, so deduplicate on id.