# Overview

URL: https://zerodev.ca/docs/kora/extensions/stores

> Issue a license for every purchase on BuiltByBit, Tebex, Stripe, Gumroad and eight more stores, and revoke it on refunds.



The Stores module connects the stores you sell on. Every purchase gets its own license, and refunds, disputes and ended subscriptions change it without anyone copying keys by hand.

<Callout type="info">
  This module depends on [`core`](/docs/kora/extensions/core). It replaces the old BuiltByBit module: on first start it imports a BuiltByBit setup it finds, with its secrets, resource links and purchases, and the old `/api/v1/builtbybit/placeholder` and `/api/v1/builtbybit/webhook` URLs keep working.
</Callout>

## Stores

| Store                 | How the buyer gets the key                               | Refund or chargeback               |
| --------------------- | -------------------------------------------------------- | ---------------------------------- |
| BuiltByBit            | Written into the download by a license key placeholder   | Revokes, through the purchase sync |
| Voxel Shop (Polymart) | Written into the download by an External API placeholder | Revokes                            |
| SpigotMC              | Your plugin trades the injected buyer ID for the key     | Revoke by hand                     |
| Tebex                 | Redeem page with the `tbx-` transaction ID and email     | Revokes, disputes suspend          |
| Gumroad               | Gumroad's own key from the receipt                       | Revokes, disputes suspend          |
| Lemon Squeezy         | Lemon Squeezy's own key from the receipt                 | Revokes                            |
| Payhip                | Payhip's own key, or the redeem page                     | Revokes                            |
| Ko-fi                 | Redeem page with the transaction ID and email            | No refund events                   |
| Stripe                | Shown on the redeem page right after checkout            | Revokes, disputes suspend          |
| WooCommerce           | Redeem page with the order number and billing email      | Revokes                            |
| itch.io               | Redeem page with the download link                       | No refund events                   |
| Envato                | Redeem page with the purchase code                       | No refund events                   |

## Connecting a store

1. Open **Stores**, press **Create** and pick the store. Its logo sits next to the name.
2. Paste the secrets the store gives you, such as Tebex's webhook secret or Stripe's signing secret.
3. Press **Map Product** and enter the store's product ID, or `*` for every product, and pick the Kora product it sells.
4. Paste the store's **Webhook URL** into the store. Stores that cannot show the key to the buyer also get a **Redeem page** URL to link from your product page or receipt.

Each store has its own unguessable URL. Every request is also checked against the store's own signature or secret, so a leaked URL alone cannot issue licenses. **New URL** replaces both URLs if one leaks.

## How purchases become licenses

* One license per purchase and product. Redelivered webhooks, re-downloads and renewals find the same license.
* Buyers become customers. A buyer seen before on the same store is matched by their store account, otherwise by email, otherwise a new customer is created.
* With **Use the store's license key** on, Gumroad, Lemon Squeezy and Payhip keys become the Kora key, so buyers use the key in their receipt.
* Refunds give the license the **Refund status**, disputes and ended subscriptions the **Dispute status**, and won disputes or resumed subscriptions bring it back. A refunded license is never reinstated.
* Purchases of products with no mapping are logged as ignored.

Each store's detail view lists what it sent and what Kora did: issued, existing, revoked, suspended, reinstated, ignored, rejected or failed. Every purchase is on the **Purchases** page with the license it issued.

## The redeem page

`/api/v1/stores/redeem/<token>` on the API address is a public page in the dashboard's language.

| Store                                                     | What the buyer enters                                                                                                                              |
| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| Tebex, Gumroad, Lemon Squeezy, Payhip, Ko-fi, WooCommerce | The order or transaction ID and the email they bought with. A wrong email gets the same answer as an unknown order.                                |
| Stripe                                                    | Nothing. Send Checkout or the Payment Link to the page with `?session_id={CHECKOUT_SESSION_ID}` and the key appears once the payment is confirmed. |
| itch.io                                                   | Their download link or key. Kora checks it with itch.io's API.                                                                                     |
| Envato                                                    | Their purchase code. Kora checks it with Envato's API.                                                                                             |

Keys are masked with **Show** and **Copy** buttons. The text is under **Redeem page** on the **Lang** page.

## Store notes

### BuiltByBit

Create a placeholder of type **External license key** with the store's webhook URL and secret. BuiltByBit writes the buyer's key into every download, the same key each time. With a **Private API token**, Kora also syncs purchases every ten minutes and on **Sync Now**, and revokes licenses of refunded or chargebacked buyers. A purchase webhook with the body `{"event":"purchase","secret":"<secret>","user_id":"{user_id}","username":"{username}","resource_id":"{resource_id}"}` issues the license at purchase time.

### Voxel Shop

Add an **External API** placeholder and a webhook for **Purchases** and **Purchase removed**, both with the store's webhook URL. Paste the placeholder's API signing secret and the webhook secret into the store. Requests more than five minutes old are refused.

### SpigotMC

SpigotMC has no purchase API. Your plugin posts `{"user":"%%__USER__%%","resource":"%%__RESOURCE__%%","nonce":"%%__NONCE__%%"}` to the webhook URL and gets `{"key":"...","status":"active"}` back. Values that were not replaced are refused. Fill **Allowed buyer IDs** from your buyer list to stop invented IDs.

### Tebex

Add an endpoint with the webhook URL for `payment.completed`, `payment.refunded`, `payment.dispute.opened`, `payment.dispute.won` and `payment.dispute.lost`, paste the secret, then press **Validate** in Tebex. Kora answers the validation webhook.

### Stripe

Add an endpoint for `checkout.session.completed`, `checkout.session.async_payment_succeeded`, `charge.refunded`, `charge.dispute.created`, `charge.dispute.closed` and `customer.subscription.deleted`. Map Payment Link IDs (`plink_...`) or a `kora_product` value in the Checkout Session metadata. Test and live mode need separate stores.

### Gumroad

Turn on **Generate a unique license key per sale**, paste your seller ID, and set the webhook URL as the **Ping endpoint**. Gumroad pings are not signed, so the seller ID and the unguessable URL are what protect them.

### Lemon Squeezy

Send `order_created`, `order_refunded`, `license_key_created`, `license_key_updated`, `subscription_expired` and `subscription_resumed` to the webhook URL with the store's signing secret.

### Payhip, Ko-fi, WooCommerce

Payhip needs your API key and the `paid` and `refunded` webhooks. Ko-fi needs the verification token. WooCommerce needs an **Order updated** webhook with the store's secret.

### itch.io and Envato

Paste an itch.io API key and game ID, or an Envato personal token with **View your items' sales history**. Buyers use the redeem page.
