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:

JSON
{
  "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" }
  }
}
TypeWhenNotes
purchase.createdA paid order completesIncludes the buyer's email so you can create the account in your system.
refund.createdA refund or chargebackdata.refund has amount_cents, reason and initiated_by; revoke the key.
bridge.testYou press Send testdata is { "test": true }.
license.revokedA bought or granted license stops working: a refund, a chargeback, or AppsXi removed itOptional. data.license has id, kind, status and environment; data.order has id and status (or is null for a free grant).
coupon.redeemedA paid order used one of your coupon codesOptional. 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.

JavaScript
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:

JSON
{ "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.