A webhook sends a request to your own server when something happens in your store, like a purchase or a new review, within seconds of it happening. This guide covers adding one, the events you can choose, what arrives, and how to check each request really came from Enviglo.
Before you start
You need the Integrations permission. Owners and admins have it; staff need it given to them. See Roles and permissions.
You need an https address on the public internet that answers within 8 seconds with a status in the 200s.
Add a webhook
In your store's dashboard, open Settings → Integrations.
In the Webhooks card, enter your Endpoint URL, starting with https://.
Under Events, tick what you want to hear about. The three license events are ticked to start with.
Press Add webhook.
Your signing secret appears in a box at the top of the card. Press Copy, store it somewhere safe on your server, then press .
Done
This is the only time Enviglo shows the signing secret. It starts with whsec_, and you need it to check the signature.
Enviglo turns away addresses that aren't https, that point at localhost or a private network, or that have a username or password in them.
Events
Event
In the dashboard
Sent when
LICENSE_GRANTED
License granted
A purchase was recorded or a license was granted.
LICENSE_REVOKED
License revoked
A license was revoked or restored.
LICENSE_TRANSFERRED
License transferred
A license moved to a new owner.
LICENSE_EXPIRED
Subscription ended
A subscription ran out, so its owner no longer has access.
PRODUCT_PUBLISHED
Product published
A product went live or was unpublished.
PRODUCT_UPDATED
Product updated
Product details were saved.
FILE_UPLOADED
File uploaded
A new file version is available.
REVIEW_POSTED
Review posted
A customer posted or updated a review.
Some events cover more than one thing, and the payload says which:
LICENSE_GRANTED covers purchases recorded in game, including renewals and extra copies, as well as licenses your team grants, codes people redeem, free products people claim and the products inside a bundle. For an in-game purchase, data.outcome is created, copy_added or renewed.
LICENSE_EXPIRED is sent once, when the subscription's end date passes. Its data has the license (with its expiresAt and owner) and the product.
LICENSE_REVOKED covers both directions: data.action is revoked or restored.
For PRODUCT_PUBLISHED, check data.product.status.
For REVIEW_POSTED, data.review.updated is true when a customer changed a review they'd already posted.
The Test button sends a PING event, whatever you ticked.
What Enviglo sends
Each delivery is a POST with a JSON body and these headers:
Header
What it holds
Content-Type
application/json
User-Agent
Enviglo-Webhooks/1.0
X-Enviglo-Event
The event, like LICENSE_GRANTED.
X-Enviglo-Delivery
An ID for this delivery, the same as the body's id.
X-Enviglo-Timestamp
When it was sent, in Unix seconds.
X-Enviglo-Signature
v1= followed by the signature.
The body always has the same four fields: id, event, createdAt (when it was sent) and data. A license event looks like this:
The owner is an Enviglo account. owner.robloxId is the Roblox account linked to it, and null if there isn't one. Alongside license and product, a license event carries a few fields about what happened, depending on how it came about.
The other events carry:
Product published and Product updated:product, with its id, slug, name, status, priceCents, robuxPrice and updatedAt.
File uploaded:product (id and name), file, with its id, version, fileName, sizeBytes and createdAt, and uploadedBy, the username of whoever uploaded it.
Review posted:review (id, rating, body and updated), product (id, name and slug), and author (id and username).
Ping:store (id, slug and name) and sentBy, the username of whoever pressed Test.
Check the signature
Every request is signed with your webhook's signing secret, so you can turn away anything that didn't come from Enviglo:
Take the X-Enviglo-Timestamp header and the raw request body, exactly as they arrived.
Join them with a full stop: the timestamp, then ., then the body.
Work out the HMAC-SHA256 of that with your signing secret, as lower-case hex.
Compare it with the part of X-Enviglo-Signature after v1=, using a constant-time comparison.
Turn away old requests: check the timestamp is within a few minutes of now.
In Node.js:
import { createHmac, timingSafeEqual } from"node:crypto";
// rawBody: the request body exactly as it arrived, as a string.// headers: the request's headers, with lower-case names, as Node gives them.exportfunctionverifyEnviglo(rawBody, headers, secret) {
const timestamp = String(headers["x-enviglo-timestamp"] ?? "");
const signature = String(headers["x-enviglo-signature"] ?? "");
if (!/^\d+$/.test(timestamp) || !signature.startsWith("v1=")) returnfalse;
// Turn away anything more than five minutes old, so a copied request can't be replayed later.if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) returnfalse;
const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const given = signature.slice("v1=".length);
return given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}
The signature covers the exact bytes Enviglo sent, so check it before you parse the JSON, and never against a body you've parsed and turned back into text. A server that reads the raw body:
import { createServer } from"node:http";
import { verifyEnviglo } from"./verify-enviglo.js"; // the function above, in its own fileconstSIGNING_SECRET = "YOUR_SIGNING_SECRET"; // whsec_…, kept wherever your server keeps secretscreateServer((req, res) => {
let rawBody = "";
req.setEncoding("utf8");
req.on("data", (chunk) => (rawBody += chunk));
req.on("end", () => {
if (!verifyEnviglo(rawBody, req.headers, SIGNING_SECRET)) {
res.writeHead(401).end();
return;
}
const delivery = JSON.parse(rawBody);
console.log(delivery.event, delivery.data);
res.writeHead(200).end();
});
}).listen(3000);
Answer quickly with a status in the 200s, and do any slow work afterwards: Enviglo waits 8 seconds at most.
Retries and failures
A delivery counts as delivered when your server answers with a status in the 200s within 8 seconds. Enviglo doesn't follow redirects, so use your endpoint's final address.
When a delivery fails, Enviglo tries again up to five more times, waiting longer each time: a minute before the first retry, then five minutes, half an hour, two hours and six hours. After six tries in all, it gives up on that event.
Each try is a fresh request, with its own id, timestamp and signature. If your server did the work but didn't answer in time, the same event can arrive twice, so make your handler safe to run twice, for example by remembering which licenses you've already handled.
After 25 failed tries in a row, across all events, Enviglo switches the webhook off. The card shows it as Paused, with "Disabled after repeated failures", and events waiting for a retry are given up. Fix your server, then press the play button to resume it.
Recent deliveries, under each webhook, lists its last eight events: delivered, retrying or given up, with the error when there was one. Press Retry on one to send it again now. A paused webhook has to be resumed first.
Test it
Press Test next to a webhook to send it a PING event, signed like any other. Enviglo tells you what came back: "Ping delivered (HTTP 200).", the status your server answered with, or why it couldn't reach it. A failed ping isn't retried: press Test again once you've fixed things.
A store can send 10 test pings every 10 minutes.
Pause, change or delete
Pause: press the pause button next to a webhook. It gets no events until you press the play button to resume it.
Change: you can't edit a webhook's address or events. Add a new webhook with what you want, then delete the old one. The new one has its own signing secret.
Delete: press the bin button, then Delete. It stops receiving events immediately.
Adding, pausing, resuming and deleting webhooks shows in your store's Audit log.
Plan limits
Plan
Webhooks
Free
1
Pro
5
Ultimate
15
Paused webhooks count too. At the limit, Enviglo says so, for example "Your plan covers 1 webhook. Pro raises that to 5." See Plans, limits and fees.
Troubleshooting
"Webhook URLs must use https." Use an https:// address.
"Webhooks cannot point at localhost." or "Webhooks cannot point at private network addresses." Enviglo only sends to addresses on the public internet.
"Remove credentials from the URL." Take the username and password out of the address.
"Pick at least one event." Tick at least one event.
"Too many test pings in a short time." Wait for the time the message gives, then test again.
"This endpoint is paused. Resume it before retrying." Press the play button, then Retry.
Signatures never match. Check you're using the raw body, the X-Enviglo-Timestamp header, and the whole signing secret, including whsec_. Compare against the signature without its v1=.