Sales
Read the store's inworld sales ledger and reconcile it with sale.completed webhooks.
The sales endpoint reads your store's inworld sales ledger. It covers sales recorded inworld only; Second Life Marketplace sales are not included. Besides reporting, it is how you find any sale.completed webhook you did not receive. Field shapes are on the Object reference page.
List sales
GET /v1/salesReturns a cursor-paginated list of the store's sales, oldest first (by created, then by id). Scope: sales:read. Query parameters limit (default 100, at most 1000) and cursor are optional; see Pagination and errors.
By default the list leaves out sales that were deleted and sales of products that were deleted. Sales of paused products are always included. Optional query parameters change what is returned:
| Parameter | Type | Notes |
|---|---|---|
includeDeletedSales | boolean | Include sales that were deleted. Defaults to false. |
includeDeletedProducts | boolean | Include sales of products that were deleted. Defaults to false. |
createdFrom | string | Only sales created at or after this time (inclusive). |
createdTo | string | Only sales created before this time (exclusive). |
createdFrom and createdTo are RFC 3339 timestamps with an explicit offset or Z, such as 2026-10-01T00:00:00Z or 2026-10-01T00:00:00-03:00, and are compared in UTC. Fractional seconds of 1 to 7 digits are accepted; a leap second (:60) is refused. URL-encode them in the query string, so a + offset is sent as %2B. A timestamp without an offset, a value that cannot be read, or a createdFrom that is not earlier than createdTo returns 400. A value other than true or false for either boolean also returns 400.
The cursor is bound to the request it came from. When you page through results, send exactly the same includeDeletedSales, includeDeletedProducts, createdFrom, and createdTo values on every page request. A cursor sent with any of them changed, with a key for another store, or that cannot be read at all returns 400 with the message The cursor does not match this request.
curl --fail-with-body "https://integrations.allomancy.net/v1/sales?createdFrom=2026-10-01T00:00:00Z&createdTo=2026-11-01T00:00:00Z&includeDeletedSales=true&includeDeletedProducts=true" \
-H "X-API-Key: allo_live_8Kd2...zQ.Hk9...4w"import requests
response = requests.get(
"https://integrations.allomancy.net/v1/sales",
headers={"X-API-Key": "allo_live_8Kd2...zQ.Hk9...4w"},
params={
"createdFrom": "2026-10-01T00:00:00Z",
"createdTo": "2026-11-01T00:00:00Z",
"includeDeletedSales": "true",
"includeDeletedProducts": "true",
},
)
print(response.json())const response = await fetch(
"https://integrations.allomancy.net/v1/sales?createdFrom=2026-10-01T00:00:00Z&createdTo=2026-11-01T00:00:00Z&includeDeletedSales=true&includeDeletedProducts=true",
{
headers: { "X-API-Key": "allo_live_8Kd2...zQ.Hk9...4w" },
},
);
console.log(await response.json());<?php
$ch = curl_init("https://integrations.allomancy.net/v1/sales?createdFrom=2026-10-01T00:00:00Z&createdTo=2026-11-01T00:00:00Z&includeDeletedSales=true&includeDeletedProducts=true");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-API-Key: allo_live_8Kd2...zQ.Hk9...4w"]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;{
"data": [
{
"id": "6f1d2c3b-4a5e-4f60-8b7c-9d0e1f2a3b4c",
"payment": 250,
"netIncome": 225,
"location": "Allomancy Plaza",
"productId": "3a1f0c9b-7d2e-4c84-9a6f-1b5e8d0c2a37",
"productName": "Copper Lantern",
"productActive": true,
"productDeletionTime": null,
"merchant": {
"uuid": "a1b2c3d4-e5f6-4789-9abc-def012345678",
"username": "store.owner"
},
"payer": {
"uuid": "c7d8e9f0-1a2b-4c3d-9e4f-5a6b7c8d9e0f",
"username": "resident.username"
},
"receiver": {
"uuid": "c7d8e9f0-1a2b-4c3d-9e4f-5a6b7c8d9e0f",
"username": "resident.username"
},
"delivered": true,
"failedToDeliver": false,
"type": 1,
"imported": false,
"couponCode": null,
"deletionDate": null,
"created": "2026-10-21T14:03:05.391027Z",
"updated": "2026-10-21T14:03:06.204518Z"
}
],
"pagination": { "nextCursor": null, "previousCursor": null, "limit": 100 }
}Each row is a SaleResponse. Status: 200, 400, or 404 if the key's store cannot be found.
A few fields deserve a note:
- Names and product state are current values.
productName,productActive,productDeletionTime, and the residents' usernames show what they are now, not what they were at the time of the sale. Asale.completedpayload carries the values at event time instead. couponCodeis the code of the coupon used on the sale. It stays filled in even after the coupon or the product is deleted, and isnullwhen no coupon was used.typeis a TransactionType integer. The list returns sales of every type, not only the ones that fire webhooks.
Reconciling with sale.completed
Some sale.completed events may never reach you: a delivery can fail for good once its retries run out, and in rare cases an event is lost before it is queued, because capture is best effort. To find the ones you missed, compare the events you received with the ledger by id.
Choose a window with a margin
Pick the period to check and widen it on both sides, for example by an hour. A sale's created time and its event's date are recorded separately and can differ slightly, so match on ids, never on exact timestamps. Leave out any time when your subscription did not exist or was inactive. Sales from a time it did not exist are never sent to it, and sales from a time it was inactive are not expected; one that does arrive around a re-enable is harmless, because you match by id.
List every sale in the window
Request the window with createdFrom and createdTo, and set both includeDeletedSales=true and includeDeletedProducts=true, because a sale or its product can be deleted after its event was sent. Follow nextCursor until it is null, sending the same parameters each time.
Look for missing ids
Only sales that are not imported and whose type is 1 (Purchase) or 7 (Gacha) fire sale.completed. For each such row, check whether its id is among the transactionId values of the events you received. Any row without a match is a sale whose event you did not receive.
payment.received has no equivalent listing in the API, so it cannot be reconciled this way.