The Fasty API PRO
The API lets your own code talk to your store: a game that unlocks when the player bought the HUD, a scoreboard that gives loyalty points, a site that lists your newest products, a sheet that keeps a copy of your sales. Each key does only what you allow, and it works straight from LSL.
Get a key
In the panel, open Store settings → API and click New key. Only the store owner can create keys.
Give it a name (My card game) and choose what the key can do. Start with only what you need: you can change it later without replacing the key.
Copy the key now. It starts with
fsk_and is shown only once. If you lose it, revoke it and create another.
Keep the key secret
Anyone holding the key can do what it allows. Never put it on a website or in an object other people can open: in Second Life, the script holding the key must be no-copy and no-modify. Leaked one? Revoke it in the panel: it stops working right away.
Up to 5 active keys per store. The panel shows, for each key, the last 4 characters, what it can do, when it was last used and how many requests it made today.
Your first request
Every request goes to this address, with the key in the Authorization header:
https://api.fastyvendor.com/api/v1curl https://api.fastyvendor.com/api/v1/me \
-H "Authorization: Bearer fsk_…"GET /me answers with your store, this key, what it can do, the limits and today's usage. It is the easiest way to check a key works.
From LSL
llHTTPRequest sends the header with HTTP_CUSTOM_HEADER. This one checks if whoever touched the object bought a product:
string KEY = "fsk_…"; // the script must be no-copy, no-modify
string PRODUCT = "prd_…"; // the product id, from the panel
default
{
touch_start(integer n)
{
llHTTPRequest("https://api.fastyvendor.com/api/v1/customers/" + (string)llDetectedKey(0)
+ "/purchases?limit=1&product_id=" + PRODUCT,
[HTTP_CUSTOM_HEADER, "Authorization", "Bearer " + KEY], "");
}
http_response(key id, integer status, list meta, string body)
{
if (status != 200) llOwnerSay(llJsonGetValue(body, ["error", "message"]));
else if (llJsonGetValue(body, ["purchased"]) == JSON_TRUE) llSay(0, "Unlocked!");
else llSay(0, "Get it at the store first.");
}
}And this one gives 10 points to the winner of a round:
llHTTPRequest("https://api.fastyvendor.com/api/v1/customers/" + (string)winner + "/points",
[HTTP_METHOD, "POST", HTTP_MIMETYPE, "application/json",
HTTP_CUSTOM_HEADER, "Authorization", "Bearer " + KEY,
HTTP_CUSTOM_HEADER, "Idempotency-Key", "round-" + (string)round + "-" + (string)winner],
"{\"delta\":10,\"note\":\"Won a round\"}");Good to know in LSL:
- Bigger answers: LSL keeps only the first 2 KB of an answer. Add
HTTP_BODY_MAXLENGTH, 16384for lists, and ask for small pages (limit=1is all a license check needs). - Errors arrive in the body, so you can read
error.messageanderror.retry_afterwithllJsonGetValue. LSL does not show the response headers. - Second Life's own limit: one object can send about 25 requests every 20 seconds. Past that,
llHTTPRequestreturnsNULL_KEYand says Too many HTTP requests too fast. Spread requests out, or cache answers in the script.
From JavaScript or Python
const res = await fetch(`https://api.fastyvendor.com/api/v1/customers/${avatarKey}/purchases?product_id=prd_…`, {
headers: { Authorization: `Bearer ${process.env.FASTY_KEY}` },
})
const { purchased } = await res.json()import os, requests
r = requests.get(
f"https://api.fastyvendor.com/api/v1/customers/{avatar_key}/purchases",
params={"product_id": "prd_…"},
headers={"Authorization": f"Bearer {os.environ['FASTY_KEY']}"},
)
purchased = r.json()["purchased"]The API has no CORS on purpose: it does not answer JavaScript running in a browser page, where anyone could read the key. Call it from a server, a bot or a script.
What a key can do
Each area can be View or View and change. A new key can only view.
| In the panel | Scope | What it opens |
|---|---|---|
| Products and groups | products:read · products:write | Products, groups of variations, prices, stock, pausing |
| Sales | sales:read | Sales, with the same details webhooks send |
| Customers and purchases | customers:read | Customers and did this avatar buy it? |
| Deliveries | deliveries:write | Send a product, redeliver a sale or everything an avatar owns |
| Points | points:read · points:write | Balance, history, give or take points |
| Gift cards | gift_cards:read · gift_cards:write | List, give, adjust and cancel gift cards |
| Coupons | coupons:read · coupons:write | List, create and change coupons |
| Promotions | discounts:read · discounts:write | List, create and end promotions |
| Vendors and hubs | vendors:read · vendors:write | List vendors and hubs, change what a vendor sells |
| Reports | reports:read | The sales summary |
No key can move L$. Refunds, sending L$ and profit shares stay in the panel, so a leaked key can never empty your store. Your team, plan and account are not in the API either.
When Pro ends
| Your store | The API |
|---|---|
| Pro | Everything the key allows. |
| Pro expired | Reading keeps working, so scripts that check purchases in your games keep working. Changes answer 403 PRO_REQUIRED until you renew. You can still revoke keys. |
| Basic | No keys. |
Limits
Limits are per store: more keys do not mean more requests.
| What | Limit |
|---|---|
| Reads | 600 per minute (10 per second) |
| Changes | 120 per minute |
| Deliveries | 60 per minute and 5,000 per day |
| Reports | 20 per minute |
| All requests | 200,000 per day |
| At the same time | 5 requests |
| Page size | 50 by default, up to 200 |
| Batch | 500 items per request |
| Request body | 64 KB |
Daily limits reset at 00:00 UTC. When the store reaches one, you get a single IM in Second Life.
Every answer tells you where you stand, in the X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers (seconds until the minute resets). Past a limit, the answer is 429 with retry_after in the body and a Retry-After header: wait that many seconds and try again.
Sending a wrong key 30 times in 15 minutes blocks that address for 15 minutes.
Errors
Errors come as JSON, with a code that never changes and a message in English:
{
"error": {
"code": "MISSING_SCOPE",
"message": "This key does not have the products:write scope. The store owner can add it in Settings → API.",
"request_id": "req_62csqsmw6e1r86"
}
}| Status | code | Meaning |
|---|---|---|
| 400 | BAD_REQUEST | Something in the request is wrong; the message says what. |
| 401 | UNAUTHORIZED | No key, wrong key, or a revoked key. |
| 403 | MISSING_SCOPE | The key is not allowed to do this. |
| 403 | PRO_REQUIRED | The store has no Pro (or it expired, for changes). |
| 403 | STORE_SUSPENDED | The store is suspended: read-only. |
| 404 | NOT_FOUND | The route or the thing you asked for does not exist. |
| 409 | CONFLICT, IDEMPOTENCY_MISMATCH | It clashes with what is there, or the same Idempotency-Key came with a different request. |
| 429 | RATE_LIMITED | A limit was reached. Read retry_after. |
| 500 | INTERNAL | Our fault. Try again; if it keeps happening, send the request_id to support. |
Pages
Lists answer { "items": [...], "next_cursor": "..." }. Pass next_cursor as ?cursor= to get the next page; null means it was the last one. Use ?limit= to choose the page size.
Safe retries
A POST may reach Fasty even when your script never hears the answer (a timeout, a region restart). Send an Idempotency-Key header with any unique text, and use the same one when you retry: Fasty does it only once and answers the repeat with the first answer. Keys are kept for 24 hours. The same key with a different request answers 409.
Always use it to give points, gift cards and deliveries.
Routes
The full reference, with every field, is the OpenAPI file at /api/v1/openapi.json (no key needed). Load it in Postman, Insomnia or any OpenAPI tool to try the routes or generate a client.
| Area | Routes |
|---|---|
| Account | GET /me |
| Products | GET /products · GET /products/{id} · PATCH /products/{id} · PATCH /products (the same change for up to 500) |
| Groups | GET /groups · GET /groups/{id} · PATCH /groups/{id} |
| Sales | GET /sales · GET /sales/{id} |
| Deliveries | POST /products/{id}/send · POST /sales/{id}/redeliver · POST /customers/{key}/redeliver |
| Customers | GET /customers · GET /customers/{key} · GET /customers/{key}/purchases |
| Points | GET /customers/{key}/points · POST /customers/{key}/points |
| Gift cards | GET /gift-cards · GET /gift-cards/{id} · POST /gift-cards · POST /gift-cards/{id}/adjust · POST /gift-cards/{id}/cancel |
| Coupons | GET /coupons · GET /coupons/{id} · POST /coupons · PATCH /coupons/{id} |
| Promotions | GET /discounts · POST /discounts · POST /discounts/{id}/end |
| Vendors | GET /vendors · GET /vendors/{id} · PATCH /vendors/{id} · GET /hubs |
| Reports | GET /reports/summary |
Send only the fields you want to change in a PATCH. If your HTTP client cannot send PATCH, send a POST with the header X-HTTP-Method-Override: PATCH.
Did this avatar buy it?
GET /customers/{key}/purchases?product_id=prd_… answers purchased: true when the avatar owns the product: bought for themselves or received as a gift, not refunded. Older versions of the product count too. Check several products at once with commas (product_id=prd_a,prd_b), or any variation of a group with group_id=.
This is the base for licenses: a HUD, a game or a door that only opens for buyers.
Send a product
POST /products/{id}/send with { "avatar_key": "…" } or { "username": "…" } delivers the product for free through your hubs, like Send to someone in the panel. It shows in your sales as a L$0 sale sent by you. The avatar does not need to be online: the hub delivers when they are. If Second Life is slow to find a username, the answer is 202 with pending: true and the send goes out by itself.
Keep a copy of your sales
GET /sales?sort=updated_at&order=asc lists sales oldest change first. Keep the next_cursor of the last page and ask again later: new sales, and sales that changed (delivered, refunded), come after it. Each sale is the same object JSON webhooks send, so one parser reads both.
Other filters: status, origin (vendor, gacha, marketplace, sent…), product_id, buyer_key, from and to.