LiaDevelopers

Server reference

Lia tells your server when a charge is paid or refunded and when a reward is paid, signed with your webhook secret. What's sent, how to check the signature, and how retries work.

#Setting one up

Give the Lia team an https address on your server, and keep the webhook signing secret (whsec_…) that came with your keys. Lia then sends a POST there when something happens in your app.

A webhook is a nudge, not the truth: check with the API before acting on one (for a charge, GET /charges/:id). And don't rely on webhooks alone; your page can also tell your server (see Sell an item).

#What's sent

HTTP
POST /lia/webhook
Content-Type: application/json
Lia-Signature: t=1761998400,v1=5f2c8a…

{
  "type": "charge.paid",
  "app": "your-app",
  "at": 1761998400123,
  "data": {
    "id": "ch_9b1f4c2e7a0d5b8f3e6a1c4d",
    "playerId": "p_3f9c0a6d1b2e4f5a6b7c",
    "productId": "extra-lives",
    "amount": 5,
    "status": "paid",
    "mode": "live",
    "createdAt": 1761998400000,
    "deliveredAt": null,
    "refundedAt": null
  }
}
FieldType
typestringWhat happened (below).
appstringYour app id.
atnumberWhen it was sent, in milliseconds since 1970.
dataobjectWhat it's about.

#Types

TypeWhendata
charge.paidA person paid on Lia's pay sheet, in test or live.The charge, with status: "paid"
charge.refundedYou refunded a charge.The charge, with status: "refunded"
reward.paidYou paid a reward from your pool, in test or live.{ key, playerId, amount, reason, mode }

Check data.mode (or the charge's mode) so test events never grant anything real. More types may be added: ignore types you don't know and answer 200.

#Checking the signature

Every webhook has a Lia-Signature header, t=<seconds>,v1=<signature>. The signature is HMAC SHA-256, in hex, of the seconds, a dot, and the raw body, keyed with your whole webhook secret (including whsec_):

Text
v1 = hex(HMAC_SHA256(key = "whsec_…", message = t + "." + rawBody))

Check it against the raw body exactly as it arrived, before parsing it, and refuse webhooks more than a few minutes old.

Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

export function liaWebhook(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries((header ?? "").split(",").map((part) => part.split("=")));
  const t = Number(parts.t);
  if (!t || !parts.v1) return null;
  if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return null;
  const expected = Buffer.from(createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"));
  const given = Buffer.from(parts.v1);
  if (expected.length !== given.length || !timingSafeEqual(expected, given)) return null;
  return JSON.parse(rawBody);
}
Cloudflare Workers, Deno, Bun
export async function liaWebhook(request, secret, toleranceSeconds = 300) {
  const rawBody = await request.text();
  const parts = Object.fromEntries((request.headers.get("Lia-Signature") ?? "").split(",").map((part) => part.split("=")));
  const t = Number(parts.t);
  if (!t || !parts.v1 || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return null;
  const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
  const signature = new Uint8Array(parts.v1.match(/../g).map((byte) => parseInt(byte, 16)));
  const ok = await crypto.subtle.verify("HMAC", key, signature, new TextEncoder().encode(`${t}.${rawBody}`));
  return ok ? JSON.parse(rawBody) : null;
}

Handle webhooks has a complete handler.

#Answering and retries

  • Answer with any 2xx status, quickly. Do slow work after answering.
  • If your server doesn't answer 2xx (or can't be reached), Lia tries once more straight away. After that, the webhook isn't sent again.
  • So webhooks can be missed, and can arrive twice. Make your handler safe to run twice (key it on the charge id or the reward key), and have another way to catch up, such as Lia.charges() when the player opens your app.

#Your secret

Your webhook secret changes whenever your keys are made again. Keep it on your server, next to your key.

Type to search every page, method and endpoint.