> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qwacks.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Web Payments

> Sell shop items for real money on Windows, macOS, Linux and WebGL through Stripe, Tap or Moyasar.

On a build with no phone store (Windows, macOS, Linux, WebGL), a real-money item is paid through your
own payment gateway: Stripe, Tap or Moyasar. The player pays on the gateway's page in the browser, and
the money goes straight to your gateway account. Your game makes the same call as for any item:
`Shop.PurchaseAsync(item.Id)`.

```
Player taps Buy ──▶ checkout opens ──▶ player pays on the gateway's page ──▶ gateway tells Flock ──▶ item delivered
```

Nothing is delivered until the gateway confirms the payment. A checkout left unpaid closes after an
hour, and nothing is charged.

> [!NOTE]
>
> Web checkout needs the Flock SDK for Unity 1.98.0 or later. It needs no extra package. On Android and
> iOS the same calls pay through the phone's store instead: see
> [In-App Purchases](https://docs.qwacks.com/guides/real-money-purchases.md).

**1. Connect your gateway to Flock**

Open **Monetization > Payments** in the Flock dashboard and connect the gateway you sell through, with
your own keys. They stay on Flock's server and never reach the player's device. Use the gateway's
**test** keys while you build: their purchases are marked as tests and left out of your revenue.

> [tab] Stripe
>
> | Field | What to enter |
> | - | - |
> | Secret key | Your Stripe secret key (`sk_test_…` while testing) |
> | Webhook secret | The signing secret of the webhook below (`whsec_…`) |
>
> In Stripe, under **Developers > Webhooks**, add an endpoint at
> `https://api-flock.qwacks.com/v1/shop/webhook/<your game id>/stripe` with these events:
> `checkout.session.completed`, `checkout.session.expired`, `payment_intent.payment_failed`,
> `charge.refunded` and `charge.dispute.closed`. Then paste its signing secret into Flock.
> Without it, a payment is still delivered, within about 15 minutes instead of at once.
>
> Stripe charges in the item's currency and refuses a price below its smallest charge in your
> account's own currency (for an account that settles in dirhams, 2.00 AED).

> [tab] Tap Payments
>
> | Field | What to enter |
> | - | - |
> | Secret key | Your Tap secret key (`sk_test_…` while testing) |
> | Publishable key | Your Tap publishable key |
>
> Flock gives Tap the address for each payment's notice, so there is nothing to register. Tap
> charges in the item's own currency.

> [tab] Moyasar
>
> | Field | What to enter |
> | - | - |
> | Secret key | Your Moyasar secret key (`sk_test_…` while testing) |
> | Publishable key | Your Moyasar publishable key |
> | Webhook secret | The secret token of the webhook below |
>
> In Moyasar's dashboard, add a webhook at
> `https://api-flock.qwacks.com/v1/shop/webhook/<your game id>/moyasar`, choose a secret token, and
> paste the same token into Flock. A player whose card is declined can try another on the same page:
> the purchase stays open until it is paid or its hour ends.
>
> Moyasar charges only in Saudi riyals: a price in USD is converted at Flock's rate, and any other
> currency is refused.

> [!WARNING]
>
> A refund or dispute made in Stripe's or Moyasar's own dashboard does not reach Flock yet: the purchase
> stays paid, and the player keeps what it granted.

Connect one gateway, or name the one to use in the game when several are active (below).

**2. Price the items in real money**

Price the item in one of the **Real Currencies** of your currency config, as for a store item. The price
is the gateway's charge: 799 in SAR is 7.99 riyals. An item needs no store product id for web checkout.

**3. Connect in your game**

Connect once, after the SDK starts. On Windows, macOS, Linux and WebGL builds, `FlockStore` connects a
web checkout by itself:

```csharp theme={null}
FlockStore store;

async void Start()
{
    store = await FlockStore.ConnectAsync(new FlockStoreOptions
    {
        // Your own pages, where the player's browser lands after paying or giving up.
        ReturnUrl = "https://yourgame.example/paid",
        CancelUrl = "https://yourgame.example/cancelled"
    });

    // Every paid checkout, the ones PurchaseAsync answers included: refresh what the screen shows here.
    store.PurchaseCompleted += (item, result) => RefreshTheShop();
}
```

| Option | What it does |
| - | - |
| `PaymentProvider` | The gateway to pay through when several are active: `PaymentProviders.Stripe`, `Tap` or `Moyasar`. Empty uses the game's one active gateway. A Mac App Store build sets `PaymentProviders.AppleAppStore` here, to sell through the App Store instead |
| `ReturnUrl`, `CancelUrl` | Where the browser lands after paying and after giving up |
| `OpenPaymentPage` | Opens the payment page yourself, instead of in a new browser tab (see WebGL below) |
| `CheckEvery` | How often a waiting checkout is read; 3 seconds by default |

`store.IsWebCheckout` is true on these builds, and `store.GetPrice(item.Id).Text` gives the item's own
price to show; on a web checkout it is never null. The number is written in the player's device format,
so `SAR 7.99` is one example of how it can look.

## Buy

```csharp theme={null}
try
{
    PurchaseResult bought = await FlockClient.Instance.Shop.PurchaseAsync(item.Id);
    // Paid: bought.Granted lists what it delivered. Read the wallet and inventory again.
}
catch (FlockStoreException e) when (e.Reason == FlockStoreFailureReasons.Expired)
{
    // Left unpaid for the hour a checkout stays open: nothing was charged.
}
catch (FlockStoreException e)
{
    // PaymentDeclined: the gateway declined the payment or the player gave up. Nothing was charged.
}
```

`PurchaseAsync` opens a checkout, opens its payment page, and waits until it is paid, declined or expired.
A checkout is never opened twice: if no answer comes back, the call fails rather than trying again.

* **The player leaves the game** before paying: the checkout stays open on the server. At the player's
  next sign-in it is found again, and `PurchaseCompleted` reports it once paid.
* **Paid while the game was closed:** Flock delivers it all the same, and it shows in the wallet and
  inventory, but no `PurchaseCompleted` is raised for it.
* **Cancelling the call** (its cancellation token) stops the waiting, not the checkout: once paid,
  `PurchaseCompleted` reports it.
* **Signing out** ends the waiting. The checkout is found again at that player's next sign-in.

## WebGL

A browser opens a new tab only for a short time after the player's click. The payment page opens as
soon as the checkout is ready, about a second and a half after the click in our tests, and a Chromium
browser (Chrome, Edge) opened it. Firefox allows a similar time; Safari allows about a second, so it may
block the tab.

For Safari, show your own **Pay now** button, whose click opens the page at once:

```csharp theme={null}
string paymentPage;

store = await FlockStore.ConnectAsync(new FlockStoreOptions
{
    OpenPaymentPage = url => { paymentPage = url; ShowPayNowButton(); }
});

// The Pay now button's click:
void OnPayNow() => Application.OpenURL(paymentPage);
```

## Test before you ship

Connect the gateway's test keys, then buy with one of its test cards (Stripe's
`4242 4242 4242 4242`, any future date and any CVC; Tap's and Moyasar's are in their own docs). A test
purchase is delivered like any other, marked as a test, and left out of your revenue. Leave one checkout
unpaid to see it close as `Expired` after the hour.

## When something fails

Every failure's `Message` says what went wrong and, on a `Fix:` line, how to fix it.

| Code | What happened | What to do |
| - | - | - |
| `ShopRealCurrencySettlementUnavailable` | No web gateway is connected or active for the game | Connect one under **Monetization > Payments**, or turn **Active** back on |
| `ShopWebhookProviderUnknown` | Several gateways are active and the game named none, or it named a store | Set `FlockStoreOptions.PaymentProvider`, or pause the others |
| `PaymentProviderProviderNotFound` | The gateway you named is not connected, or it is paused | Connect it, or turn **Active** back on |
| `PaymentProviderCheckoutFailed` | The gateway would not open the checkout | Read the gateway's reason: keys it refuses, a currency it does not take, or a price below its smallest charge |
| `ShopNotARealCurrencyItem` | The item is priced in a game currency | It is bought from the wallet; `PurchaseAsync` does that by itself |
| `ShopItemNotForSale` | The item or its shop is switched off | Read the shop again: it is no longer listed |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.