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 does | Copies a key from the library | Clicks one button |
| Trials | Started on the AppsXi listing, then a trial key | startTrial() in the app |
| After purchase | Enter the key | Unlocks by itself (waitForUnlock()) |
| Plugins in a DAW | Key field | Device code: "go to /activate and enter BCDF-2345" |
| Offline | Signed token, 30 days | Signed token, 30 days plus your grace period |
| Buyer data | None | None: an anonymous ID that only your app sees |
Set it up
- 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.
- Create a test product key (
pk_test_…) and later a live one (pk_live_…). The product key is the OAuthclient_id. It is safe to ship in the app. - Add the SDK for your language from /sdk and embed AppsXi's public key (the
xvalue from/api/v1/licenses/public-key). - Optional: register a redirect URI such as
com.example.myapp:/oauthif you useASWebAuthenticationSessionon 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
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 restartcheck()answers from the cached token offline and refreshes online at most once a day.signIn()opens the system browser on/oauth/authorizewith 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)
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)
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
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
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:
{
"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
}subis 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_devicebinds the token to one computer: a token copied to another machine does not verify.refis when the SDK should refresh (daily when online).expis 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
revokedand 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.
| Authorize | GET /oauth/authorize?response_type=code&client_id=pk_…&redirect_uri=…&code_challenge=…&code_challenge_method=S256&state=… |
| Device code | POST /api/v1/oauth/device_authorization with client_id, device_name, platform |
| Token | POST /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 trial | POST /api/v1/trials/start with Authorization: Bearer axr_…, client_id, device_id |
| Sign out | POST /api/v1/oauth/revoke with client_id, token, device_id |
| Public key | GET /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:
{
"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>.jsonon a Mac and%APPDATA%\AppsXi\Tickets\<client_id>.jsonon 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,ticketanddevice_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.