Webhooks: your server hears about every player from a creator code
A player signs up for NLauncher with your code or link — we instantly send a POST to your server. Once they’ve played enough, we send another. Your server hands out the bonus itself: spawn currency, a kit, a rank.
Set up in 5 minutes
- Creator dashboard → “Server integration (webhooks)”. No dashboard yet? Apply — an admin grants creator status.
- Webhook URL — an address on your server,
https://only, e.g.https://myserver.com/api/nlauncher/ref. All your codes go to one address; which code was used is inref_code. - Secret key — “Copy” it and put it on your server (as an environment variable). You use it to check the signature of every request.
- “Send test request” — the dashboard instantly shows your server’s response code and body. Below is a log of the last 50 requests with a “Retry” button.
Events
referral.registeredAn account signed up with a creator’s code or link (or entered the code within 7 days of signing up). status: pending — waiting for them to play, review — we’re checking it.
referral.qualifiedThe player is really playing: they’ve hit the threshold (usually 2 hours over 3 different days). It’s the same moment the creator’s NLcoin reward is booked. The best time for a bonus. hold_until — when our fake-account check ends.
referral.revokedThe player no longer counts: ban, admin decision, duplicate. reason: banned, admin, duplicate. You can take the bonus back. Only sent if you already got an event about this player.
testThe “Send test request” button: "test": true, a made-up player — don’t give anything.
Request
POST <url>
Content-Type: application/json
User-Agent: NLauncher-Webhooks/1
X-NL-Signature: sha256=<hex HMAC-SHA256(secret, raw body)>
X-NL-Event: referral.qualified
X-NL-Delivery: 5f0c7a52-3c1e-4c1f-9b8e-2a6d1e4b7c90 (= event_id, same for retries)
X-NL-Attempt: 1
X-NL-Timestamp: 1790683201 (unix, = sent_at in the body)
{
"event": "referral.qualified",
"event_id": "5f0c7a52-3c1e-4c1f-9b8e-2a6d1e4b7c90",
"ref_code": "MYSERVER",
"nickname": "Steve",
"user_id": "3f2b9c1e8a7d4e6f9b0c1d2e3f4a5b6c",
"minecraft_uuid": "3f2b9c1e-8a7d-4e6f-9b0c-1d2e3f4a5b6c",
"created_at": "2026-09-29T12:00:00Z",
"status": "qualified",
"hold_until": "2026-10-06T12:00:00Z",
"attempt": 1,
"sent_at": "2026-09-29T12:00:01Z"
}
event_idUUID, the same for all retries of one event — use it to drop duplicates.ref_codeThe creator code the player came with.nicknameThe player’s Minecraft nickname (NLauncher account).user_idPermanent NLauncher account id — the profile UUID without dashes.minecraft_uuidThe same UUID with dashes — how a server with NLauncher login sees the player. On an offline-mode server the UUID differs — match by nickname.created_atWhen the event happened, RFC 3339, UTC. attempt and sent_at — of this attempt.X-NL-Signaturesha256= + hex HMAC-SHA256 of the raw body; the key is the secret as a string.X-NL-TimestampUnix send time, the same as sent_at inside the signed body. Drop requests older than 10 minutes.New fields may be added — don’t fail on unknown ones.
Signature check
Compute the HMAC over the raw body bytes (before parsing JSON) and compare in constant time.
const crypto = require('crypto');
const express = require('express');
const app = express();
const SECRET = process.env.NL_WEBHOOK_SECRET;
app.post('/api/nlauncher/ref', express.raw({ type: 'application/json' }), (req, res) => {
const got = req.get('X-NL-Signature') || '';
const want = 'sha256=' + crypto.createHmac('sha256', SECRET).update(req.body).digest('hex');
if (got.length !== want.length || !crypto.timingSafeEqual(Buffer.from(got), Buffer.from(want))) {
return res.status(401).send('bad signature');
}
const ev = JSON.parse(req.body);
if (alreadySeen(ev.event_id)) return res.sendStatus(200);
if (ev.event === 'referral.qualified') giveBonus(ev.nickname, ev.minecraft_uuid);
if (ev.event === 'referral.revoked') takeBonus(ev.nickname);
res.sendStatus(200);
});
<?php
$secret = getenv('NL_WEBHOOK_SECRET');
$body = file_get_contents('php://input');
$want = 'sha256=' . hash_hmac('sha256', $body, $secret);
if (!hash_equals($want, $_SERVER['HTTP_X_NL_SIGNATURE'] ?? '')) {
http_response_code(401);
exit('bad signature');
}
$ev = json_decode($body, true);
if (already_seen($ev['event_id'])) { http_response_code(200); exit('ok'); }
if ($ev['event'] === 'referral.qualified') give_bonus($ev['nickname'], $ev['minecraft_uuid'] ?? null);
if ($ev['event'] === 'referral.revoked') take_bonus($ev['nickname']);
http_response_code(200);
echo 'ok';
import hashlib, hmac, os
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ['NL_WEBHOOK_SECRET'].encode()
@app.post('/api/nlauncher/ref')
def nl_ref():
body = request.get_data()
want = 'sha256=' + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(want, request.headers.get('X-NL-Signature', '')):
abort(401)
ev = request.get_json()
if already_seen(ev['event_id']):
return 'ok'
if ev['event'] == 'referral.qualified':
give_bonus(ev['nickname'], ev.get('minecraft_uuid'))
elif ev['event'] == 'referral.revoked':
take_bonus(ev['nickname'])
return 'ok'
Test your implementation: the secret test_secret and the body {"event":"test","event_id":"00000000-0000-4000-8000-000000000000"} give sha256=85e7ea9606714a7ed6ba43a628cdd42854e4c5a5686eed34974dc2f155ee7211.
Response and retries
- Reply 2xx within 5 seconds. Put heavy work in your own queue and answer right away.
- Non-2xx, a redirect, a timeout or a network error — we retry after 1 min, 5 min, 30 min, 2 h, 12 h and 24 h: 7 attempts in total. After that it’s “Not delivered” in the log, and you can “Retry” by hand.
- Check
event_idfor repeats. If the connection drops after your reply, the event comes again — answer 200 and don’t give anything twice. - https only, no redirects. Internal network addresses and localhost aren’t accepted — we check on save and before every send.
- Change the secret in the dashboard and the old one stops working immediately.
Privacy
A partner only receives the player’s nickname and id — to give the bonus. We never share email, IP, device or any other data. Players see this condition when they enter a creator code.