Verification endpoint#
Required for manual verification
You must implement this endpoint to enable manual verification of in-game quest tasks.
You host this endpoint. Gamearly calls it when a player clicks Verify in the UI.
Method: POST · Content-Type: application/json
{
"task_id": 123,
"partner_user_id": "A1B2C3"
}
{
"task_id": 123,
"partner_user_id": "A1B2C3",
"completed": true
}
Authenticating Gamearly's request#
We sign the request body using a shared secret you can reveal or rotate in your project settings (Verification Secret).
Header
X-Gamearly-Signature: t=<unix>,v1=<hex_hmac>
t— UNIX timestamp in secondsv1—HMAC_SHA256(secret, "<t>." + <raw JSON body bytes>), hex encoded
Validation rules#
- Parse
tandv1from the header. - Reject stale timestamps — e.g. if
abs(now - t) > 300seconds. - Recompute
expected_v1with your secret using the exact raw request body bytes, not a re-serialised JSON string. - Compare
expected_v1andv1using a constant-time function. - If any check fails, return
401.
Implementation#
import crypto from "crypto";
import express from "express";
const app = express();
// Keep the exact raw body bytes for HMAC verification
app.use(express.json({
verify: (req, _res, buf) => { req.rawBody = buf; }
}));
const SECRET = process.env.GAMEARLY_VERIFICATION_SECRET;
function parseSignature(headerValue = "") {
// Example: "t=1699999999,v1=abc123..."
const out = {};
headerValue.split(",").forEach(part => {
const [k, v] = part.split("=", 2);
if (k && v) out[k.trim()] = v.trim();
});
return { t: out.t, v1: out.v1 };
}
function safeEqualHex(a, b) {
// Constant-time compare for hex strings
const A = Buffer.from(a, "utf8");
const B = Buffer.from(b, "utf8");
return A.length === B.length && crypto.timingSafeEqual(A, B);
}
app.post("/gamearly/verify-task", (req, res) => {
const sigHeader = req.get("X-Gamearly-Signature") || "";
const { t, v1 } = parseSignature(sigHeader);
if (!t || !v1) return res.status(401).send("missing signature");
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - Number(t)) > 300) {
return res.status(401).send("stale timestamp");
}
// expected = HMAC_SHA256(SECRET, `${t}.` + rawBodyBytes)
const hmac = crypto.createHmac("sha256", SECRET);
hmac.update(`${t}.`);
hmac.update(req.rawBody); // exact bytes
const expected = hmac.digest("hex");
if (!safeEqualHex(expected, v1)) {
return res.status(401).send("bad signature");
}
const { task_id, partner_user_id } = req.body;
// TODO: look up completion in your DB
const completed = false;
return res.json({ task_id, partner_user_id, completed });
});
app.listen(3000);
import os
import hmac
import hashlib
import time
import json
from django.http import JsonResponse, HttpResponseBadRequest
from django.views.decorators.csrf import csrf_exempt
SECRET = os.environ["GAMEARLY_VERIFICATION_SECRET"]
@csrf_exempt
def verify_task(request):
if request.method != "POST":
return HttpResponseBadRequest("POST only")
sig = request.headers.get("X-Gamearly-Signature", "")
parts = dict(p.split("=", 1) for p in sig.split(",") if "=" in p)
t = parts.get("t")
v1 = parts.get("v1")
if not t or not v1:
return JsonResponse({"error": "missing signature"}, status=401)
now = int(time.time())
try:
t_int = int(t)
except ValueError:
return JsonResponse({"error": "bad timestamp"}, status=401)
if abs(now - t_int) > 300:
return JsonResponse({"error": "stale timestamp"}, status=401)
# expected = HMAC_SHA256(SECRET, f"{t}." + raw_body_bytes)
raw = request.body # exact bytes as received
msg = f"{t}.".encode("utf-8") + raw
expected = hmac.new(SECRET.encode("utf-8"), msg, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, v1):
return JsonResponse({"error": "bad signature"}, status=401)
data = json.loads(raw or b"{}")
task_id = data.get("task_id")
partner_user_id = data.get("partner_user_id")
# TODO: look up completion in your DB
completed = False
return JsonResponse({
"task_id": task_id,
"partner_user_id": partner_user_id,
"completed": completed
})
// ASP.NET Core minimal API (.NET 6+)
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
app.MapPost("/gamearly/verify-task", async (HttpRequest request) =>
{
var secret = Environment.GetEnvironmentVariable("GAMEARLY_VERIFICATION_SECRET")!;
// Read the exact raw bytes before any JSON binding touches them.
using var buffer = new MemoryStream();
await request.Body.CopyToAsync(buffer);
var raw = buffer.ToArray();
var header = request.Headers["X-Gamearly-Signature"].ToString();
var parts = header.Split(',')
.Select(p => p.Split('=', 2))
.Where(kv => kv.Length == 2)
.ToDictionary(kv => kv[0].Trim(), kv => kv[1].Trim());
if (!parts.TryGetValue("t", out var t) || !parts.TryGetValue("v1", out var v1))
return Results.Unauthorized();
if (!long.TryParse(t, out var ts) ||
Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - ts) > 300)
return Results.Unauthorized();
// expected = HMAC_SHA256(secret, $"{t}." + rawBodyBytes)
var prefix = Encoding.UTF8.GetBytes($"{t}.");
var message = new byte[prefix.Length + raw.Length];
Buffer.BlockCopy(prefix, 0, message, 0, prefix.Length);
Buffer.BlockCopy(raw, 0, message, prefix.Length, raw.Length);
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var expected = Convert.ToHexString(hmac.ComputeHash(message)).ToLowerInvariant();
if (!CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(v1)))
return Results.Unauthorized();
var payload = JsonSerializer.Deserialize<VerifyRequest>(raw)!;
// TODO: look up completion in your DB
var completed = false;
return Results.Ok(new
{
task_id = payload.task_id,
partner_user_id = payload.partner_user_id,
completed
});
});
record VerifyRequest(int task_id, string partner_user_id);
Verification logic#
The actual logic is up to you. Two patterns work well and scale.
Pattern A — precomputed completions table#
Fastest. Have a job write to a dedicated table whenever a player finishes something relevant. Associate the action with the player and the Gamearly task id. Your endpoint then just reads that table and replies.
Pros: very fast responses, no heavy joins on the request path. Cons: you must keep the table up to date.
Tip
With this pattern you can also proactively report completions via
/quests/complete_task for a smoother experience.
Pattern B — on-the-fly verification#
Store a task config row describing how to verify a task_id, then compute the answer at
request time.
Pros: simpler, always up to date. Cons: slower responses, more complex queries.
Notes#
- You don't need Gamearly's user id. We send your own
partner_user_id. You can store our id if it helps analytics, but it isn't required for verification. - Unknown
task_id— returncompleted: false. - Latency — keep it fast, e.g. under 500 ms. Requests that take multiple seconds time out.
If you expect long processing, respond quickly with
completed: falseand use/quests/complete_taskto report completion later.