In-world vendor API
How paired vendors and delivery servers authenticate, record payments, retry safely, and confirm deliveries — with example requests and responses.
12 min read
This is the HTTP protocol that veedo vendors and delivery servers use after you pair them. Base URL: https://veedo.store/api/v1/sl. Every call is POST. Successful replies are { "ok": true, "data": … }; failures are { "ok": false, "error": { "code": "…", "message": "…" } }.
This is not the Premium store read API (vd_live_ keys). Those only GET catalog and sales data — see Connect to the veedo API. Do not put pairing secrets in public scripts or notecards.
Most creators never call this by hand. Use the official veedo starter kit. This page is for understanding the flow, debugging, or building an approved partner object.
Roles
| Object | Pair as | Main jobs |
|---|---|---|
| Vendor | kind: "vendor" | Take L$, call /purchase, optionally give items locally, call /confirm |
| Delivery server | kind: "server" | Hold inventory, /deliveries/claim, give items, /deliveries/complete |
1. Pairing (no signature yet)
The object posts once to start pairing, then polls until you approve the code in the dashboard. The secret is returned once on the first successful poll after approval.
Start — POST /api/v1/sl/pair/start
Request body (example):
{ "kind": "vendor", "objectName": "Mainstore board", "objectKey": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "ownerKey": "ffffffff-1111-2222-3333-444444444444", "ownerName": "mira.vortex", "region": "Jaqumia", "pos": [128.0, 64.0, 22.0], "scriptVersion": "2.1.0", "contents": [] }
kind is vendor, server, hub, or bridge. Second Life also sends x-secondlife-object-key and related headers when the request is from in-world.
Response data:
{ "code": "AB12CD", "pollToken": "…", "url": "https://veedo.store/pair/AB12CD", "expiresIn": 900 }
Poll — POST /api/v1/sl/pair/poll with { "code": "AB12CD", "pollToken": "…" }.
While waiting: { "status": "pending" }. After you approve and the object claims the secret:
{ "status": "paired", "kind": "vendor", "deviceId": "<uuid>", "secret": "<base64url>" }
Delivery servers get "kind": "server" and the same deviceId + secret shape. Store the secret on the object; it is never shown again.
2. Authentication (every call after pairing)
Signed requests use HMAC-SHA256 over the raw body. Headers (names are case-insensitive):
- X-Veedo-Device
- The `deviceId` UUID from pairing (vendors and servers)
- X-Veedo-Timestamp
- Unix time in seconds, as a decimal string
- X-Veedo-Nonce
- Unique string ≤ 64 characters (e.g. `llGenerateKey()`)
- X-Veedo-Signature
- Base64 HMAC-SHA256 of the canonical message
Canonical message (exactly five lines, joined with \n):
POST\n{pathname}\n{timestamp}\n{nonce}\n{rawBody}
Example message for a purchase:
POST\n/api/v1/sl/purchase\n1730000000\nabc-nonce-uuid\n{"payer":"…","amount":100}
Signature = HMAC-SHA256(secret, message) encoded as base64. Timestamp must fall inside the store's sign window (default 120 seconds). Body max 64 KB. Device rate limit defaults to 90 requests/minute per store.
Hubs use X-Veedo-Hub; CasperVend bridges use X-Veedo-Bridge. Same timestamp / nonce / signature rules.
3. Purchase (payment)
After money() succeeds on the vendor, post the payment. Path: POST /api/v1/sl/purchase (device signature required).
Request body (example):
{ "payer": "<avatar-uuid>", "payerName": "buyer.resident", "payerDisplay": "Buyer", "amount": 100, "productId": "<product-uuid>", "inGroup": 1, "recipient": null, "region": "Jaqumia", "pos": [128, 64, 22] }
inGroup may be 1/0 or boolean. Optional recipient / recipientName for gifts.
Success response data (HTTP 200, sale accepted):
{ "transactionId": "<uuid>", "ok": 1, "reason": "", "refund": 0, "change": 0, "deliver": { "mode": "server", "to": "<receiver-uuid>", "folder": "Dress", "items": [] }, "splits": [{ "id": "<split-uuid>", "to": "<payee-uuid>", "amount": 10 }], "rental": null, "message": "Thanks! Dress is on its way from the store's delivery server." }
deliver.mode is vendor (give from this prim's contents, then /confirm), server (delivery server will claim), or none (nothing to give).
Business refusal is still HTTP 200 with ok: 0 — return L$ as instructed:
{ "transactionId": "<uuid-or-null>", "ok": 0, "reason": "price_mismatch", "refund": 90, "change": 0, "deliver": { "mode": "none", "to": "<payer>", "folder": "", "items": [] }, "splits": [], "rental": null, "message": "The price just changed. Your L$ has been returned; please pay again." }
| `reason` | Meaning |
|---|---|
paused | Vendor or product paused |
store_suspended | Store suspended |
buyer_blocked | Buyer blocked |
product_unavailable | Product missing or not for sale |
price_mismatch | Paid amount ≠ expected price |
group_only | Group members only |
limit_reached | Purchase limit hit |
not_giftable | Gifts not allowed |
rental_taken | Rental slot taken |
gacha_empty | Gacha empty |
validation_error | Bad fields |
4. Payment retries (idempotent)
If the HTTP call fails with status 0, 499, or ≥ 500 (timeout, drop, server error), retry the same purchase up to 3 times.
- Reuse the same JSON body, same
X-Veedo-Timestamp, and sameX-Veedo-Nonce. - Wait ~2 seconds between attempts.
- veedo keys the sale as
{deviceId}:{payer}:{timestamp}:{nonce}. A retry returns the sametransactionIdand decision — you will not double-charge the buyer in analytics. - After a successful response (
ok: 1orok: 0), do not reuse that nonce for a different payment. - A brand-new payment from the avatar needs a new timestamp and nonce.
Never invent a new nonce to “retry” the same money event. That can look like a second sale. Only reuse timestamp+nonce on transport failures.
5. Vendor delivery confirmation
When deliver.mode is vendor, give the listed items, pay any splits, then confirm. Path: POST /api/v1/sl/confirm.
Request body (example):
{ "transactionId": "<uuid>", "delivered": 1, "paid": ["<split-uuid>"], "failed": [], "refunded": 0, "error": "" }
delivered, refunded accept 1/0 or booleans. Put paid split ids in paid; unpaid ones in failed.
If the give failed: { "transactionId": "…", "delivered": 0, "paid": [], "failed": [], "refunded": 0, "error": "item_missing" } — veedo can hand the work to a delivery server.
Response data: { "transactionId": "…", "delivered": true }.
6. Delivery server: claim and complete
Only devices paired as servers may call these (others get 403).
Claim — POST /api/v1/sl/deliveries/claim with { "limit": 10 }.
Response data:
{ "deliveries": [{ "id": "<delivery-uuid>", "to": "<customer-uuid>", "item": "Dress" }] }
Give each item, then complete — POST /api/v1/sl/deliveries/complete:
{ "results": [{ "id": "<delivery-uuid>", "ok": 1 }, { "id": "<other-uuid>", "ok": 0, "error": "item_missing" }] }
Response data echoes status per id, e.g. DELIVERED, PENDING (will retry), FAILED, or skipped.
| `error` from script | What veedo does |
|---|---|
item_missing | Requeue until max attempts (default 5), ~30s × attempt backoff |
not_transferable | Mark FAILED immediately (no transfer perm) |
give_failed | Requeue like a soft failure |
server_timeout | Lease expired (~3 minutes) — row returns to PENDING |
Claim again after each successful ping, after a full batch (≥ 10), and when a redelivery comes back with mode: "server". Backup servers only pick up rows older than ~45 seconds.
7. Other device endpoints
| Path | Body (short) | Use |
|---|---|---|
/ping | Stock, version, command acks | Heartbeat; returns config / next ping / commands |
/purchases | avatar, optional name | List products that avatar can redeliver |
/redeliver | avatar, productId | Queue another copy (mode vendor or server) |
/demo | avatar, productId | Queue a demo give |
/code | avatar, username | Issue a website touch sign-in code |
8. HTTP errors
| HTTP | Code | What to do |
|---|---|---|
| 401 | unauthorized | Bad/missing headers, bad signature, stale timestamp, or nonce already used |
| 401 | unknown_device | Device deleted — clear secret and re-pair |
| 400 | validation_error | Fix JSON body |
| 403 | forbidden | Wrong role (e.g. vendor calling claim) |
| 404 | not_found | Unknown transaction / product / pairing |
| 409 | conflict | Object already live on another store |
| 410 | expired | Pairing code expired — touch to pair again |
| 413 | payload_too_large | Shrink body |
| 429 | rate_limited | Back off |
| 500 | internal_error | Retry; for purchase reuse timestamp+nonce |
On ping, three 401s in a row (or unknown_device) should clear the secret and ask the owner to pair again.
End-to-end sale (typical)
- 1Shopper pays the vendor in L$.
- 2Vendor POSTs
/purchase(signed). Getsok: 1anddeliver.mode. - 3If
modeisvendor: give items + pay splits, then/confirm. - 4If
modeisserver: delivery server/deliveries/claim→ give →/deliveries/complete. - 5Sale shows as delivered in the dashboard; shopper can redeliver later from the website or a vendor menu.
For pulling sales into Discord bots or sheets without touching in-world objects, use API keys and endpoint reference.
Read next
- Connect to the veedo APIUse Premium API keys to read your sales, catalog, vendors, customers and analytics from bots, scripts and spreadsheets.
- API endpoint referenceEvery read endpoint an API key can call, with paths, scopes and query parameters.
- Sales integrations and inbound hooksPoint CasperVend, mdVendor and the SL Marketplace at veedo so external sales land in your dashboard.
- Place and pair vendorsRez veedo vendors at your mainstore, events or partner stores and link them to products.
- Set up your delivery serverThe box that holds your items and sends them out, even while you're offline.
- A shopper says nothing arrivedFind the sale, see what happened and resend it in a minute.
Didn't find what you need? Open a ticket and the veedo team will reply in your dashboard and by IM.
Contact support