← Home
⌕
Ask on Discord
General ER:LC FiveM API
Developers
DOCUMENTATION/Public API v1

Webhooks

Receive your server's events at your URL, signed with X-Blixye-Signature: verifying the signature, retries and shutdown.

A webhook is a URL of yours that Blixye sends a POST to every time something you subscribed to happens: a new call, a citation, a ban, a promotion… The event list and each one's scope is in API v1: events. They are created, changed and deleted through /v1/webhooks with the webhooks:manage scope (API v1: webhooks); a key can only subscribe to events covered by its own scopes.
NOTE A webhook is a URL of yours that Blixye sends a POST to every time something you subscribed to happens: a new call, a citation, a ban, a promotion… The event list and each one's scope is in API v1: events. They are created, changed and deleted through /v1/webhooks with the webhooks:manage scope (API v1: webhooks); a key can only subscribe to events covered by its own scopes.
The destination
NOTE The destination
NOTE
https only, no username or password in the URL, and 500 characters at most.
Every IP the name resolves to must be public: not private, loopback, link-local or reserved. It is checked on save and again on every delivery.
Redirects are not followed: a 3xx counts as a failure.
How many destinations you can have depends on the plan: 1 on Plus, 5 on Premium and 20 on Business.
What arrives
NOTE What arrives
NOTE
A POST with the event envelope as JSON: {"id", "type", "created_at", "data"}. It is the same envelope, with the same id, that GET /v1/events returns.
X-Blixye-Signature: t=<unix time>,v1=<hex HMAC-SHA256>.
X-Blixye-Event: the event type.
X-Blixye-Delivery: the delivery id, the same across all its retries. Use it, or the event id, to avoid processing anything twice.
User-Agent: always Blixye-Webhooks/1.0.
Verifying the signature
NOTE Verifying the signature
The secret (whsec_…) is shown only once, when the webhook is created or its secret rotated. The signature is the HMAC-SHA256, with that secret, of the time t, a dot and the body EXACTLY as it arrives, byte for byte: do not re-serialise it. Compare in constant time and reject the delivery if t is more than 5 minutes away from your clock.
NOTE The secret (whsec_…) is shown only once, when the webhook is created or its secret rotated. The signature is the HMAC-SHA256, with that secret, of the time t, a dot and the body EXACTLY as it arrives, byte for byte: do not re-serialise it. Compare in constant time and reject the delivery if t is more than 5 minutes away from your clock.
In Node.js, with no dependencies:
NOTE In Node.js, with no dependencies:
const http = require('node:http'); const crypto = require('node:crypto'); const SECRET = process.env.BLIXYE_WEBHOOK_SECRET; function verify(header, rawBody) { const m = /^t=([0-9]+),v1=([a-f0-9]{64})$/.exec(header || ''); if (!m) return false; if (Math.abs(Math.floor(Date.now() / 1000) - Number(m[1])) > 300) return false; const expected = crypto.createHmac('sha256', SECRET) .update(m[1] + '.') .update(rawBody) .digest('hex'); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(m[2])); } http.createServer((req, res) => { const chunks = []; req.on('data', (c) => chunks.push(c)); req.on('end', () => { const rawBody = Buffer.concat(chunks); if (!verify(req.headers['x-blixye-signature'], rawBody)) { res.writeHead(400).end(); return; } const event = JSON.parse(rawBody.toString('utf8')); res.writeHead(204).end(); console.log(event.type, event.id, req.headers['x-blixye-delivery']); }); }).listen(3000);
const http = require('node:http'); const crypto = require('node:crypto'); const SECRET = process.env.BLIXYE_WEBHOOK_SECRET; function verify(header, rawBody) { const m = /^t=([0-9]+),v1=([a-f0-9]{64})$/.exec(header || ''); if (!m) return false; if (Math.abs(Math.floor(Date.now() / 1000) - Number(m[1])) > 300) return false; const expected = crypto.createHmac('sha256', SECRET) .update(m[1] + '.') .update(rawBody) .digest('hex'); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(m[2])); } http.createServer((req, res) => { const chunks = []; req.on('data', (c) => chunks.push(c)); req.on('end', () => { const rawBody = Buffer.concat(chunks); if (!verify(req.headers['x-blixye-signature'], rawBody)) { res.writeHead(400).end(); return; } const event = JSON.parse(rawBody.toString('utf8')); res.writeHead(204).end(); console.log(event.type, event.id, req.headers['x-blixye-delivery']); }); }).listen(3000);
NOTE const http = require('node:http'); const crypto = require('node:crypto'); const SECRET = process.env.BLIXYE_WEBHOOK_SECRET; function verify(header, rawBody) { const m = /^t=([0-9]+),v1=([a-f0-9]{64})$/.exec(header || ''); if (!m) return false; if (Math.abs(Math.floor(Date.now() / 1000) - Number(m[1])) > 300) return false; const expected = crypto.createHmac('sha256', SECRET) .update(m[1] + '.') .update(rawBody) .digest('hex'); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(m[2])); } http.createServer((req, res) => { const chunks = []; req.on('data', (c) => chunks.push(c)); req.on('end', () => { const rawBody = Buffer.concat(chunks); if (!verify(req.headers['x-blixye-signature'], rawBody)) { res.writeHead(400).end(); return; } const event = JSON.parse(rawBody.toString('utf8')); res.writeHead(204).end(); console.log(event.type, event.id, req.headers['x-blixye-delivery']); }); }).listen(3000);
In PHP:
NOTE In PHP:
<?php $secret = getenv('BLIXYE_WEBHOOK_SECRET'); $rawBody = file_get_contents('php://input'); $header = $_SERVER['HTTP_X_BLIXYE_SIGNATURE'] ?? ''; if (!preg_match('/^t=([0-9]+),v1=([a-f0-9]{64})$/', $header, $m) || abs(time() - (int) $m[1]) > 300 || !hash_equals(hash_hmac('sha256', $m[1] . '.' . $rawBody, $secret), $m[2])) { http_response_code(400); exit; } $event = json_decode($rawBody, true); http_response_code(204); error_log($event['type'] . ' ' . $event['id']);
<?php $secret = getenv('BLIXYE_WEBHOOK_SECRET'); $rawBody = file_get_contents('php://input'); $header = $_SERVER['HTTP_X_BLIXYE_SIGNATURE'] ?? ''; if (!preg_match('/^t=([0-9]+),v1=([a-f0-9]{64})$/', $header, $m) || abs(time() - (int) $m[1]) > 300 || !hash_equals(hash_hmac('sha256', $m[1] . '.' . $rawBody, $secret), $m[2])) { http_response_code(400); exit; } $event = json_decode($rawBody, true); http_response_code(204); error_log($event['type'] . ' ' . $event['id']);
NOTE <?php $secret = getenv('BLIXYE_WEBHOOK_SECRET'); $rawBody = file_get_contents('php://input'); $header = $_SERVER['HTTP_X_BLIXYE_SIGNATURE'] ?? ''; if (!preg_match('/^t=([0-9]+),v1=([a-f0-9]{64})$/', $header, $m) || abs(time() - (int) $m[1]) > 300 || !hash_equals(hash_hmac('sha256', $m[1] . '.' . $rawBody, $secret), $m[2])) { http_response_code(400); exit; } $event = json_decode($rawBody, true); http_response_code(204); error_log($event['type'] . ' ' . $event['id']);
Answer quickly with a 2xx and do the work afterwards: Blixye waits 5 seconds at most.
NOTE Answer quickly with a 2xx and do the work afterwards: Blixye waits 5 seconds at most.
Retries and shutdown
NOTE Retries and shutdown
NOTE
Deliveries go out in a pass that runs every minute: an event arrives within about a minute, not instantly.
A 2xx is a completed delivery. Anything else (a 3xx, 4xx or 5xx, a timeout or a network error) is a failure.
A failed delivery is retried after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h: 8 attempts in total. After that it is given up.
10 CONSECUTIVE failures on the same destination, from any delivery, turn it off: it is left with disabled_reason too_many_failures and its pending deliveries are given up. A single success resets the count.
To turn it back on: PATCH /v1/webhooks/{id} with {"is_active": true}. It starts from zero, but whatever was lost while it was off is not re-sent: recover it with GET /v1/events.
Order is not guaranteed: a retry can arrive after newer events. Sort by created_at.
Delivery history is kept for 30 days.
When you rotate the secret (POST /v1/webhooks/{id}/rotate-secret), the old one stops signing immediately: every later delivery, retries included, uses the new one. Update your receiver right away.
NOTE When you rotate the secret (POST /v1/webhooks/{id}/rotate-secret), the old one stops signing immediately: every later delivery, retries included, uses the new one. Update your receiver right away.
If you cannot receive webhooks (behind NAT, or a script that runs now and then), poll GET /v1/events: they are the same events (API v1: events).
NOTE If you cannot receive webhooks (behind NAT, or a script that runs now and then), poll GET /v1/events: they are the same events (API v1: events).
← Previous
Idempotency
Next →
API v1: server
36 / 54 Something missing here →
STEP 1 · PLAN ×
Pick your server's plan
After payment you name it and we configure it together. You can change plan whenever you like.
FREE1 OF 1 LEFT Up to 500 characters and basic CAD. To try it. 0 €
PLUSMONTHLY Full CAD, moderation, activity and economy. 5,99 €
PREMIUMER:LC & FIVEM Everything in PLUS, FiveM included and higher limits. 12,99 €
ENTERPRISEPERMANENT All of PREMIUM forever, self-hosted, never a fee. Contact
Continue to payment
Cancel any time · 14-day withdrawal right, unless you ask to start straight away
Confirm and pay
ER:LC PREMIUM
Monthly · one licence
12,99 €
EMAIL
CARD
Back
In development — unavailable
Blixye is in development: services can’t be purchased yet.Secure payment · VAT included
Name your server
This is the name your players see in the launcher and on Discovery. You can change it whenever.
SERVER NAME
It gets a short code, like SKY-T2Q, to link the launcher.
NEXT Once created we open the setup guide: launcher, Discord and modules in three steps.
Back
Create the server
Type at least 3 characters.
STEP 1 OF 3 · GUIDE ×
1. Send your people the launcher
It is the only thing they install, and only once. They sign in with Discord, we verify their Roblox account and the overlay shows over the session.
Go to the download
2. Create your departments
Police, sheriff, fire or whatever you run. Each with its ranks and permissions: that decides who can punish, clock in or read records.
3. Open the server
With the first duty shift the calls, activity and economy start filling up. From there you just tune it.
Back
Next
⌕ ESC
In-game overlay Radio, sheet and dispatch over the session, no alt-tab. ER:LC CAD and departments Queued calls, units with status and rosters with ranks. ER:LC Moderation and appeals Punishments with context, logs and appeals that do not get lost. ER:LC Staff activity Quotas per rank, shifts and an automatic weekly report. ER:LC Economy and businesses Accounts, payroll, player businesses and inventory. ER:LC Blixye Framework The core: characters, permissions, database and QBCore and ESX bridge. FiveM Inventory Weight, containers and safe player trades. FiveM Police MDT Records, fines, warrants and plate lookups. FiveM Banking Accounts, transfers, payroll and business accounts. FiveM Jobs Shifts, payroll and per-company hierarchy. FiveM Garages Vehicle ownership, persistent damage and impound. FiveM Housing Buying, renting, furniture and personal storage. FiveM Phone Messages, calls, apps and in-world socials. FiveM EMS Per-limb injuries, treatment and a hospital with beds. FiveM All the scripts All eight scripts unlimited, as a permanent purchase or a subscription. FiveM Install Blixye on ER:LC From an empty server to the first duty shift, without touching a game file. Docs Install the FiveM framework The zip, the SQL and your current base bridge, in the right order. Docs Your first week What to set up each day so you do not end up with twenty half-done things. Docs CAD and dispatch Calls, units and priorities inside the overlay. Docs Moderation and appeals Punishments with evidence, appeals and the log that covers you. Docs Activity and shifts Hours, quotas and who is really patrolling. Docs Scripts and free version What each script free version limits and what the full one opens. Docs QBCore and ESX bridge Living with your current base without duplicating players or money. Docs Performance Where to look when the server stutters. Docs
Nothing by that name. Try “inventory”, “CAD” or “licences”.
↵ open CTRL K search 37 results
ROTATE THE KEY ×
! The current key stops working the moment you confirm. This cannot be undone.
current key → new key on confirm
Paste the new key into the script config.
Restart the resource: refresh and ensure.
Until you do, the script will not load.
Yes, rotate the key Cancel
What each plan includes ×
FREE
DISCORD
PLUS
PREMIUM
ENTERPRISE
Price
Free
2,99 €/mo
5,99 €/mo
12,99 €/mo
Contact
GENERAL
Characters stored
500
—
5000
∞
∞
Custom invite code
—
✓
✓
✓
✓
CAD
Max corporations
2
—
6
∞
∞
Max subdivisions
0
—
6
∞
∞
MDT data wipe
1 month
—
1 year
Never
Never
MDT access
Complete
—
Complete
Complete
Complete
Logs history
7 days
—
90 days
∞
∞
Data export
—
—
✓
✓
✓
Investigations
—
—
✓
✓
✓
Evidence storage
—
—
Unmetered
Unmetered
Unmetered
Custom laws
50
—
150
∞
∞
Real-time map
—
—
✓
✓
✓
Bodycam
—
—
✓
✓
✓
Realtime map location
—
—
✓
✓
✓
Advanced search
—
—
✓
✓
✓
Radio channels (Discord VC)
10
—
50
100
∞
Roles per corporation
15
—
50
∞
∞
BANK
Bank accounts
1 per char
—
3 per char
6 per char
12 per char
Transfers
✓
—
✓
✓
✓
Transaction history
✓
—
✓
✓
✓
Cards per account
1
—
2
3
5
Timed cards
Fixed durations
—
✓
✓
✓
Usage-based cards
—
—
Max 5 per char
✓
✓
Role rewards
✓
—
✓
✓
✓
Allow negative balance
—
—
✓
✓
✓
Scheduled transfers
—
—
✓
✓
✓
Player debts
—
—
✓
✓
✓
Per-operation limits
—
—
✓
✓
✓
Shared accounts
Max 2 members
—
Max 5 members
∞
∞
ADMIN
Player heatmap
—
—
✓
✓ more precise
✓ more precise
Max forms
3
5
10
∞
∞
Your server on the phone
—
—
✓
✓
✓
Max admin roles
owner +2
owner +5
owner +10
∞
∞
Priority support
—
✓
✓
✓
✓
Staff audit log history
7 days
30 days
90 days
∞
∞
Beta features
—
✓
✓
✓
✓
Create server
Character and storage addons stack on any plan.
Cookies ESSENTIAL ONLY
We only use the essential ones: your session, sign-in security and your language. There is no analytics and no advertising, so there is nothing to accept or reject.
Got it
Cookie policy