# Sell an item

> Sell something in your app for $LIA, end to end. The pay sheet in your page, then confirm and deliver on your server, with nothing lost if the page reloads. From the Lia developer docs: https://docs.lialabs.org/guides/sell-an-item

You'll sell "3 extra lives" for 5 LIA. The person pays on Lia's sheet, your server checks the charge with Lia, adds the lives, and marks the charge delivered.

## 1. Add the product

Ask the Lia team to add a product to your app: id `extra-lives`, name "3 extra lives", price 5. Your server can check it's there:

```sh
curl https://api.lialabs.org/api/apps/v1/products -H "Authorization: Bearer $LIA_KEY"
```

## 2. In your page: pay, then tell your server

```js title="shop.js"
async function buyExtraLives() {
  let charge;
  try {
    charge = await Lia.pay("extra-lives");
  } catch (error) {
    if (error.code === "cancelled") return; // they changed their mind
    Lia.toast("Couldn't open the payment", { tone: "error" });
    return;
  }
  await deliver(charge.chargeId);
}

// Your server checks the charge and gives the lives. Safe to call twice for one charge.
async function deliver(chargeId) {
  const { session } = await Lia.session();
  const res = await fetch("/api/deliver", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ session, chargeId }),
  });
  if (!res.ok) return Lia.toast("We'll add it as soon as we can", { tone: "error" });
  const { lives } = await res.json();
  showLives(lives);
  Lia.haptic("success");
}
```

## 3. On your server: check, deliver, mark it

```js title="server.js"
import { lia } from "./lia.js"; // the helper from the Server API page

app.post("/api/deliver", async (req, res) => {
  // Who is asking: from Lia, not from the page.
  const { valid, playerId } = await lia("POST", "/session/verify", { session: req.body.session });
  if (!valid) return res.status(401).json({ error: "session" });

  // Is this charge real, paid, and theirs?
  let charge;
  try {
    ({ charge } = await lia("GET", `/charges/${encodeURIComponent(req.body.chargeId)}`));
  } catch (error) {
    return res.status(404).json({ error: "no_charge" });
  }
  if (charge.playerId !== playerId) return res.status(403).json({ error: "not_yours" });
  if (charge.status === "refunded") return res.status(409).json({ error: "refunded" });

  // Give it once, keyed on the charge id (a second call finds it already given).
  const lives = await db.grantOnce(charge.id, playerId, () => db.addLives(playerId, 3));

  // Then tell Lia it's delivered (safe to repeat).
  await lia("POST", `/charges/${charge.id}/deliver`);
  res.json({ lives });
});
```

The important parts:

- **The player comes from `/session/verify`**, never from the page.
- **The charge comes from `GET /charges/:id`**: its player, product and status are Lia's.
- **Grant once per charge id**, in your own database, so a retry, a webhook and the page can't give it twice. Do it in one transaction with recording the charge id.
- **Deliver after granting.** If your server dies between the two, the charge stays `paid` and you'll see it again (below), and your once-per-charge check stops a double grant.

## 4. Never lose one

The page can reload or lose its connection between paying and your server. Catch up when your app opens, and when a charge arrives without a `pay()` waiting:

```js
Lia.init().then(async function () {
  Lia.ready();
  for (const charge of await Lia.charges()) await deliver(charge.chargeId);
});
Lia.onCharge(function (charge) { deliver(charge.chargeId); });
```

## 5. Or deliver from a webhook

If your server gives items without the page (adding coins to an account, say), use the `charge.paid` [webhook](/guides/webhooks) too. Check the charge with `GET /charges/:id`, grant once per charge id, then deliver. With both the page and the webhook, whichever arrives first delivers and the other finds it done.

## When you can't deliver

If something you sold can't be given (it sold out, the account is gone), give the $LIA back:

```js
await lia("POST", `/charges/${charge.id}/refund`);
```

The person gets the full price back and a refunded charge can't be delivered. Tell them in your page.

## Test it

With your test key and your app in test, buy it as a tester: the sheet says Test, no $LIA moves, and everything else happens for real. See [Testing](/guides/testing).
