---
title: Quickstart
nav: Quickstart
order: 2
---
# 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)
1. Generate an ES256 key pair on your backend and paste the **public** JWK into **Public JWK/JWKS**. Never upload the private key.
2. Set **Issuer** (for example your game id: `orbit`).
3. Set **Fulfillment URL** (for example `https://api.yourgame.com/appside/webhook`) and a **webhook secret** of 32+ characters.
4. Press **Send test event**: health should show *acknowledged*.

## 2. Sign a player token on your backend

:::tabs
```js Node
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 token
```
```python Python
from 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

:::tabs
```js Web
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 }
```
```swift iOS
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 only
```
```kotlin Android
val 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())
```
```csharp Unity
// 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);
```
```js React Native
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);
```
```dart Flutter
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)

:::tabs
```js Node
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
});
```
```python Python
@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](/docs/testing/) for decline, 3-D Secure and outage cases.
