Sign in with AppsXi

Level 2 is the recommended way to license an app on AppsXi. The buyer clicks Sign in with AppsXi, approves once in the browser, and your app gets a signed entitlement for your product only. No license keys to type, trials start inside the app, and the app unlocks by itself after the buyer pays.

Level 1 (license key)Level 2 (Sign in with AppsXi)
Buyer doesCopies a key from the libraryClicks one button
TrialsStarted on the AppsXi listing, then a trial keystartTrial() in the app
After purchaseEnter the keyUnlocks by itself (waitForUnlock())
Plugins in a DAWKey fieldDevice code: "go to /activate and enter BCDF-2345"
OfflineSigned token, 30 daysSigned token, 30 days plus your grace period
Buyer dataNoneNone: an anonymous ID that only your app sees

Set it up

  1. In Developer → your app → Pricing and licensing, set the integration to Level 2: Sign in with AppsXi. Choose seats per license, the trial length (with AppsXi as who runs it), what happens when a trial ends (locked, read only, limited mode) and the offline grace period (0 to 60 days, default 14). The trial goes on once AppsXi has seen your app sign in once, for example from a development build with your sandbox client.
  2. Create a test product key (pk_test_…) and later a live one (pk_live_…). The product key is the OAuth client_id. It is safe to ship in the app.
  3. Add the SDK for your language from /sdk and embed AppsXi's public key (the x value from /api/v1/licenses/public-key).
  4. Optional: register a redirect URI such as com.example.myapp:/oauth if you use ASWebAuthenticationSession on the Mac. Loopback addresses (http://127.0.0.1:<any port>/…) always work and need no setup.

With a test key, sign-in works on your own unpublished app and only sandbox licenses count, so you can rehearse trials and purchases with sandbox test licenses.

The flow in your app

Text
launch ─► check()  ── licensed ──► unlock
              │       trial ─────► unlock, show "5 days left"
              │       none ──────► startTrial() if trialAvailable, else paywall
              │       trial_expired / revoked ─► paywall
              └─ signed_out ─► signIn()  (or signInWithCode() in a plugin)

paywall ─► openCheckout() ─► waitForUnlock() ─► licensed, no restart
  • check() answers from the cached token offline and refreshes online at most once a day.
  • signIn() opens the system browser on /oauth/authorize with PKCE and waits on a loopback address. The buyer is usually already signed in to AppsXi, so it is one click.
  • signInWithCode() is for plugins that cannot receive a redirect: show the code, the buyer approves it at /activate.
  • startTrial() starts your trial: one per buyer account and one per computer for each product, so reinstalling or new accounts do not reset it.
  • openCheckout() opens your AppsXi checkout (already signed in); waitForUnlock() checks every few seconds until the purchase completes. The trial license becomes the paid one.
  • signOut() signs out on this computer and frees its seat. Buyers can also sign apps out from their library.

TypeScript (Electron)

TypeScript
import { AppsXi } from "./appxi-signin";

const appxi = new AppsXi({ clientId: "pk_live_…", publicKeys: ["<x>"], storageDir: app.getPath("userData") });
let e = await appxi.check();
if (!e.signedIn) e = await appxi.signIn((url) => shell.openExternal(url));
if (e.status === "none" && e.trialAvailable) e = await appxi.startTrial();
if (e.licensed) unlockAll(e.status === "trial" ? e.daysLeft : null);
else showPaywall(async () => {
  await appxi.openCheckout((url) => shell.openExternal(url), "paywall");
  if ((await appxi.waitForUnlock()).licensed) unlockAll(null);
});

Swift (macOS)

Swift
let appxi = AppXiSignIn(clientID: "pk_live_…", publicKeys: ["<x>"])
var access = await appxi.check()
if case .signedOut = access.state { access = try await appxi.signIn() }
switch access.state {
case .licensed: unlockAll()
case .trial(let daysLeft): unlockAll(); showBanner("\(daysLeft) days left")
case .none(let canTry): if canTry { access = try await appxi.startTrial() } else { showPaywall() }
default: showPaywall()   // appxi.openCheckout(); access = await appxi.waitForUnlock()
}

C++ and JUCE plugins

C++
appxi::SignIn sia({"pk_live_…", {"<x>"}, dataDir});
auto a = sia.check();
if (a.state == appxi::State::SignedOut) {
    code = sia.startDeviceSignIn();          // show code.userCode, open code.verificationUriComplete
    // from a juce::Timer every code.interval seconds:
    auto p = sia.pollDeviceSignIn(code);     // Pending, SlowDown, Approved, Denied, Expired
}

Compile with OpenSSL (libcrypto) and either -DAPPXI_USE_CURL or your own HTTP function (for example built on juce::URL).

.NET

C#
var appxi = new AppXiSignIn("pk_live_…", new[] { "<x>" }, dataDir);
var a = await appxi.CheckAsync();
if (a.State == AccessState.SignedOut) a = await appxi.SignInAsync(url => Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }));

