# Orders API

> Follow an order from payment to delivery, and refund a paid order to take its coins and items back, with the server API.

An order is the purchase of a pack with real money, created by a [checkout](/docs/api/checkouts). It moves through a few statuses, from `pending` when the player has not paid yet to `fulfilled` once the coins and items are delivered. This page reads an order and refunds it. The examples on this page continue from the [setup](/docs/api#setup-for-the-examples), and use the id of an order that the player has paid.

## Get an order

Returns an order with its current status. Use it when the player comes back from the payment page, to confirm that the coins were delivered before you tell them, and to follow an order you created.

```endpoint
GET /orders/{orderId}
```

**Authentication:** secret key. **Idempotency:** not needed.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `orderId` | string | The id of the order: 24 hexadecimal characters, as returned by [Order a currency pack](/docs/api/checkouts#order-a-currency-pack). |

<!-- tabs:start -->
```bash tab="curl"
curl "https://gamecoin.apilow.com/api/v1/orders/665f1c2e8a3b4d5e6f7081b4" \
  -H "Authorization: Bearer $GAMECOIN_SECRET_KEY"
```
```javascript tab="Node.js"
const order = await gamecoin.orders.get("665f1c2e8a3b4d5e6f7081b4");
console.log(order.status, order.amountCents, order.vatCents, order.netCents, order.paidAt);
```
```python tab="Python"
order = gamecoin.orders.get("665f1c2e8a3b4d5e6f7081b4")
print(order.status, order.amount_cents, order.vat_cents, order.net_cents, order.paid_at)
```
```php tab="PHP"
$order = $gamecoin->orders->get('665f1c2e8a3b4d5e6f7081b4');
echo $order->status, ' ', $order->amountCents, ' ', $order->vatCents, ' ', $order->netCents, ' ', $order->paidAt?->format(DATE_ATOM), "\n";
```
```go tab="Go"
order, err := client.Orders.Get(ctx, "665f1c2e8a3b4d5e6f7081b4")
check(err)
fmt.Println(order.Status, order.AmountCents, order.VatCents, order.NetCents, order.PaidAt)
```
```csharp tab="C#"
var order = await gamecoin.Orders.GetAsync("665f1c2e8a3b4d5e6f7081b4");
Console.WriteLine($"{order.Status} {order.AmountCents} {order.VatCents} {order.NetCents} {order.PaidAt:u}");
```
<!-- tabs:end -->

**Response** `200 OK`: an [Order](/docs/api/objects#order). The one below was created by a checkout and paid in the simulated payment page.

```json
{
  "id": "665f1c2e8a3b4d5e6f7081b4",
  "playerId": "665f1c2e8a3b4d5e6f708192",
  "status": "fulfilled",
  "pack": { "sku": "gems-500", "name": "Bag of gems" },
  "amountCents": 499,
  "currency": "EUR",
  "country": "BE",
  "vatRateBp": 2100,
  "vatCents": 87,
  "netCents": 412,
  "createdAt": "2026-10-05T22:14:32.995Z",
  "paidAt": "2026-10-05T22:14:34.054Z",
  "fulfilledAt": "2026-10-05T22:14:34.054Z",
  "refundedAt": null,
  "creatorCode": null
}
```

**Errors**

| Status | Code | When | What to do |
|---|---|---|---|
| 404 | `NOT_FOUND` | No order has this id in your game and environment (including an id that is not 24 hexadecimal characters) | Check the id, and that the key is the one of the game that created the order. |

```json
{ "error": { "code": "NOT_FOUND", "message": "Order not found" } }
```

**Notes**

- The `status` is the truth. Never rely on the `?status=` of the return URL: read the order.
- A new order is `pending`. When the payment succeeds it goes to `fulfilled` within moments: the coins and items are delivered, `paidAt` and `fulfilledAt` are set. `paid` is a short step on the way, so you will rarely see it. If the player has not paid yet, poll the order every few seconds, or read it when they come back.
- The final statuses are `fulfilled` (until a refund), `refunded`, `chargeback`, `failed` (the payment was refused), `canceled` and `expired` (a `pending` order that was not paid within one hour). They are described in [Objects](/docs/api/objects#order-statuses).
- `amountCents` is the price in euro cents, VAT included; `vatRateBp` is the rate in basis points (2100 is 21 %); `vatCents` and `netCents` split the amount.
- An order is only visible with the key of the game that created it. The browser reads its own orders with [Get an order of the token player](/docs/api/client#get-an-order-of-the-token-player).

## Refund an order

Refunds a paid order: the money goes back to the player, and the coins and items the order delivered are taken back. Use it when a player asks for their money back, or when you cancel a sale.

```endpoint
POST /orders/{orderId}/refund
```

**Authentication:** secret key. **Idempotency:** **none**: this call has no `Idempotency-Key`. Do not blindly retry it: see the notes.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `orderId` | string | The id of a `fulfilled` order: 24 hexadecimal characters. |

**Body** (JSON; optional, an empty body means `{}`)

| Field | Type | Required | Rules |
|---|---|---|---|
| `reason` | string | no | Free text, at most 500 characters, recorded with the refund. |

<!-- tabs:start -->
```bash tab="curl"
curl -X POST "https://gamecoin.apilow.com/api/v1/orders/665f1c2e8a3b4d5e6f7081b4/refund" \
  -H "Authorization: Bearer $GAMECOIN_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason": "Player request"}'
```
```javascript tab="Node.js"
const orderId = "665f1c2e8a3b4d5e6f7081b4"; // the id of a fulfilled order
const refunded = await gamecoin.orders.refund(orderId, { reason: "Player request" });
console.log(refunded.status, refunded.refundedAt.toISOString());
```
```python tab="Python"
order_id = "665f1c2e8a3b4d5e6f7081b4"  # the id of a fulfilled order
refunded = gamecoin.orders.refund(order_id, reason="Player request")
print(refunded.status, refunded.refunded_at.isoformat())
```
```php tab="PHP"
$orderId = '665f1c2e8a3b4d5e6f7081b4'; // the id of a fulfilled order
$refunded = $gamecoin->orders->refund($orderId, ['reason' => 'Player request']);
echo $refunded->status, ' ', $refunded->refundedAt->format(DATE_ATOM), "\n";
```
```go tab="Go"
orderID := "665f1c2e8a3b4d5e6f7081b4" // the id of a fulfilled order
refunded, err := client.Orders.Refund(ctx, orderID, &gamecoin.RefundParams{Reason: "Player request"})
check(err)
fmt.Println(refunded.Status, *refunded.RefundedAt)
```
```csharp tab="C#"
var orderId = "665f1c2e8a3b4d5e6f7081b4"; // the id of a fulfilled order
var refunded = await gamecoin.Orders.RefundAsync(orderId, new RefundRequest { Reason = "Player request" });
Console.WriteLine($"{refunded.Status} {refunded.RefundedAt:u}");
```
<!-- tabs:end -->

**Response** `200 OK`: the [Order](/docs/api/objects#order), now `refunded`, with `refundedAt` set.

```json
{
  "id": "665f1c2e8a3b4d5e6f7081b4",
  "playerId": "665f1c2e8a3b4d5e6f708192",
  "status": "refunded",
  "pack": { "sku": "gems-500", "name": "Bag of gems" },
  "amountCents": 499,
  "currency": "EUR",
  "country": "BE",
  "vatRateBp": 2100,
  "vatCents": 87,
  "netCents": 412,
  "createdAt": "2026-10-05T22:14:32.995Z",
  "paidAt": "2026-10-05T22:14:34.054Z",
  "fulfilledAt": "2026-10-05T22:14:34.054Z",
  "refundedAt": "2026-10-05T22:14:40.586Z",
  "creatorCode": null
}
```

**Errors**

| Status | Code | When | What to do |
|---|---|---|---|
| 400 | `VALIDATION_FAILED` | An unknown field, or a `reason` longer than 500 characters | Read `details.fieldErrors`. |
| 402 | `PAYMENT_FAILED` | The payment provider could not refund | Retry later; the order is unchanged. |
| 404 | `NOT_FOUND` | No order has this id in your game and environment | Check the id. |
| 409 | `CONFLICT` | The order is not `fulfilled`: it is `pending`, `failed`, `expired`, already `refunded`… (`details.from` is its status) | Nothing to refund. Read the order to see where it stands. |

```json
{
  "error": {
    "code": "CONFLICT",
    "message": "Order cannot go from pending to refunded",
    "details": { "from": "pending", "to": "refunded" }
  }
}
```

**Notes**

- **Only a `fulfilled` order can be refunded**, and only once. The money is refunded to the player; in the test environment the refund is simulated.
- The coins are taken back with a ledger entry of type `refund_clawback` that points to the order: the `paid` and `bonus` amounts that the pack delivered. The items of the pack are removed from the inventory, up to what the player still owns.
- If the player has **already spent** the coins, the balance goes below zero: the shortfall is kept as a debt on the `paid` part, and the next credits repay it first. A game setting (« Après un remboursement » in the dashboard, which is in French for now) can stop balances at zero instead. A player in debt cannot spend until the debt is repaid.
- A refund has **no idempotency key**. After a network error or a `5xx`, do not retry blindly: read the order with [Get an order](#get-an-order). If it is `refunded`, the refund went through; a second refund would answer `CONFLICT` anyway. The SDKs retry a refund on `429` only.
- A pack with `maxPerPlayer` can be bought again after a refund: a refunded order does not count toward the limit.
