License bridge
If you already issue license keys, AppsXi can send each sale and refund to your server. The key you return appears in the buyer's AppsXi library.
Set it up
Developer → your app → Pricing → License bridge: enter an https URL on a public host name and save. AppsXi shows a signing secret (whsec_…) once; store it on your server. You can rotate it and send a test event from the same page.
Events
AppsXi sends POST requests with a JSON body:
{
"id": "evt_…_p",
"type": "purchase.created",
"created": 1792800000,
"app": { "id": "app_…", "slug": "my-app", "name": "My App" },
"data": {
"license": { "id": "lic_…", "kind": "paid", "type": "personal", "seats": 2, "environment": "live" },
"order": { "id": "ord_…", "currency": "USD", "list_price_cents": 2900, "discount_cents": 0, "subtotal_cents": 2900, "paid_at": "…" },
"buyer": { "id": "buyer_…", "email": "buyer@example.com" }
}
}| Type | When | Notes |
|---|---|---|
purchase.created | A paid order completes | Includes the buyer's email so you can create the account in your system. |
refund.created | A refund or chargeback | data.refund has amount_cents, reason and initiated_by; revoke the key. |
bridge.test | You press Send test | data is { "test": true }. |
license.revoked | A bought or granted license stops working: a refund, a chargeback, or AppsXi removed it | Optional. data.license has id, kind, status and environment; data.order has id and status (or is null for a free grant). |
coupon.redeemed | A paid order used one of your coupon codes | Optional. data.coupon has code, kind and value; data.order has the amounts and paid_at; data.license.id is the buyer's license. |
The optional events are off until you tick them under Also send on the License bridge section, so a server written for the first two events keeps working. Answer them with any 2xx; their response body is ignored. They carry the buyer's id but not their email.
buyer.id is stable per app and matches the sub of AppsXi entitlement tokens and the buyer field of the Developer API. Event ids are stable, so treat repeats as duplicates. Ignore event types you do not know; AppsXi may add more.
Verify the signature
Each request has AppXi-Signature: t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of "<t>.<raw body>" with your secret. Reject requests older than five minutes.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(header, rawBody, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return v1.length === expected.length && timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}Return your key
Answer a purchase.created event with 200 and:
{ "license_key": "YOUR-KEY-1234" }AppsXi encrypts it and shows it in the buyer's library. Any 2xx counts as delivered.
Retries
AppsXi waits up to 10 seconds for an answer and does not follow redirects. Failed deliveries are retried with backoff, up to 12 attempts. Every attempt is listed on the License bridge page. Replay works for purchase and refund events.