The entitlement token

Every answer carries the buyer's status for your product and, when they may use it, an Ed25519 JWT:

JSON
{
  "iss": "https://appsxi.com",
  "sub": "buyer_9f3c…",
  "product": "app_…",
  "license": "lic_…",
  "status": "trial",
  "env": "live",
  "lvl": 2,
  "seats": { "this_device": "<device id>", "max": 2 },
  "ref": 1760400000,
  "trial_end": 1760918400,
  "iat": 1760313600,
  "exp": 1760918400
}
  • sub is pseudonymous and different for every product, so developers cannot track buyers across apps. It is the same ID the Level 1 API returns.
  • seats.this_device binds the token to one computer: a token copied to another machine does not verify.
  • ref is when the SDK should refresh (daily when online). exp is the hard limit: 30 days plus your grace period for a license, the end of the trial for a trial.
  • The SDKs refuse a token if the computer's clock is set back before the last time AppsXi answered.
  • Refunds and chargebacks revoke the license; the next refresh returns revoked and the app falls back to your locked state.

Endpoints

For your own client, or a standard OAuth library such as AppAuth. Metadata: /.well-known/oauth-authorization-server.

AuthorizeGET /oauth/authorize?response_type=code&client_id=pk_…&redirect_uri=…&code_challenge=…&code_challenge_method=S256&state=…
Device codePOST /api/v1/oauth/device_authorization with client_id, device_name, platform
TokenPOST /api/v1/oauth/token with client_id, device_id and grant_type = authorization_code (code, code_verifier, redirect_uri), urn:ietf:params:oauth:grant-type:device_code (device_code), refresh_token (refresh_token) or urn:appxi:params:oauth:grant-type:desktop-ticket (ticket)
Start a trialPOST /api/v1/trials/start with Authorization: Bearer axr_…, client_id, device_id
Sign outPOST /api/v1/oauth/revoke with client_id, token, device_id
Public keyGET /api/v1/licenses/public-key (JWK set)

device_id is a hash of the machine id, 16 to 128 characters; the SDKs use SHA-256 of the machine UUID and your key. The token answer:

JSON
{
  "token_type": "Bearer",
  "refresh_token": "axr_…",
  "status": "active",
  "entitlement": "<JWT>",
  "expires_at": "2026-11-26T08:00:00.000Z",
  "refresh_after": "2026-10-14T08:00:00.000Z",
  "seats_used": 1,
  "seats_max": 2,
  "license_type": "personal",
  "trial_available": false,
  "trial_end_behavior": "locked",
  "checkout_url": "https://appsxi.com/checkout/my-app?src=app",
  "updates": { "feed_base": "…/updates/my-app", "feed_token": "axf_…" }
}

status is one of active, trial, trial_expired, none, seat_limit or revoked. The refresh token is only in the first answer: store it. Errors follow OAuth: invalid_client, invalid_grant, authorization_pending, slow_down, access_denied, expired_token, plus trial_used and trial_unavailable from the trial endpoint.

AppsXi Desktop

When a buyer installs your app with AppsXi Desktop, Desktop leaves a one-time ticket for it. The SDKs pick it up in signIn(), so the buyer never sees a sign-in screen. Nothing to do on your side.

How it works, if you write your own client:

  • The ticket is in ~/Library/Application Support/AppsXi/Tickets/<client_id>.json on a Mac and %APPDATA%\AppsXi\Tickets\<client_id>.json on Windows, as {"ticket": "axt_…"}. Only the signed-in user can read it.
  • Exchange it at the token endpoint with grant_type=urn:appxi:params:oauth:grant-type:desktop-ticket, client_id, ticket and device_id. The answer is the same as any other sign-in, including the refresh token.
  • Delete the file whether or not the exchange works. A ticket works once, for 7 days, with your live product key only.
  • Desktop only leaves tickets for apps set to Level 2 that have a live product key.

Trial analytics

With Level 2 on, Pricing and licensing shows trials started, active trials, trials that bought, trial-to-paid conversion, starts per day, and whether trials started in the app or on AppsXi. Pass a source to openCheckout() (for example "export_paywall") to see which paywall moments convert.