Open the panel

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 ​

  1. In the panel, open Store settings → API and click New key. Only the store owner can create keys.

  2. 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.

  3. 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/v1
bash
curl 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:

lsl
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:

lsl
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, 16384 for lists, and ask for small pages (limit=1 is all a license check needs).
  • Errors arrive in the body, so you can read error.message and error.retry_after with llJsonGetValue. LSL does not show the response headers.
  • Second Life's own limit: one object can send about 25 requests every 20 seconds. Past that, llHTTPRequest returns NULL_KEY and says Too many HTTP requests too fast. Spread requests out, or cache answers in the script.

From JavaScript or Python ​

js
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()
python
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 panelScopeWhat it opens
Products and groupsproducts:read · products:writeProducts, groups of variations, prices, stock, pausing
Salessales:readSales, with the same details webhooks send
Customers and purchasescustomers:readCustomers and did this avatar buy it?
Deliveriesdeliveries:writeSend a product, redeliver a sale or everything an avatar owns
Pointspoints:read · points:writeBalance, history, give or take points
Gift cardsgift_cards:read · gift_cards:writeList, give, adjust and cancel gift cards
Couponscoupons:read · coupons:writeList, create and change coupons
Promotionsdiscounts:read · discounts:writeList, create and end promotions
Vendors and hubsvendors:read · vendors:writeList vendors and hubs, change what a vendor sells
Reportsreports:readThe 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 storeThe API
ProEverything the key allows.
Pro expiredReading 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.
BasicNo keys.

Limits ​

Limits are per store: more keys do not mean more requests.

WhatLimit
Reads600 per minute (10 per second)
Changes120 per minute
Deliveries60 per minute and 5,000 per day
Reports20 per minute
All requests200,000 per day
At the same time5 requests
Page size50 by default, up to 200
Batch500 items per request
Request body64 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:

json
{
  "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"
  }
}
StatuscodeMeaning
400BAD_REQUESTSomething in the request is wrong; the message says what.
401UNAUTHORIZEDNo key, wrong key, or a revoked key.
403MISSING_SCOPEThe key is not allowed to do this.
403PRO_REQUIREDThe store has no Pro (or it expired, for changes).
403STORE_SUSPENDEDThe store is suspended: read-only.
404NOT_FOUNDThe route or the thing you asked for does not exist.
409CONFLICT, IDEMPOTENCY_MISMATCHIt clashes with what is there, or the same Idempotency-Key came with a different request.
429RATE_LIMITEDA limit was reached. Read retry_after.
500INTERNALOur 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.

AreaRoutes
AccountGET /me
ProductsGET /products · GET /products/{id} · PATCH /products/{id} · PATCH /products (the same change for up to 500)
GroupsGET /groups · GET /groups/{id} · PATCH /groups/{id}
SalesGET /sales · GET /sales/{id}
DeliveriesPOST /products/{id}/send · POST /sales/{id}/redeliver · POST /customers/{key}/redeliver
CustomersGET /customers · GET /customers/{key} · GET /customers/{key}/purchases
PointsGET /customers/{key}/points · POST /customers/{key}/points
Gift cardsGET /gift-cards · GET /gift-cards/{id} · POST /gift-cards · POST /gift-cards/{id}/adjust · POST /gift-cards/{id}/cancel
CouponsGET /coupons · GET /coupons/{id} · POST /coupons · PATCH /coupons/{id}
PromotionsGET /discounts · POST /discounts · POST /discounts/{id}/end
VendorsGET /vendors · GET /vendors/{id} · PATCH /vendors/{id} · GET /hubs
ReportsGET /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.