Quickstart: sell your first pack in 15 minutes
You need an Appside Studio workspace in test mode with one published store (Studio → Preview & publish).
1. Connect your backend (Studio → Integration)
- Generate an ES256 key pair on your backend and paste the public JWK into Public JWK/JWKS. Never upload the private key.
- Set Issuer (for example your game id:
orbit). - Set Fulfillment URL (for example
https://api.yourgame.com/appside/webhook) and a webhook secret of 32+ characters. - Press Send test event: health should show acknowledged.
2. Sign a player token on your backend
import { Appside } from 'appside/node';
const appside = new Appside({ storeId: 'orbit', issuer: 'orbit', kid: 'game-key-1',
privateKey: process.env.APPSIDE_PRIVATE_KEY_PEM, webhookSecret: process.env.APPSIDE_WEBHOOK_SECRET,
serverKey: process.env.APPSIDE_SERVER_KEY, baseURL: 'https://app.appside.build' });
app.post('/appside/token', requireLogin, (req, res) =>
res.json({ token: appside.playerToken(req.user.id) })); // 5-minute, single-use ES256 tokenfrom appside import Appside
appside = Appside('orbit', issuer='orbit', kid='game-key-1', privateKey=PRIVATE_KEY_PEM,
webhookSecret=WEBHOOK_SECRET, serverKey=SERVER_KEY, baseURL='https://app.appside.build')
@app.post("/appside/token")
def token(user=Depends(current_user)):
return {"token": appside.playerToken(user.id)}
Only call it from an authenticated endpoint: the token says "this signed-in player is buying".
3. Open the store from the game
import { Appside } from 'appside/web';
const appside = new Appside({ baseURL: 'https://app.appside.build' });
appside.configure('orbit', { environment: 'test', returnURL: 'https://yourgame.com/appside/return' });
appside.onPurchaseCompleted = result => refreshWalletFromBackend(); // UI hint only
const fresh = () => fetch('/appside/token', { method: 'POST' }).then(r => r.json()).then(j => j.token);
await appside.openStore(await fresh()); // whole store
await appside.purchase('voyager', await fresh()); // one pack, straight to checkout
// on your return page:
const result = appside.handle(location.href); // { status, orderId, state, verified: false }import AppsideSDK
let sdk = Appside(baseURL: URL(string: "https://app.appside.build")!,
openURL: { url in browser.open(url) }) // e.g. SFSafariViewController
try sdk.configure("orbit", environment: "test", returnURL: URL(string: "orbitquest://return")!)
try await sdk.openStore(playerToken)
try await sdk.purchase("voyager", playerToken: freshPlayerToken)
// in .onOpenURL / scene(_:openURLContexts:)
let result = try sdk.handle(incomingURL) // UI hint onlyval appside = Appside(activity, "https://app.appside.build")
appside.configure("orbit", "test", "orbitquest://return")
appside.openStore(playerToken) // Custom Tabs; returns CompletableFuture<Uri>
appside.purchase("voyager", freshPlayerToken)
// in onNewIntent / onCreate of the activity with the return intent-filter:
val result = appside.handle(intent.data.toString())// Attach AppsideSDK.Appside to a GameObject that survives scene loads, set baseURL in the inspector.
appside.configure("orbit", "test", "orbitquest://return");
appside.onPurchaseCompleted += result => ShowToast(result.orderId);
appside.openStore(playerToken); // iOS, Android, WebGL; deep links forwarded automatically
appside.purchase("voyager", freshPlayerToken);import { Linking } from 'react-native';
import { Appside } from '@appside/react-native';
const appside = new Appside({ Linking, baseURL: 'https://app.appside.build' });
appside.configure('orbit', { environment: 'test', returnURL: 'orbitquest://return' });
await appside.openStore(playerToken);final appside = Appside(baseURL: 'https://app.appside.build');
appside.configure('orbit', environment: 'test', returnURL: 'orbitquest://return');
await appside.openStore(playerToken);
await appside.purchase('voyager', freshPlayerToken);
purchase(sku) needs no server-to-server checkout session: the signed token plus the SKU is enough, and the price comes from your published catalogue.
4. Grant on the webhook (server)
app.post('/appside/webhook', express.raw({ type: '*/*' }), async (req, res) => {
if (!appside.verifyWebhook(req.body, req.headers)) return res.sendStatus(401);
const event = JSON.parse(req.body);
if (event.type === 'order.paid' && !event.probe) await grantOnce(event.order_id, event.player_id, event.order);
if (event.type === 'order.refunded') await revokeOnce(event.order_id, event.player_id);
res.sendStatus(204); // any 2xx = acknowledged; anything else is retried
});@app.post("/appside/webhook")
async def webhook(request: Request):
raw = await request.body()
if not appside.verifyWebhook(raw, request.headers): return Response(status_code=401)
event = json.loads(raw)
if event["type"] == "order.paid" and not event["probe"]: grant_once(event["order_id"], event["player_id"], event["order"])
return Response(status_code=204)
grantOnce must be idempotent on order_id: Appside retries until it gets a 2xx (up to 72 h), so you may see the same event twice. To double-check an order from your server: await appside.getOrder(orderId) (uses your server key from Studio → Integration).
5. Test it
Buy a pack with card 4242 4242 4242 4242, any future expiry, any CVC. The webhook arrives within a second and your player has the coins. See Testing for decline, 3-D Secure and outage cases.