# Getting started AppsXi sells and distributes signed Mac and Windows desktop apps. You keep your own signing identity; AppsXi checks every file, hosts it, takes payment through your own Square account, issues licenses and serves updates. ## 1. Set up your developer account 1. Sign in at [appsxi.com/sign-in](https://appsxi.com/sign-in) and open **Developer**. 2. Add a passkey (Developer → Security): one tap with Face ID, Touch ID, Windows Hello or your phone. Uploads, payouts, prices and API tokens need it. A passkey added after your first one works right away, for everything. For its first 24 hours, builds it sends (uploads, or through GitHub or an API token set up with it) are checked right away and go live a day after they were sent. 3. Connect your Square account (Developer → Payments). Buyers pay you through Square; AppsXi takes its fee from each sale. 4. Fill in your public profile and add your signing identities (Apple Team ID, Windows certificate subject). Add a support email: paid apps cannot go live or be bought without one. Buyers' questions about how your app works arrive under Developer → Support; see [Buyer support](https://appsxi.com/docs/support). ## 2. Make a package AppsXi accepts - **Mac:** `.dmg`, `.pkg` or `.zip`, signed with a Developer ID and notarized, with the ticket stapled. - **Windows:** `.msix`, `.msi`, `.exe` or `.zip`, Authenticode signed by a trusted certificate. See [Package standard](https://appsxi.com/docs/package-standard). Run the same checks locally before uploading: ```sh npx appxi check dist/MyApp-1.2.0.dmg --app my-app ``` ## 3. Create the listing and upload Create the app in the dashboard (name, category, screenshots, price or free, trial length, license level), then upload a build from the app page, with [the CLI](https://appsxi.com/docs/cli), or from [GitHub Actions](https://appsxi.com/docs/github-action). Most decisions arrive within an hour. Every finding comes with a fix. Games also need a [content rating](https://appsxi.com/docs/content-ratings) before they go live. ## 4. Ship updates Point your updater at your AppsXi feed (Sparkle, WinSparkle, electron-updater or App Installer). Each approved build reaches installed copies within five minutes; a bad release can be rolled back from the dashboard. See [Updates](https://appsxi.com/docs/updates). ## 5. Licensing Free apps need nothing. Paid apps choose a level: - **Level 0:** AppsXi checks the purchase at download. No code in your app. - **Level 1:** your app validates a license key with AppsXi's API and gets a signed token it can verify offline. See [Licensing](https://appsxi.com/docs/licensing). - Already have your own license server? Use the [license bridge](https://appsxi.com/docs/license-bridge) so AppsXi sales create keys in your system. ## For AI coding tools These docs are available as plain text at [/llms.txt](https://appsxi.com/llms.txt) and [/llms-full.txt](https://appsxi.com/llms-full.txt). Connect your assistant to the remote AppsXi MCP server (`https://appsxi.com/api/mcp`) and the local one (`npx appxi mcp`), and run `npx appxi ai` in your project to add the AppsXi skills for Claude Code, a Cursor rule and an `AGENTS.md` section. Then ask: "Make this app AppsXi ready." See [MCP and AI tools](https://appsxi.com/docs/mcp). --- # Package standard AppsXi lists the exact file you upload. It never rebuilds, repackages or re-signs it, so the signature always names you. ## Formats | Platform | Accepted | Notes | | --- | --- | --- | | macOS | `.dmg`, `.pkg`, `.zip` | A `.zip` or `.dmg` contains the `.app`. A `.pkg` must be signed with a Developer ID Installer certificate. | | Windows | `.msix`, `.msi`, `.exe`, `.zip` | `.msix` is preferred: it installs cleanly and updates through App Installer. | Files are 1 byte to 4 GB. ## Which file to upload Upload one file per platform: the same installer you would put on your website. Buyers download exactly that file, so it must contain everything they paid for. - **A Mac app:** the `.dmg` or `.zip` with the `.app` inside. - **Audio plugins (AU, VST3, CLAP), drivers, or anything installed outside Applications:** a `.pkg`. - **An app and plugins you ship as two downloads:** combine them into one `.pkg` that installs both (or a `.dmg` holding that `.pkg`). Uploading only the plugins `.pkg` means buyers get only the plugins. - **Windows:** your signed installer, `.msix`, `.msi` or `.exe`, or a `.zip` of the signed app. ## macOS - Every executable is signed with a **Developer ID Application** certificate, with Hardened Runtime on. - The app (and the `.dmg` or `.pkg`) is **notarized** and the ticket is **stapled**: `xcrun notarytool submit --wait`, then `xcrun stapler staple`. - No blocked entitlements: `com.apple.security.cs.disable-executable-page-protection`, `com.apple.security.get-task-allow`. - Entitlements that allow code injection (`allow-dyld-environment-variables`, `disable-library-validation`) are reviewed by a person. - Library paths must not point to folders anyone can write to. ## Windows - The installer and the programs inside it carry a valid **Authenticode** signature from a trusted certificate (a cloud HSM such as Azure Trusted Signing is fine). - Bundled programs are built with ASLR and DEP. - Silent install is expected for `.msix` and `.msi`; for `.exe`, say in `appxi.json` how to install silently. ## Signer pinning The first approved build records who signs your app. A later build signed by someone else is blocked until you add that identity to your account (Developer → Profile → Signing identities). This protects your users if your build machine or account is ever compromised. ## Inside the app - **No hardcoded secrets.** Live API keys (Stripe, AWS, OpenAI, Anthropic, GitHub, Square and others) in the app are a denial: anyone can extract them. Call the service from your own server. - **Sell through AppsXi.** The AppsXi build of a paid app must not send buyers to an outside checkout. Link to your AppsXi listing or use the AppsXi helper. - **Updates come from AppsXi.** A self-updating app must point its updater at its AppsXi feed, or turn the updater off. See [Updates](https://appsxi.com/docs/updates). ## appxi.json Commit an [appxi.json](https://appsxi.com/docs/appxi-json) next to your project to declare what the app needs (network, permissions, background items). The pipeline compares it with what it finds. --- # appxi.json `appxi.json` describes your package. Generate it from a build and commit it: ```sh npx appxi package dist/MyApp-1.2.0.dmg --app my-app ``` `appxi package` fills in the facts it can read from the package and keeps the `requirements` and `licensing` blocks you wrote. ## Example ```json { "schema": "appxi.package/1", "app_id": "my-app", "name": "My App", "version": "1.2.0", "platform": "macos", "format": "dmg", "architectures": ["arm64", "x86_64"], "min_os": "13.0", "bundle_id": "com.example.myapp", "signing": { "team_id": "ABCDE12345", "certificate_name": "Developer ID Application: Example Inc (ABCDE12345)" }, "product_type": "app", "install": { "method": "dmg", "silent": true }, "requirements": { "network": { "mode": "optional", "domains": ["api.example.com"] }, "account": "none", "permissions": ["microphone"], "background": [], "runtimes": [], "data_collection": [] }, "updates": { "framework": "sparkle", "feed": "appxi", "channel": "stable", "ed_public_key": "…" }, "licensing": { "mode": "none" } } ``` ## Fields | Field | Meaning | | --- | --- | | `app_id` | Your app's AppsXi slug. | | `product_type` | `app`, `game`, `audio_plugin`, `extension` or `cli`. It decides which install and launch tests run. | | `requirements.network.mode` | `none`, `optional` or `required`; list the domains the app talks to. | | `requirements.account` | `none`, `optional` or `required`: whether people need an account with you. | | `requirements.permissions` | Camera, microphone, screen recording, accessibility, full disk access, location, contacts and so on. | | `requirements.background` | Login items, launch agents, services. | | `requirements.runtimes` | Anything people must install first (.NET, Java, a driver). | | `requirements.data_collection` | What the app sends to you (analytics, crash reports). | | `updates.framework` | `sparkle`, `electron_updater`, `winsparkle`, `app_installer` or `none`. | | `updates.channel` | `stable` or `beta`. | | `licensing.mode` | `none` (free or Level 0), `license_key` (Level 1) or `appxi_sdk` (Level 2, Sign in with AppsXi, coming later). | The listing shows the requirements to buyers. When the pipeline sees something the manifest does not declare, such as a launch agent or a permission, the build goes to a person. --- # Pipeline checks and fixes Every build goes through the same checks, usually in under an hour. Run them yourself first with `npx appxi check `. ## Outcomes | Status | Meaning | | --- | --- | | `approved` | Live for buyers, and in the update feed within five minutes. | | `limited` | Passed the automatic checks but needs a person to confirm one finding. | | `denied` | Not listed. Each finding says how to fix it; upload a new build. | | `frozen` | A listed file changed or a new detection matched. Downloads stop until a person reviews it. | | `withdrawn` | You rolled it back; the previous build is served again. | ## Checks 1. **Format and size.** The file type matches its contents and is under 4 GB. 2. **Hash.** AppsXi records the SHA-256 and SHA-512 of the exact file and shows it on the listing. 3. **Malware scan.** ClamAV on the file and everything inside it, signatures updated daily. Approved builds are rescanned as signatures change. 4. **Signature and notarization.** Developer ID and notarization on Mac, Authenticode on Windows. 5. **Signer pinning.** The signer must be on your account. 6. **Code integrity.** Hardened Runtime, entitlements, library paths, Electron fuses on Mac; signed bundled programs, ASLR and DEP on Windows. 7. **Secrets.** Hardcoded live keys and private keys. 8. **Purchase integrity.** Links to outside checkouts in a paid app. 9. **Updates.** The updater points at the AppsXi feed, and Sparkle updates carry a valid EdDSA signature. ## Fixes | Finding | Fix | | --- | --- | | Not notarized | `xcrun notarytool submit --wait`, then `xcrun stapler staple` the `.app` (and `.dmg`), and upload again. | | Not signed | Sign every executable with your Developer ID (Mac) or code signing certificate (Windows) before packaging. | | Signed by an identity not on your account | Sign with the identity on your account, or add this one under Profile → Signing identities. | | Entitlement | Remove the named entitlement (Xcode: Signing & Capabilities) and rebuild. | | Malware detection | If you believe it is a false positive, reply to the decision email; a person reviews it. | | Updater points outside AppsXi | Point the updater at your AppsXi feed, or turn it off, and upload again. | | Sparkle signature | Run `sign_update` on exactly the file you upload (after notarizing and stapling) and send its signature with the build (`--ed-signature` or `--sparkle-sign`). | | Hardcoded key | Remove it from the app and rotate it now; anyone can extract it. Call the service from your own server. | | Outside checkout | Link to your AppsXi listing or use the AppsXi helper's checkout, or remove the link from the AppsXi build. | | EdDSA key changed | Follow Sparkle's key rotation: sign the new version with the old key as well. A person reviews it. | Secret findings are shown only to you, never on the public listing. --- # Content ratings A content rating tells buyers, and parents, who an app is for. You answer a few questions about what the app shows, and AppsXi turns the answers into one age label. You rate the app yourself; AppsXi does not play it. A rating that does not match the app counts as a misleading listing. ## Who needs one - **Games** (the Games category, or apps you marked as a game) need a rating before they go live. An approved build waits until the rating is in; saving it publishes the app if nothing else is missing. - **Other apps** can add one if it helps buyers. Most tools do not need it. Fill it in under **Apps > your app > Overview > Content rating**. You can change it any time; the listing updates right away. ## The questions | Question | Answers | |---|---| | Violence | None · cartoon or fantasy · realistic (people or animals get hurt) · intense or graphic | | Blood | None · a little · lots of blood or gore | | Sexual content | None · suggestive themes · nudity or sexual scenes that are not explicit | | Strong language | None · mild · strong now and then · frequent strong swearing or slurs | | Drugs, alcohol and tobacco | None · mentioned or shown · used, or use is rewarded | | Simulated gambling | No · yes (casino-style games with no real money) | | Paid random rewards | None · paid random items that can't be cashed out | | Chat and shared content | No · people can chat or share things they made | | Sharing personal information | No · the app shares location or personal information with other people or companies | Explicit sexual content, betting real money, and selling random items or prizes with real-world value (they can be sold, traded for money or cashed out) are not allowed on AppsXi: the last two count as gambling with real money. ## The labels The label is the highest one any answer leads to. | Label | Comes from | |---|---| | **Everyone** | Nothing below, or only cartoon or fantasy violence, mild language, or paid random items that can't be cashed out | | **Teen (13+)** | Realistic violence, a little blood, suggestive themes, strong language now and then, drugs or alcohol mentioned or shown, simulated gambling | | **Mature (17+)** | Intense violence, gore, nudity or sexual scenes, frequent strong language, drug or alcohol use | | **Adults only (18+)** | Reserved. Nothing AppsXi lists today leads to it; it is for content AppsXi may allow later. | Chat and sharing personal information do not change the age. The listing shows them next to the label ("Users interact", "Shares info"), with the other answers that matter, like "Fantasy violence" or "Mild language". ## Where buyers see it - **Listing:** the label and the notes, next to "How this app was checked". - **Cards and charts:** a small 13+, 17+ or 18+ badge. Everyone has no badge. - **Mature and Adults only:** the listing asks visitors to confirm they are 17 (or 18) or older before it shows. The answer is kept on that browser for a year; AppsXi stores no birthday or other personal information. - **Adults only:** the app does not rank and is not promoted: it is left out of the charts, the weekly Top 10 lists, Launch Day and the home page. Search, the app list, category pages and your developer page show it only to visitors who have confirmed they are 18 or older. --- # Updates AppsXi hosts your update feed. Each approved build appears in it within five minutes, and nothing reaches installed copies without passing the pipeline first. ## Feed URLs ``` https://appsxi.com/updates////appcast.xml Sparkle 2, WinSparkle https://appsxi.com/updates////latest-mac.yml electron-updater (Mac) https://appsxi.com/updates//windows//latest.yml electron-updater (Windows) https://appsxi.com/updates//windows//.appinstaller MSIX App Installer ``` The beta feed includes stable releases, so beta users always get the newest build. ## Sparkle (macOS) In `Info.plist`: ```xml SUFeedURL https://appsxi.com/updates/my-app/macos/stable/appcast.xml SUPublicEDKey your-base64-public-key ``` Sign each release with your own EdDSA key, after notarizing and stapling, and send the signature with the build: ```sh npx appxi publish MyApp.dmg --app my-app --sparkle-sign /path/to/sign_update # or: --ed-signature "" ``` AppsXi verifies the signature against the `SUPublicEDKey` inside the app before approving. Sparkle compares `CFBundleVersion`, so raise it with every release. ## WinSparkle Call `win_sparkle_set_appcast_url` with the `windows` appcast URL. ## electron-updater ```yaml # electron-builder.yml publish: provider: generic url: https://appsxi.com/updates/my-app/${os}/stable ``` Use `mac` → `macos` and `win` → `windows` in the path (set the URL per platform if your config cannot map it). AppsXi serves `latest-mac.yml` and `latest.yml` with SHA-512 hashes of the exact files it checked. ## MSIX AppsXi serves an `.appinstaller` file for `.msix` builds. Install from it and Windows checks for updates on launch. ## Paid apps Feeds for paid Level 1 apps need a license-bound token. The license validation response includes it: ```json "updates": { "feed_token": "axf_…", "feed_base": "https://appsxi.com/updates/my-app", "appcast_url": "…/macos/stable/appcast.xml?token=axf_…" } ``` Set the feed URL at runtime (Sparkle: `feedURLString(for:)` in your updater delegate; electron-updater: `autoUpdater.setFeedURL` with `requestHeaders: { Authorization: "Bearer axf_…" }`). A refund revokes the token. Level 0 paid apps update from the buyer's AppsXi library. ## Rollback Developer → your app → Update feeds → **Roll back** withdraws the current build for that platform and channel. The previous approved build is served again within five minutes. Copies that already updated stay on the withdrawn version until the next release, which must have a higher version number. ## Staged rollouts Once an app has 1,000 installs, each approved update reaches a few copies first and everyone later. By default that is 1% of copies for a day, then 10% for a day, then everyone. A bad release then reaches few people, and you have time to pause it or roll back. - Copies are chosen by their license feed token (paid apps), their AppsXi Desktop session, or their network address on public feeds. The same copy stays in or out for the whole rollout. - Developer → your app → **Staged rollouts** sets the steps: each is a share of copies (1 to 99%, rising) and how long it lasts (1 hour, 6 hours, 1 day, 3 days, 1 week or 1 month), up to five steps, then everyone. A copy that has the update keeps it at the next step. Changes apply to the next approved update. - The same section chooses when to stage: Automatic (1,000 installs or more), Always, or Never. Use Never only if you have to ship fixes to everyone at once. - Developer → your app → Update feeds shows a running rollout, its step and when the next one starts, with **Pause** (stay at the current step until you decide; paused time does not count), **Resume** and **Release to everyone**. **Roll back** works as usual and ends the rollout. - AppsXi can pause a rollout while it investigates a report. ## Caching Public feeds are cached for up to five minutes in total. Token feeds are cached privately for one minute. During a staged rollout feeds are not cached, so each copy gets its own answer. --- # Pricing, sales and bundles Set everything in **Developer → your app → Pricing and licensing**, and bundles in **Developer → Bundles**. Prices are in US dollars. AppsXi's fee is 5% of what the buyer pays before tax, on every option below. ## Pricing models Each app uses one model. It shows on your listing, and buyers can filter the store by it (Free, Freemium, Free trial, Paid). | Model | What buyers get | How to set it | | --- | --- | --- | | **Free** | Everything, forever. Nothing to buy. | Tick **Free** under Price. | | **Freemium** | The app downloads free and keeps working; a paid upgrade unlocks more. | Set the upgrade price, make sure your app checks a license (Level 1 or 2, or [your own keys](https://appsxi.com/docs/own-license-server)), then turn on **Freemium** and list what is free and what the upgrade adds. | | **Free trial** | The full app free for 3, 7, 14 or 30 days, then they buy once. | Set a price, then choose the trial length and who runs it under Licensing. Your app must check a license. | | **Paid** | They pay once and keep it. | Set a price. | | **Pay what you want** | They choose the amount, at least your minimum. | Turn on Pay what you want. | **Trials never take a card.** A buyer starts one with their AppsXi account (not a throwaway email address), and it never charges them, so there is nothing to cancel. When it ends, the app locks, goes read only or switches to a limited mode (your choice under Licensing) until they buy. A trial can sit on top of a fixed price, pay what you want or freemium (a free trial of the upgrade). **A trial needs your app to check a license.** Nothing outside an installed app can stop it when a trial ends, so the trial setting only turns on when your app checks: an AppsXi trial at Level 1 or 2 (after we see your app check a test license once), or a trial your app or your server runs against your own keys. See [Using your own license server](https://appsxi.com/docs/own-license-server). **Freemium needs your app to check the license.** Anyone can download a freemium app and get its updates, so the app itself decides what to unlock: with [Level 1](https://appsxi.com/docs/licensing) it checks the buyer's key, with [Level 2](https://appsxi.com/docs/sign-in-with-appxi) it reads the signed entitlement. Apps with their own license server can do the same with [their own keys](https://appsxi.com/docs/own-license-server). An app that checks nothing cannot tell free users from buyers, so AppsXi does not let it turn on freemium. Buyers see your two lists on the listing before they download, so keep each line short and specific: "Up to 3 projects", "Batch export". AppsXi does not offer subscriptions or trials that charge a card when they end. Every purchase is paid once. ## Fixed price Free, or $2.99 to $1,000. A new price applies to new purchases right away; buyers keep what they bought. Changing the price ends any running sale, so a sale can never show a "was" price that is not real. ## License types Everyone can buy the **personal** license at your price. Under **License types** on the Pricing page you can add up to three more, each with its own price. Buyers choose one on your listing and can change it at checkout. | License | For | Price rule | | --- | --- | --- | | **Personal** | One person, at home or for their own work. | Your price above. Always offered. | | **Commercial** | Use at a company or for paid client work. | At least your personal price, up to $5,000. | | **Team** | One purchase that covers a set number of computers at work, for example 5 or 10. | At least your personal price, up to $5,000. More computers than a personal license, up to 100. | | **Education** | Students, teachers and schools. | From $0.99 to less than your personal price. | - **Education is for school email addresses.** Only buyers whose confirmed sign-in email ends in `.edu`, `.edu.` (for example `.edu.au`), `.ac.` (for example `.ac.uk`) or `.k12..us` can buy it; AppsXi checks this when it prices the order, not just on the page. Schools whose addresses end differently cannot use it yet; send those buyers a coupon code instead. - **Sales and codes take the same share off every license.** A sale that takes 25% off your personal price takes 25% off the team license too, and a code for $5 off a $20 app takes 25% off any license. A 100% off code (free grant) is the exception: it gives the personal license only, so a giveaway never hands out a free team or commercial license. - **Pay what you want and bundles sell the personal license only.** Free apps have no license types. Your other licenses stay saved and come back when you set a fixed price. - **The license records its type and computers.** A team license for 10 computers has `seats_max: 10` in the [license API](https://appsxi.com/docs/licensing) and in Sign in with AppsXi, so your app counts seats for you. At Level 0 nothing in the app counts computers: the number is what the license allows. - **Team licenses need AppsXi keys or the license bridge.** If buyers get [your own uploaded keys](https://appsxi.com/docs/own-license-server), each purchase gets one code, so a team could not get its seats; AppsXi hides the team license while your own keys are on. The [license bridge](https://appsxi.com/docs/license-bridge) sends `license.seats` with each purchase, so your server can issue a key for the right number of computers. - **A trial becomes the license bought.** Someone on a trial who buys the team license keeps the same key, now with the team's computers. The exception is a purchase whose key is held for a day (a brand-new account with a risk signal): it gets a new key, and the trial key stops working, so a key seen during the trial cannot be resold as a paid one. - Switching a license off hides it from buyers and keeps its price for later. Licenses already bought keep their type. - Changes to license prices are kept in your app's price history. To open checkout with a license chosen, add `license` to the link: `/checkout/your-app?license=team`. ## Pay what you want Set a **minimum** ($0.99 or more) and a **suggested** price. Buyers see "Pay what you want, from $3" on your listing and type their amount at checkout, starting at the suggested price. Anything from the minimum to $1,000 is accepted. - Coupon codes and sales do not apply to pay what you want prices. - The app ranks on the paid charts. Each purchase counts as one sale at the amount paid. - To go back to a fixed price, save a price again. ## Sales and launch discounts A sale shows your list price struck through with a real end time. Because the struck-through price is a claim about your regular price, AppsXi only allows it after the app has sold at that list price for **30 of the last 90 days**. A **launch discount** is the exception: it runs in your first 7 days (or around a scheduled Launch Day) and shows as "launch price" without a struck-through price. Sales run for up to 31 days and cannot overlap. ## AppsXi sales events AppsXi runs store-wide sales a few times a year, each with its own page at `/sales/`, a banner on the home page and a minimum discount (for example 20%). Open events are listed under **Sales** on your app's Pricing page: 1. Enter your sale price (it must be at least the event's minimum off your list price) and click **Join** before the join date. 2. The price drops for everyone when the event starts and goes back when it ends. The dates never move once apps have joined. 3. You can leave before the event starts. After it starts, you can end your sale early like any other sale. The 30 of 90 days rule applies to event sales too. ## Coupon codes Percent off, amount off, or a free grant (100% off, capped at 500 a month per app and never counted on the charts). Share links like `/checkout/your-app?code=LAUNCH25`. A code applies to the list price and is used only when it beats a running sale. On a commercial, team or education license it takes the same share off as it would from the personal price; a free grant code works on the personal license only. ## Bundles Combine 2 to 20 of your apps with fixed paid prices at one price lower than buying them separately. **Developer → Bundles** creates a draft; publish it when it looks right. The bundle gets a page at `/bundles/` and shows on each included app's listing. - **Buyers who already own some of the apps pay only for the rest**, at the bundle's discount rate. For example, a $24 bundle of a $10 app and a $20 app (20% off) costs $16 for someone who owns the $10 app. - One payment, and one license per app, each with that app's own seats, terms and license key or Sign in with AppsXi entitlement. - Each app's share of the payment is in proportion to its list price. On the charts, the sale counts for every app in the bundle with its share; a share below half the app's list price counts as half a sale, like any deep discount. - A refund of a bundle refunds the whole order and revokes all of its licenses. - Every app must be live. If one is paused, the bundle cannot be bought until it is back. Bundles mix only your own apps. Multi developer bundles need a separate design for splitting payments and come later. ## Cart Buyers can add paid apps from different developers to a cart and pay for them with one card entry. Square sends each payment to one seller, so the cart does not make one combined charge: - **Each app in a cart is a normal single-app order on your Square account**: your price, the same 5% AppsXi fee, its own receipt and license, and the same refund rules as **Buy now**. The buyer's card statement shows one line per app. - **The cart sells the personal license at today's price**, with any running sale. Commercial, team and education licenses, coupon codes, bundles and pay what you want prices stay on your app's own checkout, which the cart links to. - Pay what you want apps, apps that are sold out of your own keys, and apps whose developer cannot take payments right now cannot be bought through the cart. If your app stops selling after a buyer added it, it stays in their cart with a note and is skipped at checkout. - One payment covers up to 5 apps; the rest wait in the buyer's cart. When one app's charge fails, the others still go through, and an app is never charged twice. - Cart orders show as **Cart** under **Where sales come from**. An affiliate link the buyer clicked for your app still counts. ## Receipts Every order has a printable receipt in the buyer's library (**Receipt** under the app): the order number and date, you as the seller, the app with its license type and computers, the price, any discount, refunds and the total, with AppsXi named as the marketplace. Buyers can add a company name, tax ID and address for their own records; they appear only on that receipt. Business accounts appear under their registered business name; individual developers appear under their store name, so their personal legal name stays private. Square also emails its own card receipt. ## Taxes You are the seller, so the taxes on your sales are yours: income tax, and sales tax wherever you owe it. AppsXi does not add sales tax at checkout, collect it or withhold anything from your sales. Square, which pays you, may report your sales to the IRS on Form 1099-K as the law requires. If a state's law ever requires AppsXi itself to collect sales tax on marketplace sales, we will tell you at least 30 days before it starts. ## Where a purchase came from Checkout links can carry a source: `?src=app&from=export_paywall`. The SDKs add `src=app` to `checkoutUrl()` and the `from` you pass to `openCheckout()`. AppsXi records both on the order so you can see which paywall moments convert. --- # Licensing Three levels, picked per product in Pricing and licensing. Most new apps should use **Level 2, [Sign in with AppsXi](https://appsxi.com/docs/sign-in-with-appxi)**: one button, in-app trials and automatic unlock after purchase. ## Level 0: no code AppsXi checks the purchase when the buyer downloads. Each purchase gets a license key in the buyer's library, and refunds and chargebacks revoke it. Good for apps that do not need copy protection. ## Level 1: license key API Each purchase gets a key like `AXI-7KQ2M-9WD4T-1HXZ8-C3PNB`. Your app validates it once, stores the signed token, and checks the token offline after that. ### Product keys In Developer → your app → Licensing you get two publishable product keys: - `pk_test_…` works only with sandbox licenses (test purchases and sandbox trials). - `pk_live_…` works only with real licenses. A product key can only validate; it cannot read buyer data, so it is safe to ship in the app. ### Validate ```http POST https://appsxi.com/api/v1/licenses/validate Authorization: Bearer pk_live_… Content-Type: application/json { "license_key": "AXI-7KQ2M-9WD4T-1HXZ8-C3PNB", "device_id": "", "device_name": "Studio Mac", "platform": "macos" } ``` Send a hash of the machine id, never the raw id. A valid response: ```json { "valid": true, "status": "active", "license": "lic_…", "seats_used": 1, "seats_max": 2, "license_type": "personal", "trial_expires_at": null, "token": "", "updates": { "feed_token": "axf_…", "feed_base": "…", "appcast_url": "…" } } ``` When `valid` is false, `reason` is one of `unknown_product_key`, `unknown_license`, `seat_limit`, `trial_used_on_device`, `device_hash_required`, `rate_limited`, or the license status (`revoked` after a refund or chargeback, `expired` after a trial ends). ### Offline tokens `token` is a compact JWT signed with Ed25519 (`alg: EdDSA`). Verify it with AppsXi's public key from [/api/v1/licenses/public-key](https://appsxi.com/api/v1/licenses/public-key) (a JWK set; embed it in your app). Claims: | Claim | Meaning | | --- | --- | | `sub` | Pseudonymous buyer id, stable per app. | | `product` | Your app. | | `license` | License id. | | `status` | `active` or `trial`. | | `env` | `live` or `sandbox`. | | `seats` | `{ this_device, max }`. | | `type` | The license type: `personal`, `commercial`, `team` or `education`. | | `exp` | 30 days for a license; never past a trial's end. | Validate again when the token is close to `exp`. If the network is down, keep running until `exp`. ### Helpers Single-file helpers do the validation, offline check, trial status, checkout link and feed URL for you. Copy the file into your project: | Language | File | Needs | | --- | --- | --- | | TypeScript (Electron, Node 18+) | [appxi-license.ts](https://appsxi.com/sdk/appxi-license.ts) | nothing | | Swift (macOS 12+) | [AppXiLicense.swift](https://appsxi.com/sdk/AppXiLicense.swift) | nothing beyond the system frameworks | | C++ / JUCE | [appxi_license.hpp](https://appsxi.com/sdk/appxi_license.hpp), [appxi_license.cpp](https://appsxi.com/sdk/appxi_license.cpp) | OpenSSL; libcurl or your own HTTP function (for example `juce::URL`) | | .NET 6+ | [AppXiLicense.cs](https://appsxi.com/sdk/AppXiLicense.cs), [AppsXi.License.csproj](https://appsxi.com/sdk/AppsXi.License.csproj) | BouncyCastle.Cryptography | See the [helpers README](https://appsxi.com/sdk/README.md) for what each call returns. ```swift let license = AppXiLicense(productKey: "pk_live_…", publicKeys: [""], appSlug: "my-app") _ = try await license.activate(key: enteredKey) // once, online if let e = license.current() { unlock(e) } // every launch, offline if license.needsRefresh() { _ = try? await license.refresh() } ``` ### Seats Each `device_id` takes one seat. Buyers free seats from their AppsXi library. `seats_max` comes from the license the buyer bought: your **Computers per license** setting for personal, commercial and education licenses, and the team's number of computers for a team license (see [License types](https://appsxi.com/docs/pricing#license-types)). You do not need to do anything for team licenses: the same key simply activates on more computers. ### Trials Set a trial of 3, 7, 14 or 30 days on the app and choose **AppsXi** as who runs it. People start a trial from the listing and get a trial key; the response has `status: "trial"` and `trial_expires_at`, then `expired` when it ends. One trial per device. When they buy, the same key becomes a full license. A trial goes on only after AppsXi has seen your app check a license once: issue a test trial key on the Sandbox tab and activate it in a development build. At Level 0 there is no AppsXi trial, because nothing in the app would stop it; apps with their own license server can run their own trial instead (see [Using your own license server](https://appsxi.com/docs/own-license-server)). ### Sandbox Test purchases in the Square sandbox create sandbox licenses that only `pk_test_` keys accept. Use them in development builds. Your app → **Sandbox** issues test keys without a purchase (full licenses or trials, any seat count) and lets you: - move a trial's clock forward a day or a week, expire it now, or restart it; - convert a trial to a full license, as a purchase does; - revoke a key, as a refund does, and restore it; - free all seats; - send `purchase.created` for a test key to your [license bridge](https://appsxi.com/docs/license-bridge), and replay any past bridge event. Sandbox trials use their own one-trial-per-device record, so testing never uses up a device's real trial. Sandbox licenses never count toward sales, reviews or rankings. ## License types Besides the personal license, you can sell commercial, team and education licenses with their own prices, set under **License types** on the Pricing page. The type is a matter of the license terms; AppsXi enforces two things for you: - **Seats:** a team license's `seats_max` is the number of computers it covers, at Level 1 and Level 2. - **Who can buy education:** only buyers with a confirmed school email (`.edu`, `.edu.`, `.ac.`, `.k12..us`). The check runs in the database when the order is priced. The buyer's library and receipt show the type. Your app can read it too: `license_type` in the license API and Sign in with AppsXi answers, `type` in the offline token, and `license.type` (with the computers as `license.seats`) in [license bridge](https://appsxi.com/docs/license-bridge) events. Team licenses need AppsXi keys or the bridge, not your own uploaded keys, because uploaded keys are one per purchase. Details and price rules are in [Pricing](https://appsxi.com/docs/pricing#license-types). ## Already have a license system? Upload a batch of your own keys or single-use coupon codes, or use the [license bridge](https://appsxi.com/docs/license-bridge): AppsXi sends each sale to your server, and the key you return appears in the buyer's library. See [Using your own license server](https://appsxi.com/docs/own-license-server). --- # 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 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](https://appsxi.com/sdk/README.md) and embed AppsXi's public key (the `x` value from [`/api/v1/licenses/public-key`](https://appsxi.com/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:/…`) 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](https://appsxi.com/docs/licensing). ## 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) ```ts import { AppsXi } from "./appxi-signin"; const appxi = new AppsXi({ clientId: "pk_live_…", publicKeys: [""], 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: [""]) 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 ```cpp appxi::SignIn sia({"pk_live_…", {""}, 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 ```csharp var appxi = new AppXiSignIn("pk_live_…", new[] { "" }, 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": "", "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`](https://appsxi.com/.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: ```json { "token_type": "Bearer", "refresh_token": "axr_…", "status": "active", "entitlement": "", "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/.json` on a Mac and `%APPDATA%\AppsXi\Tickets\.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. --- # 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" } } } ``` | 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](https://appsxi.com/docs/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=,v1=`, where `v1` is HMAC-SHA256 of `"."` with your secret. Reject requests older than five minutes. ```js 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. --- # Using your own license server If your app already activates with your own license keys, you do not have to change it to sell on AppsXi. Buyers pay on AppsXi and get a key your app already accepts. ## Three ways to give buyers your key | Way | How it works | Best when | | --- | --- | --- | | **Upload your keys** | You make a batch of keys on your server and paste them in. Each buyer gets the next one the moment they pay, in their AppsXi library. | Your server can create keys ahead of time. This is the simplest for buyers. | | **Upload 100%-off coupon codes** | You make single-use codes that take the full price off in your own checkout. The buyer pays on AppsXi, then uses the code on your site to get their key. | Your keys are only issued by your own checkout. | | **[License bridge](https://appsxi.com/docs/license-bridge)** | AppsXi sends each sale to your server, and the key your server answers with appears in the buyer's library. | You can add one endpoint to your server. | Set this up in Developer → your app → Pricing and licensing → **Keys buyers get**. Choose **My own keys or codes**, say whether they are license keys or coupon codes, and add the address where buyers use them (required for coupons). Your app stays at Licensing Level 0, because your server does the checking. Use uploaded codes or the bridge, not both. ### Coupon codes - Make every code **single-use** (one redemption each), so a code that leaks costs one sale, not hundreds. - Limit each code to the product it is for, and make it take off 100%. - The buyer's library tells them where to use it, and that it covers the full price. ### When codes run out - You get a warning on the page when fewer than 10 are left. - At zero, your listing says **Sold out for now** and checkout is closed until you add more. AppsXi never takes money for a key it cannot hand out. - If someone's payment completes as the last code goes, they see "your key is on its way" and get the next code you add, automatically. ### Bad codes and refunds - Every buyer can press **My code didn't work** in their library. That marks the code, tells you, and refunds them if the purchase is within 14 days. - When two different buyers who paid report bad sale codes within 30 days, **sales pause** until you check your codes and turn sales back on. Each code counts once, reports on trial codes mark the code but never pause sales, and reports from before you turned sales back on do not count again. - With trial codes and the license bridge, a buyer who pays stops seeing their trial code; your server's key for the purchase replaces it. - Refunded codes are listed on the same page so you can turn them off on your server. - Before your listing goes live, AppsXi keeps one real code back and the review team redeems it to check that it works. It is never sold. ### Testing Sandbox (test) purchases use the test codes you upload for sandbox, or a generated `TEST-…` code when you have none, so testing never uses up real codes. ## Free trials Nothing outside an installed app can stop it working when a trial ends. Only a check inside the app can. So AppsXi turns a trial on only when your app checks licenses. Pick **Who runs the trial** under Licensing: | Choice | What happens | Needs | | --- | --- | --- | | **My app runs its own trial** | The listing shows "Free for 7 days" and anyone can download the app. Your app starts its own trial when it first opens, and locks itself when the trial ends. Buying gets a key that unlocks it. | Your own keys (uploaded or the bridge), or Level 1 or 2. | | **Trial codes from my server** | You upload trial codes too. "Try free" hands out one code, and your server ends the trial. Buying swaps the trial code for a full key. | Your own keys (uploaded or the bridge), plus uploaded trial codes. The trial button hides when trial codes run out. | | **AppsXi trial** | AppsXi issues a trial key or entitlement; your app checks it with the [license API](https://appsxi.com/docs/licensing) or [Sign in with AppsXi](https://appsxi.com/docs/sign-in-with-appxi), and the answer turns to "expired" when the trial ends. | Level 1 or 2, and we must have seen your app check a test license once (Sandbox tab). | Trials never take a card and never charge, so buyers have nothing to cancel. ### Why Level 0 cannot have an AppsXi trial At Level 0 your app does not check anything, so a trial would hand out the full app with nothing to stop it. AppsXi can stop downloads and updates, but not a copy that is already installed. That is why the trial setting refuses Level 0 unless your app runs its own trial against your own keys. ## Example: an app with its own activation server An audio plugin activates with keys like `JP-CHAIN-…` from its own server, and runs a 7-day trial from its own account page. 1. Make 200 keys on your server and upload them as **Codes for buyers**. 2. Under Licensing, choose a 7-day trial and **My app runs its own trial**. Better still, start the trial inside the app instead of on your website, so buyers do not need an account on your site. 3. Point your app's "Buy" button at your AppsXi listing for the copies you sell on AppsXi. 4. Add more keys when the page warns you, and turn refunded keys off on your server. --- # Backend Kit Most desktop apps eventually need a little backend: a switch to turn a feature off, a place to sync a buyer's presets, crash reports, a way to call an AI model without shipping your API key. The Backend Kit gives every AppsXi app those pieces, included with AppsXi. Set it up in **Developer → your app → Backend Kit**. | Piece | Needs | What it does | | --- | --- | --- | | Remote config and flags | Product key | JSON config and on/off flags, with percentage rollouts and version limits | | Storage | Sign in with AppsXi | Per-buyer key-value storage, shared by all their computers | | Crash reports | Product key, buyer opt-in | Grouped crashes with home folder names removed | | Usage counts | Product key, buyer opt-in | Anonymous daily event counts and active computers | | AI proxy | Sign in with AppsXi | OpenAI and Anthropic calls with your key kept on AppsXi | | Announcements | Product key | Short notes shown in the app and in buyers' libraries | The kit uses the same product key as [Sign in with AppsXi](https://appsxi.com/docs/sign-in-with-appxi). Test keys (`pk_test_…`) see the sandbox config and only sandbox data, so you can rehearse without touching live buyers. ## TypeScript (Electron, Node) Copy [`appxi-signin.ts`](https://appsxi.com/sdk/appxi-signin.ts) and [`appxi-kit.ts`](https://appsxi.com/sdk/appxi-kit.ts) into your project. ```ts import { AppsXi } from "./appxi-signin"; import { AppXiKit } from "./appxi-kit"; const appxi = new AppsXi({ clientId: "pk_live_…", publicKeys: [""], storageDir: app.getPath("userData") }); const kit = new AppXiKit(appxi, { appVersion: app.getVersion() }); await kit.config(); // at launch; offline it uses the last answer if (kit.flag("new_editor")) enableNewEditor(); const limit = kit.value("export_limit", 10); await kit.storage.put("presets", presets); // signed-in buyers with a license or trial const saved = await kit.storage.get("presets"); // null when there is nothing yet if (settings.sendCrashReports) process.on("uncaughtException", (e) => kit.reportCrash(e)); if (settings.shareUsage) kit.track("export"); // batched; call kit.flush() on quit const res = await kit.ai("anthropic", "messages", { model: "claude-haiku-4-5", max_tokens: 400, messages: [{ role: "user", content: "Summarize this clip's notes: …" }], }); for (const a of await kit.announcements()) showNote(a.title, a.body, a.url); ``` Other languages call the HTTP endpoints below directly. They are plain JSON over HTTPS with CORS open, so they also work from a Tauri or web front end. ## Remote config and feature flags In the dashboard, each environment (live and sandbox) has a **config** (any JSON object up to 64 KB) and **flags**: ```json { "new_editor": { "enabled": true, "rollout": 25, "min_version": "2.1" }, "legacy_export": { "enabled": false } } ``` - `rollout` (0 to 100) turns a flag on for that share of computers. It is stable: the same computer stays in or out as you raise it from 5 to 25 to 100. - `min_version` and `max_version` compare against the app version you send (`2.10` is newer than `2.9`). - The app gets flags already resolved to `true` or `false`, so your rollout rules are not visible to buyers. ```text GET /api/v1/kit/config?client_id=pk_live_…&device_id=&app_version=2.1.0 → {"config": {"export_limit": 10}, "flags": {"new_editor": true, "legacy_export": false}, "version": 7, "environment": "live"} ``` Check once at launch and, if you like, every few hours. Up to 240 checks an hour per computer. ## Storage Per-buyer key-value storage for settings, presets and sync state: the same data on every computer the buyer signs in on. Values are any JSON up to 64 KB, with up to 500 keys and 2 MB per buyer per app. Reading works for any signed-in buyer; writing needs a license or a running trial. Refunds do not delete data, but writes stop. Send the Sign in with AppsXi session with every call: ```text Authorization: Bearer axr_… X-AppXi-Client: pk_live_… X-AppXi-Device: ``` | | | | --- | --- | | List keys | `GET /api/v1/kit/storage` | | Read | `GET /api/v1/kit/storage/` → `{"key", "value", "version", "updated_at"}`, or 404 `not_found` | | Write | `PUT /api/v1/kit/storage/` with `{"value": …, "if_version": 3}` → `{"key", "version"}` | | Delete | `DELETE /api/v1/kit/storage/` (optional `If-Match: 3`) | Keys are 1 to 128 letters, digits or `. _ : / -`. `if_version` (or `If-Match`) writes only if the stored version still matches, so two computers cannot silently overwrite each other: a stale write answers 409 `{"error": "conflict", "version": 4}`. Read again, merge, and write with the new version. Use `0` to write only if the key does not exist yet. The buyer's data belongs to the buyer: AppsXi does not show it to you in the dashboard, only the totals. Deleting an AppsXi account deletes it. ## Crash reports Ask the buyer first, for example with a "Send crash reports" setting that is off by default, and only report while it is on. ```text POST /api/v1/kit/crash {"client_id": "pk_live_…", "device_id": "…", "app_version": "2.1.0", "title": "TypeError: x is undefined", "stack": "…", "os": "macos", "os_version": "15.1", "arch": "arm64", "context": {"screen": "export"}} ``` - AppsXi replaces home folder names in stacks (`/Users/jane/…`, `C:\Users\jane\…`) before storing anything. - Reports are grouped by the title and the top frames, with memory addresses removed. The dashboard shows each group's count, computers, versions and the latest stack. - Mark a group resolved with the version that fixed it. If it happens again in a newer version, it reopens. - Up to 60 reports a day per computer. The 25 most recent reports per group are kept for up to 90 days, and groups not seen for 180 days are removed. ## Usage counts Anonymous daily counts of named events, plus how many computers were active each day. No buyer identity, no properties and no free text, by design. Ask the buyer first. ```text POST /api/v1/kit/track {"client_id": "pk_live_…", "device_id": "…", "app_version": "2.1.0", "events": [{"name": "export", "count": 3}]} ``` Event names are 1 to 64 letters, digits or `_ . : -`, up to 200 different names a day. Send a batch every few minutes rather than one request per event. Only live keys appear in the dashboard. ## AI proxy Ship AI features without putting your OpenAI or Anthropic key in the app, where anyone could extract it. Your app calls AppsXi with the buyer's session; AppsXi checks that they own the app (or are on a trial, if you allow it), checks your model list and daily limits, caps the answer's length, and forwards the request with your key. Answers, including streams, come back as the provider sent them. | Provider | Endpoints | | --- | --- | | OpenAI | `POST /api/v1/kit/ai/openai/chat/completions`, `/responses`, `/embeddings` | | Anthropic | `POST /api/v1/kit/ai/anthropic/messages`, `/messages/count_tokens` | The body is the provider's own request format. Send the same three headers as storage. For Anthropic, `anthropic-version` defaults to `2023-06-01`, and `anthropic-beta` is passed through except for betas that reach your account's files, containers or hosted tools. What the proxy changes or refuses, to protect your bill: - The answer's length is capped at your **longest answer** setting (default 4,096 tokens): `max_tokens` (Anthropic and older OpenAI chat), `max_completion_tokens` or `max_output_tokens` are lowered to it, or set to it when missing. - `n` must be 1 for chat completions. - Tools that the provider runs and bills itself are refused: OpenAI web search, file search, code interpreter, image generation, hosted MCP and computer use; Anthropic web search, web fetch, code execution and MCP servers. Function tools, which your app runs, work as usual. Set up in the dashboard: 1. Paste an API key for each provider you use. Keys are encrypted at rest and never shown again. Set a spending limit at the provider too. 2. Choose the limits: requests per buyer per day (default 100), tokens per buyer per day (optional), requests per day for the whole app (default 20,000), the longest answer per request, and whether trial users may use AI. While a request runs, its longest possible answer counts against the buyer's daily tokens, so many requests at once cannot go over the limit; the real usage replaces it when the answer ends. 3. List the models you allow, for example `gpt-5-mini, claude-haiku-*` (required). 4. Turn the proxy on. Errors from AppsXi: | Status | `error` | Meaning | | --- | --- | --- | | 401 | `invalid_grant` | Sign in again | | 402 | `not_entitled` | No license or trial (or trials are not allowed AI) | | 403 | `not_enabled`, `model_not_allowed` | The proxy is off, there is no key for this provider, or the model is not in your list | | 413 | `too_large` | Requests are limited to 1 MB | | 429 | `rate_limited` | A daily limit was reached; limits reset at midnight UTC | | 503 | `temporarily_unavailable` | Try again shortly | Errors from the provider come back as `upstream_invalid_request` (with the provider's message, minus anything that identifies your account), `upstream_rate_limited` (429), `upstream_auth` (502: your key was refused) or `upstream_unavailable` (502), with `provider_status`, `type` and `code` from the provider. Requests time out after about two minutes. If a buyer disconnects or a stream is cut off before the provider reports usage, the output is estimated from the text sent so far. Usage (requests and tokens per day, and your heaviest buyers by anonymous ID) is in the dashboard. You pay the provider directly; AppsXi does not mark up or resell model usage. ## Announcements Short notes from you to people who own or are trying the app: a new version, a known issue, a survey. Up to three a day, each shown for 1 to 365 days, optionally only to some versions. They appear in the app through the SDK and in buyers' AppsXi libraries. Not for advertising other products. ```text GET /api/v1/kit/announcements?client_id=pk_live_…&app_version=2.1.0 → {"announcements": [{"id": "…", "title": "Version 2.2 adds batch export", "body": "…", "url": "https://…", "created_at": "…"}]} ``` ## Limits and privacy - Everything is per app and per environment: test keys never see live data. - The device id is the same hash Sign in with AppsXi uses; AppsXi stores a second hash of it per app for crash and usage counts. - Usage counts and crash reports carry no buyer identity. Storage and AI usage are tied to the buyer's anonymous per-app ID, the same one the licensing API returns. - Rate limits answer 429 with `{"error": "rate_limited"}`. Back off and try later. --- # Affiliates Creators, reviewers and bloggers who like your app can promote it through AppsXi Affiliates. When a buyer purchases through an affiliate's link, Square splits the payment at the moment of sale: you are paid, AppsXi takes its 5%, and the affiliate's commission goes straight into the affiliate's own Square account. There are no monthly payouts, invoices or tax forms for you to handle: affiliates handle the taxes on their own commissions. ## Turn it on In **Developer → your app → Pricing and licensing → Affiliates**: 1. Tick **Run an affiliate program**. 2. Choose the commission, from 5% to 50% of the sale price after discounts. 3. Optionally leave a note for affiliates, for example "Review copies on request". Affiliates see your app in their dashboard right away and can share links such as `appsxi.com/a/theircode/your-app`. Turning the program off stops new commissions immediately; sales already made are not affected. Programs are for paid apps sold one at a time. Bundles and free apps are not eligible. ## The money Example: a $20 sale with a 20% commission. | Line | Amount | | --- | --- | | Buyer pays | $20.00 | | Square processing (3.3% + 30¢) | $0.96 | | AppsXi fee (5%) | $1.00 | | Affiliate commission (20%) | $4.00 | | You receive | $14.04 | Technically, AppsXi creates the payment in your Square account with `app_fee_allocations`: one share to AppsXi's location and one to the affiliate's. Square's limit on application fees (90% of payments of $5 or more, 60% below) is never reached, because commissions stop at 50%. ## Attribution - A click on an affiliate link leaves a cookie for that app for 30 days. A purchase in that window is attributed to the affiliate. - **Last click wins**: if the buyer later clicks a different affiliate's link for the same app, that affiliate gets the sale. - Sales made from inside your app with the SDK's `openCheckout()` are attributed only when the buyer's browser carries a recent affiliate click. - Affiliate sales count toward the charts like any other sale. ## Rules AppsXi enforces - **No self referrals.** No commission when the affiliate is the buyer, when the affiliate belongs to your developer account, when the affiliate's Square account is your own, or when the purchase comes from the network the affiliate used that day. - **Refunds need the affiliate's Square account.** Square refunds a shared payment through the same accounts, so affiliates cannot disconnect Square while a referred sale is inside its 14-day refund window. If a refund still fails (for example the affiliate revoked AppsXi in Square), AppsXi support refunds it from your Square Dashboard. - **Refunds.** A refund within 14 days of the sale takes the affiliate's commission back in proportion (a 50% refund reverses half). After 14 days the affiliate keeps it, and the refund comes from your share and AppsXi's as usual. - **Disclosure.** Affiliates agree to disclose the relationship wherever they share a link, as the FTC's endorsement guides require. The share text in their dashboard already says "affiliate link". - **Conduct.** No spam, misleading claims, fake reviews or ads bidding on your app's name. AppsXi suspends affiliates who break the rules; tell us at [/report](https://appsxi.com/report) if you see a problem. - **Square required.** Affiliates must connect their own Square account, where Square verifies their identity. Until they do, their clicks are counted but sales carry no commission. ## What you see - **Pricing and licensing → Affiliates**: each affiliate who has linked to your app, with clicks, sales, revenue and commissions for the last 90 days. - **Pricing and licensing → Where sales come from**: paid sales by source: the AppsXi store, your app's paywalls (the `source` you pass to `openCheckout()`), affiliate links and AppsXi Desktop. - **Orders**: each referred order shows the affiliate's code and commission. ## For affiliates Join at [/affiliates](https://appsxi.com/affiliates). You choose a code, connect Square (two-step sign-in required) and copy links from your dashboard. If you also sell apps on AppsXi, you can have commissions paid into your developer account's Square instead of connecting a second one. Commissions are your income: you handle the taxes on them, and AppsXi does not withhold tax or send tax forms. --- # Sponsored placements A sponsored placement shows one of your live apps in a clearly labeled **Sponsored** slot above the results on a category page, or when buyers search for words you choose. It is the only paid placement on AppsXi. ## The rules - There is at most one sponsored slot on a page, always labeled **Sponsored** with a link to [what that means](https://appsxi.com/ads). - Sponsorship never changes the charts or the order of results. Rankings are earned (see [how rankings work](https://appsxi.com/top/methodology)). - When several campaigns target the same page, they take turns hour by hour. - If your app is already in the top three results on that page, the slot is skipped rather than showing it twice. - Only verified developers (identity checked) with no strikes can sponsor, and only live apps. If your app is frozen, withdrawn or gets a strike, its campaigns stop showing automatically. - A campaign runs for up to 90 days. ## Request a placement 1. Go to **Developer → Sponsored**. 2. Choose the app and where it shows: - **A category page**: the slot appears on that category, its parent category and its subcategories. - **Search words**: up to ten words or phrases. The slot appears when a search contains every word of one of them (for example `screen recorder` matches "best screen recorder for mac"). 3. Optionally add a short line (up to 80 characters) to show under the name instead of your short description. 4. Choose the dates and send the request. AppsXi reviews each request, replies with the price and, once agreed, approves it. Payment is by invoice for now. The agreed price is shown on the campaign. ## While it runs You can pause, resume or cancel a campaign at any time from **Developer → Sponsored**. A campaign AppsXi paused (for example after a complaint) stays paused until AppsXi resumes it. Views and clicks are counted per day (a view counts once per visitor per hour, a click once per ten minutes) and shown next to the campaign. Nothing that identifies a buyer is stored. Clicks go through a short `/s/…` link on AppsXi, which records the click and sends the buyer to your app's page. Sponsored links carry `rel="sponsored"` for search engines. --- # Charts and badges Every category and subcategory on AppsXi has its own charts, recomputed every hour: | Chart | Window | What ranks | |---|---|---| | Top Paid | Last 7 days | Verified paid sales, unique buyers, revenue, rating | | Top Free | Last 7 days | Store downloads of free indie apps, rating | | Rising | This week vs last week | Apps first published in the last 60 days, by growth | | Top Free (Open Source) | Last 7 days | Open source shelf apps, kept apart from indie apps | | Top Grossing | Last 30 days | Revenue after refunds (never shown) | | Top Rated | All time | Bayesian average of verified reviews, 5 or more | | All Time | Since launch | Verified paid sales | The full formula, the weights and the exclusions are published at [/top/methodology](https://appsxi.com/top/methodology). In short: - Only paid licenses from completed orders count. Free grants (100% off codes), trials, sandbox orders and test licenses never count. - Your own purchases and your team's never count. Orders refunded within 14 days and disputed orders are removed. - A sale below half the list price counts as half a sale, and so does a first purchase from an account less than a day old. - In-app update downloads never count toward Top Free. Each visitor counts once per app per day. - Retention (10% of Top Paid) is the share of opt-in AppsXi Desktop installs where the app was opened again a week or more after install. Apps with fewer than 20 such installs get the average, so new apps are not penalized. - Apps hidden for a human check (Approved: Limited) are not ranked until a person confirms them. - Manipulation, including fake or incentivized reviews, removes the app from every chart for 90 days. ## Your rankings Open **Apps > your app > Leaderboard** in the developer dashboard. It shows your current rank on each chart, overall and in your category and subcategory, your best rank, and a 30 day trend. ## Badge Each live app has a badge that shows its best current top 10 placement, for example `#1 in Top Paid · Menu Bar Utilities`, and says `Verified on AppsXi` otherwise. It updates by itself. ```markdown [![My App on AppsXi](https://appsxi.com/apps//badge.svg)](https://appsxi.com/apps/) ``` ```html My App on AppsXi ``` For a website there are two larger styles: `?style=card` (60px tall, with the chart and the window) and `?style=compact` (40px tall, one line). They follow the visitor's light or dark mode; add `&theme=light` or `&theme=dark` to fix the colors. ```html My App on AppsXi ``` The badge is cached for an hour, the same as the charts. ## Social card `https://appsxi.com/apps//rank-card` is a 1200 × 630 PNG for posts: your rank, the chart, the category and the window. Without a top 10 placement it is a plain "Get it on AppsXi" card. Add `?theme=dark` for the dark version. --- # Launch Day Every day AppsXi features a handful of new indie apps and big updates on the [Launch Day](https://appsxi.com/launches) page and the home page. It is free and there is no paid placement. ## Book a day 1. Get a build approved (the app must be live on the day). 2. Open **Apps > your app > Launch Day** in the developer dashboard. 3. Pick a day 2 to 90 days out, write a headline (10 to 80 characters) and, if you like, a maker's note: why you built it and who it is for. AppsXi schedules at most 6 launches a day and confirms requests within two business days. You see the decision, and the reason if a request is declined, on the same page. Days run midnight to midnight Pacific time, like Product Hunt. Rules: - One launch day per app, then one major update launch (with a version) every 60 days. - One open booking per app. Cancel it to pick another day; launches can be cancelled until the day before. - A launch is a launch, not an ad: no paid placement, and the usual [chart rules](https://appsxi.com/top/methodology) apply. Asking for reviews in exchange for anything ends the launch and removes the app from the charts. ## On the day - Your app is listed on `/launches` and, while it is launching, on the home page. Featured launches (up to 2 a day) get a larger spot. - Your listing shows "Launching today on AppsXi". - AppsXi Launch Weeks are themed runs of launch days with one featured developer per day. They show as a banner and in the day picker. ## Share kit - **Launch card**: `https://appsxi.com/apps//launch-card`, a 1200 × 630 image with your app name, headline and day. Attach it to posts on X, Bluesky, LinkedIn, Reddit or your newsletter. - **Listing link**: `https://appsxi.com/apps/`. - **Launch price**: for paid apps, add a launch discount on the Pricing page starting on your launch day. It runs for up to 7 days and the listing shows "launch price" next to the real list price. After the launch week, your [chart badge](https://appsxi.com/docs/leaderboards) keeps working in your README. --- # Built with AI Built your app with Claude Code, Cursor or another AI tool? You can say so on your listing, tell the story in a build log, and your app can appear in the monthly [Built with AI](https://appsxi.com/built-with-ai) showcase. It is optional and free. ## Mark your app 1. Open **Apps > your app > Overview > Built with AI** in the developer dashboard. 2. Choose **Yes** and tick the tools you used: Claude Code, Cursor, GitHub Copilot, Windsurf, ChatGPT or Other. 3. Add a build log if you like, then save. You can change it any time; the listing updates right away. Choosing **No** removes the label and deletes the build log. ## The label Your listing shows a small **Built with AI** label under the app name, with the tools you picked. It is your own statement: AppsXi does not check it, and the label says so ("The developer says this app was built with AI tools"). A label that is not true counts as a misleading listing. The label does not change where your app shows up. Search, categories and the charts rank apps the same way whether you say yes or no. ## The build log A build log is the story of how you made the app: a title (3 to 120 characters) and up to 20,000 characters of text. Good ones say what you asked the AI to do, what it got wrong, what you fixed by hand, and what you would do differently next time. Write it in Markdown: | You type | You get | | --- | --- | | `## Planning` | A heading | | `- item` or `1. item` | A list | | `**bold**` | **bold** | | A backtick on each side of a word | `code` | | Three backticks on their own line, before and after | A code block | | `[text](https://example.com)` | A link | Links must start with `https://` or point to a page on AppsXi; anything else shows as plain text. HTML shows as text and never runs. The listing shows the start of the log under **How it was built**, with a link to the full log at `appsxi.com/apps//build-log`. The log is public once the app is live. ## The showcase The [Built with AI](https://appsxi.com/built-with-ai) page lists apps marked Built with AI whose listing first went live this month, newest first, plus the latest build logs. Months run midnight to midnight Pacific time, like [Launch Day](https://appsxi.com/docs/launch-day). Until this month has its first app, the page shows the most recent ones. Earlier months stay in the archive, for example `/built-with-ai?month=2026-09`. An app joins the month it first went live. If you mark an app that has been live for a while, it shows up in that earlier month's archive, and its build log shows in the latest build logs. Adults-only apps are not shown on the page. ## Publish with your AI tool Your AI coding tool can also do the AppsXi work: packaging, signing checks, the manifest and publishing on every release. In your project folder run: ```sh npx appxi ai ``` It adds the AppsXi skills for Claude Code, a Cursor rule and an `AGENTS.md` section, and connects the AppsXi MCP servers. Then ask: "Make this app AppsXi ready." See [MCP and AI tools](https://appsxi.com/docs/mcp). Pieces to start from: - [Package standard](https://appsxi.com/docs/package-standard): the formats and signing every build needs. - [appxi.json](https://appsxi.com/docs/appxi-json): the manifest that says what your app needs. - [Licensing](https://appsxi.com/docs/licensing): license helper code for TypeScript, Swift, C++ and .NET. - [GitHub releases](https://appsxi.com/docs/github-action): publish every release automatically. - [CLI](https://appsxi.com/docs/cli): run AppsXi's checks on your machine before you upload. --- # Buyer support Buyers get help from two places: - **AppsXi** handles purchases and payments, downloads, refunds and license keys. Send buyers with those questions to [appsxi.com/help](https://appsxi.com/help). - **You** answer questions about how your app works: settings, bugs, crashes and feature requests. ## Support email Your support email is shown on your listings and your developer page. You need one before a paid app can go live or be bought: without it your account stays a Verified Developer instead of a Verified Seller, and checkout says you are not set up to sell yet. Free apps can go live without one. Add or change it under Developer → Profile. While you sell paid apps you can change it, but not remove it. ## The support inbox Signed-in buyers can write to you from your app's page ("Contact the developer") or from their library. They can ask before they buy, too. Their messages arrive under **Developer → Support**: - **Waiting on you**: the buyer wrote last. - **Replied**: you wrote last; the buyer may answer. - **Closed**: closed by you or the buyer. Closed conversations take no more replies; the buyer can start a new one. You see the buyer's name and whether they own or are trying the app, never their email address. The account owner and team members with the developer role can reply; the finance role can read. AppsXi emails the account owner when a buyer writes (at most once every 10 minutes per conversation), and emails the buyer when you reply. A buyer can have one open conversation per app and can start up to 5 a day. ## Reply times Each conversation records how long your first reply took. Reply within **3 business days** (Monday to Friday, Pacific time). - Closing a conversation without replying does not count as a reply: it counts as unanswered, with the time up to when you closed it. - A conversation the buyer closes before you reply does not count at all. ## Your public developer page Your page at `/developers/` shows a few facts, each only when there is enough behind it: | Shown | When | |---|---| | Median first reply time, last 90 days | From 5 conversations | | Last update and releases in the last 6 months | Always (approved stable releases) | | Refund rate band: under 2%, 2% to 5%, or over 5% of orders, last 6 months | From 20 orders | | Years on AppsXi | Always | ## Trusted Publisher Trusted Publisher also needs a median first reply within 3 business days over the last 180 days, counted once you have 5 conversations in that time. It is checked every day; an automatic badge is removed when your replies get slower than that, and comes back when they are quicker again. --- # Developer API The [CLI](https://appsxi.com/docs/cli) and the [GitHub Action](https://appsxi.com/docs/github-action) use this API to publish builds. Your own scripts can use it to read your orders, licenses and reviews. You can also download the same data as CSV files from the dashboard (see [CSV downloads](#csv-downloads)). ## Tokens Create a token in Developer → **API tokens** (two-step sign-in required). Tokens look like `axd_` plus 40 hex characters, expire after 30, 90 or 365 days, and have one or more scopes: - `builds:read`: list apps and read build status. Every token has it. - `builds:write`: upload and submit builds (**Can submit builds**). - `sandbox:write`: sandbox test licenses and trial time travel through [MCP](https://appsxi.com/docs/mcp) (**Can use the sandbox**). - `data:read`: read orders, licenses and reviews (**Can read orders, licenses and reviews**). A token cannot change prices, listings or payouts. AppsXi stores only a hash; copy the token when it is shown. Send it as `Authorization: Bearer axd_…`. ## Endpoints ### `GET /api/v1/apps` ```json { "apps": [{ "id": "", "public_id": "app_…", "slug": "my-app", "name": "My App", "status": "live", "pricing": "paid" }] } ``` ### `POST /api/v1/uploads` Body `{ "app": "my-app", "filename": "MyApp-1.2.0.dmg", "size": 123456789 }`. Returns a two-hour signed upload: ```json { "file_key": "dev/…/MyApp-1.2.0.dmg", "bucket": "uploads", "signed_url": "…", "token": "…", "tus_endpoint": "https://….storage.supabase.co/storage/v1/upload/resumable/sign", "apikey": "…", "chunk_size": 6291456 } ``` Upload files up to 6 MB with one `PUT` to `signed_url`. Larger files use [TUS](https://tus.io) at `tus_endpoint` with the header `x-signature: ` and `apikey`, metadata `bucketName`, `objectName` (= `file_key`) and `contentType`, in 6 MB chunks. ### `POST /api/v1/builds` ```json { "app": "my-app", "file_key": "dev/…/MyApp-1.2.0.dmg", "version": "1.2.0", "channel": "stable", "ed_signature": "…", "release_notes": "…" } ``` `platform` is detected from the file when omitted. Returns `201` with `{ "build_id": "…", "status": "queued", "status_url": "/api/v1/builds/…" }`. Send `AppXi-Client: action` from CI. ### `GET /api/v1/builds/{id}` ```json { "id": "…", "app": "my-app", "platform": "macos", "version": "1.2.0", "status": "denied", "decision_reason": "Not notarized", "findings": [{ "severity": "deny", "kind": "signing", "title": "…", "file": null, "fix": "…" }], "url": "https://appsxi.com/dev/apps/…" } ``` Poll every 20 seconds or more while `status` is `queued` or `running`. ## Orders, licenses and reviews These three endpoints need a token with `data:read`. They return your rows oldest first, so a script can fetch everything once and then only what is new. | Query parameter | What it does | | --- | --- | | `app` | One app, by slug, public id (`app_…`) or id. All your apps when left out. | | `since` | Rows created at or after this time: a date (`2026-10-01`, midnight UTC) or a date and time with a zone (`2026-10-01T12:00:00Z`). | | `until` | Rows created before this time, in the same format. | | `environment` | `live` or `sandbox`. Both when left out (orders and licenses). | | `limit` | Rows per page, 1 to 200. Default 100. | | `cursor` | The `next_cursor` from the previous page. | Every response looks like this: ```json { "data": [ … ], "has_more": true, "next_cursor": "WyIyMDI2LTEw…" } ``` Keep calling with `cursor=` until `has_more` is `false`. Keep the same filters while you page. To pick up new rows later, save the last row's `created_at` and pass it as `since`, then skip the ids you already have. ### Buyers and privacy A buyer appears only as `buyer`, an id like `buyer_3f9c…`. It is the same for one buyer across all rows of one app, and the same as the `sub` of AppsXi license tokens and `buyer.id` in [license bridge](https://appsxi.com/docs/license-bridge) events. It is different for each of your other apps, so it cannot be used to follow a buyer across apps. The API never returns names or emails. The orders CSV in the dashboard does include each buyer's email, because you are the seller and the Orders page already shows it. ### `GET /api/v1/orders` Paid, refunded, disputed and failed orders. Pending checkouts are left out. ```json { "id": "ord_…", "created_at": "2026-10-01T09:12:44.512+00:00", "paid_at": "2026-10-01T09:12:51.020+00:00", "app": "my-app", "app_id": "app_…", "environment": "live", "status": "paid", "currency": "USD", "list_price_cents": 2900, "discount_cents": 290, "subtotal_cents": 2610, "tax_cents": 0, "app_fee_cents": 131, "affiliate_cents": 0, "refunded_cents": 0, "coupon": "LAUNCH10", "pay_what_you_want": false, "bundle": null, "affiliate": null, "source": "launch_day", "source_detail": null, "license": "lic_…", "buyer": "buyer_…" } ``` `status` is `paid`, `refunded`, `partially_refunded`, `disputed` or `failed`. A refund changes the order's `status` and `refunded_cents` but not its `created_at`; use [license bridge](https://appsxi.com/docs/license-bridge) events to hear about refunds as they happen. Square's processing fee is not included; see your Square Dashboard. ### `GET /api/v1/licenses` Licenses and trials of your apps, including free ones and sandbox test licenses. ```json { "id": "lic_…", "created_at": "…", "app": "my-app", "app_id": "app_…", "environment": "live", "kind": "paid", "license_type": "personal", "status": "active", "seats_max": 2, "seats_used": 1, "trial_started_at": null, "trial_expires_at": null, "order": "ord_…", "key_last4": "7KQ2", "label": null, "buyer": "buyer_…" } ``` `kind` is `paid`, `free_grant`, `trial`, `bridge` or `test` (sandbox). `status` is `active`, `trial`, `expired` or `revoked`. `key_last4` helps you match a key a buyer quotes in a support email; the full key is never returned. `label` is the name you gave a sandbox test license. ### `GET /api/v1/reviews` Reviews of your apps with your reply. Reviews AppsXi hid for breaking the content policy are left out. ```json { "id": "", "created_at": "…", "updated_at": "…", "app": "my-app", "app_id": "app_…", "rating": 5, "title": "Great", "body": "…", "status": "published", "license": "lic_…", "buyer": "buyer_…", "reply": "Thanks!", "replied_at": "…" } ``` ### Example ```sh curl -s -H "Authorization: Bearer $APPXI_TOKEN" \ "https://appsxi.com/api/v1/orders?app=my-app&since=2026-10-01&limit=200" ``` The same lists are MCP tools (`list_orders`, `list_licenses`, `list_reviews`) for AI assistants; see [MCP](https://appsxi.com/docs/mcp). ## CSV downloads In the dashboard, **Orders** has buttons for orders and licenses, **Reviews** for reviews, and each app's overview page for all three for that app. Pick an app (or all apps) and a date range (UTC, both days included; leave empty for everything). Downloads need two-step sign-in in this session and hold up to 100,000 rows per file; use a shorter range for more. The columns are the API fields above, in the same order. The orders file adds `buyer_email`. Amounts are in cents. Text that a spreadsheet could run as a formula (starting with `=`, `+`, `-` or `@`) starts with an apostrophe, so a review cannot run code on your computer. ## Errors Errors are JSON: `{ "error": "", "message": "…" }`. Codes: `unauthorized` (401), `scope` (403, the token lacks `data:read`), `unknown_app` (404), `format` (400), `size` (400), `rate_limited` (429), `bad_request` (400), `unavailable` (503). Each developer account can make up to 1,200 data requests an hour, and each address up to 600 API requests an hour. --- # CLI ```sh npx appxi ``` Node 20 or later, no dependencies. It runs the same checks as the AppsXi pipeline, using the tools already on your machine: `hdiutil`, `codesign`, `spctl`, `stapler` and `pkgutil` on Mac; PowerShell or `osslsigncode`, and `7z`, for Windows installers. ## appxi check ```sh npx appxi check dist/MyApp-1.2.0.dmg --app my-app ``` Checks format, signing, notarization, entitlements, the updater feed and Sparkle key, hardcoded secrets and outside checkout links. Exits with code 1 if anything would be denied. `--json` prints a machine-readable report; `--platform windows` forces the platform for `.zip` files; `--homepage https://example.com` tells the checkout scan which links are yours. ## appxi package Writes `appxi.json` from a build (see [appxi.json](https://appsxi.com/docs/appxi-json)). `--out` sets the path. ## appxi doctor ```sh npx appxi doctor . ``` Detects your stack (Electron, Tauri, Swift, .NET, JUCE, Flutter), checks for Developer ID identities and `notarytool` (Mac) or a code signing certificate (Windows), and scans your source for hardcoded secrets and outside checkout links. ## appxi init Writes `appxi.json` and `.github/workflows/appxi-publish.yml` for your stack. Fill in the build step and add the `APPXI_TOKEN` secret. `--force` replaces an existing workflow. ## appxi publish ```sh APPXI_TOKEN=axd_… npx appxi publish dist/MyApp-1.2.0.dmg dist/MyApp-1.2.0.msix --app my-app --version 1.2.0 --notes CHANGELOG.md --wait ``` Runs `check` on each file (skip with `--skip-check`), uploads it with resumable 6 MB chunks, submits it, and with `--wait` waits for the decision, printing each finding and its fix. Exits with code 1 if a build is denied. | Option | | | --- | --- | | `--app` | Your app's slug (required). | | `--version` | Defaults to the version in the package. | | `--channel` | `stable` (default) or `beta`. | | `--notes` | A Markdown file with release notes. | | `--ed-signature` | Sparkle EdDSA signature for the file. | | `--sparkle-sign [path]` | Run Sparkle's `sign_update` for you. | | `--wait` | Wait for the pipeline decision. | In GitHub Actions it writes `build_id` to `$GITHUB_OUTPUT`. ## appxi status, appxi apps ```sh npx appxi status npx appxi apps ``` ## appxi mcp Starts a local MCP server for AI coding tools. See [MCP](https://appsxi.com/docs/mcp). ## Environment | Variable | | | --- | --- | | `APPXI_TOKEN` | A [developer API token](https://appsxi.com/docs/developer-api). Needed for `publish`, `status`, `apps`. | | `APPXI_API_URL` | Another AppsXi site (default `https://appsxi.com`). | | `NO_COLOR` | Plain output. | --- # GitHub releases There are two ways to publish from GitHub. Both keep building and signing in your own CI. ## The AppsXi GitHub App Connect GitHub in Developer → **GitHub**, choose the repositories, then pick a repository for each app (your app → **GitHub releases**). From then on, every release you publish on GitHub becomes an AppsXi build: - AppsXi takes the installers attached to the release (`.dmg`, `.pkg`, `.zip`, `.msix`, `.msi`, `.exe`) and skips blockmaps, checksums, update YAML files and source archives. Optional filename globs (for example `*-universal.dmg`) pick one file per platform. - The tag is the version (`v2.3.1` → `2.3.1`), the release notes become the changelog, and prereleases go to the beta channel (or are ignored, if you choose). - Turn off automatic submission to review each release and press **Publish** yourself. - Drafts are ignored. Each file is checked exactly like an upload, and its signature must match your pinned identity. The App asks for read-only access to contents and metadata, plus release events. It cannot push to your repositories. AppsXi fetches each file with a short-lived token limited to that one repository. Your release workflow only needs to build, sign, notarize and attach the installers to the release. ## With appxi init (no GitHub App) `npx appxi init --app my-app` writes `.github/workflows/appxi-publish.yml`. It runs on `release: published`, builds with your stack's usual commands, then runs `appxi publish --wait`. Prereleases go to the `beta` channel. Add a repository secret `APPXI_TOKEN` with a [developer API token](https://appsxi.com/docs/developer-api) that has `builds:write`. Your signing certificates stay in your own secrets; AppsXi never sees them. ## The publish action ```yaml on: release: types: [published] permissions: contents: read jobs: publish: runs-on: macos-latest steps: - uses: actions/checkout@v4 # build, sign, notarize and staple here - uses: appxi/publish-action@v1 with: token: ${{ secrets.APPXI_TOKEN }} app: my-app files: dist/*.dmg ``` | Input | Default | | | --- | --- | --- | | `token` | | Developer API token. | | `app` | | Your app's slug. | | `files` | | Installer file(s); globs are expanded. | | `version` | release tag without `v` | | | `channel` | `beta` for prereleases, else `stable` | | | `notes` | release body | | | `ed-signature` | | Sparkle signature for a single macOS file. | | `wait` | `true` | Fail the job when a build is denied. | | `cli-version` | `latest` | | Output: `build_id`. ## Security Release titles and bodies are untrusted input. The action and the generated workflow pass them through environment variables and never interpolate `${{ }}` expressions into shell scripts, so a crafted release cannot run commands on your runner. --- # MCP and AI tools AppsXi gives your AI coding tool two Model Context Protocol servers and a set of instructions: - **Remote server** at `https://appsxi.com/api/mcp`: the docs, integration guides, license helper code, `appxi.json` validation, and, with your API token, your apps, builds, denial explanations, listing, pricing, chart positions, the sandbox, and your orders, licenses and reviews. - **Local server** (`npx appxi mcp`): checks your project and installers on your machine (stack, signing setup, secrets, outside checkout links, preflight, manifest and workflow generation). - **Skills and rules**: `appxi-integrate`, `appxi-package`, `appxi-publish`, `appxi-fix-denial`, `appxi-requirements` and `appxi-test` for Claude Code, a Cursor rule, and an `AGENTS.md` section for Codex and other tools. ## Quick setup In your project folder: ```sh npx appxi ai ``` It writes `.claude/skills/appxi-*`, `.cursor/rules/appxi.mdc`, the AppsXi section of `AGENTS.md` (between `` and ``, the rest of the file is untouched) and adds both servers to `.mcp.json`. Use `--claude`, `--cursor` or `--agents` for just one, `--no-mcp` to leave `.mcp.json` alone, and `--force` to replace skills you edited. The files are also at [/ai/manifest.json](https://appsxi.com/ai/manifest.json) if you prefer to copy them yourself. ## Remote server Claude Code: ```sh claude mcp add --transport http appxi https://appsxi.com/api/mcp --header "Authorization: Bearer $APPXI_TOKEN" ``` Cursor (`.cursor/mcp.json`) and other clients that speak Streamable HTTP: ```json { "mcpServers": { "appxi": { "url": "https://appsxi.com/api/mcp", "headers": { "Authorization": "Bearer ${env:APPXI_TOKEN}" } } } } ``` The token is optional: without it the docs tools work and the account tools tell the assistant to ask for one. Create tokens in Developer → API tokens. Tick **Can use the sandbox** for the sandbox tools and **Can read orders, licenses and reviews** for the data tools. A token never lets an assistant change prices, listings, payouts or publishing, and no tool returns a buyer's name or email: buyers appear only as an id that is stable within one app. | Tool | Needs | What it does | | --- | --- | --- | | `search_docs` | nothing | Best matching sections of these docs. | | `get_doc` | nothing | One docs page as Markdown. | | `get_integration_guide` | nothing | Steps for a stack (swift, electron, tauri, dotnet, juce, flutter) and a feature (packaging, licensing, trials, checkout, updates, publishing). | | `get_code_snippet` | nothing | The license helper for typescript, swift, cpp or csharp. | | `get_sdk_versions` | nothing | API, manifest, helper, CLI and MCP versions and the deprecation policy. | | `validate_manifest` | nothing | Checks an `appxi.json` and explains each error and warning. | | `explain_check` | nothing | The fix for a pipeline finding. | | `list_apps`, `list_builds` | token | Your apps and their recent builds. | | `get_build_report`, `explain_denial` | token | A build's findings, in plain language, with the fix for each. | | `get_listing`, `get_pricing`, `get_rankings` | token | Read only; changes happen in the dashboard. | | `create_test_license`, `simulate_trial`, `list_test_licenses` | sandbox token | Sandbox licenses and trial time travel (advance, expire, convert, revoke, restore). | | `list_orders`, `list_licenses`, `list_reviews` | data token | Your orders, licenses and reviews, oldest first, 50 at a time, filtered by app, dates and live or sandbox. Same fields as the [Developer API](https://appsxi.com/docs/developer-api). | Prompts: **make_appxi_ready**, **add_trial**, **fix_denied_build**, **setup_github_publishing**, **migrate_licensing**. The docs and helper files are also exposed as resources (`appxi://docs/`, `appxi://sdk/`). ## Local server ```sh claude mcp add appxi-local -- npx -y appxi mcp ``` | Tool | What it does | | --- | --- | | `detect_stack` | Electron, Tauri, Swift, .NET, JUCE, Flutter or unknown, with evidence. | | `scan_for_secrets` | Hardcoded live keys in a folder, with masked samples. | | `scan_for_checkout_links` | Outside checkout links that a paid AppsXi build must not have. | | `check_signing_setup` | Developer ID identities and notarytool, or a Windows code signing certificate. | | `preflight_package` | The full `appxi check` on an installer. | | `generate_manifest` | `appxi.json` from an installer. | | `generate_github_action` | The publish workflow for a stack. | | `list_apps`, `get_build_report` | Same as the remote tools (set `APPXI_TOKEN`). | | `search_docs` | Searches these docs. | ## Prompts that work well - "Make this app AppsXi ready with a 7 day trial." - "Check my project for anything AppsXi would reject." - "Set up AppsXi publishing for this repo." - "Why was build 1234… denied? Fix it."