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

ObjectPair asMain jobs
Vendorkind: "vendor"Take L$, call /purchase, optionally give items locally, call /confirm
Delivery serverkind: "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
pausedVendor or product paused
store_suspendedStore suspended
buyer_blockedBuyer blocked
product_unavailableProduct missing or not for sale
price_mismatchPaid amount ≠ expected price
group_onlyGroup members only
limit_reachedPurchase limit hit
not_giftableGifts not allowed
rental_takenRental slot taken
gacha_emptyGacha empty
validation_errorBad 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 same X-Veedo-Nonce.
  • Wait ~2 seconds between attempts.
  • veedo keys the sale as {deviceId}:{payer}:{timestamp}:{nonce}. A retry returns the same transactionId and decision — you will not double-charge the buyer in analytics.
  • After a successful response (ok: 1 or ok: 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 scriptWhat veedo does
item_missingRequeue until max attempts (default 5), ~30s × attempt backoff
not_transferableMark FAILED immediately (no transfer perm)
give_failedRequeue like a soft failure
server_timeoutLease 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

PathBody (short)Use
/pingStock, version, command acksHeartbeat; returns config / next ping / commands
/purchasesavatar, optional nameList products that avatar can redeliver
/redeliveravatar, productIdQueue another copy (mode vendor or server)
/demoavatar, productIdQueue a demo give
/codeavatar, usernameIssue a website touch sign-in code

8. HTTP errors

HTTPCodeWhat to do
401unauthorizedBad/missing headers, bad signature, stale timestamp, or nonce already used
401unknown_deviceDevice deleted — clear secret and re-pair
400validation_errorFix JSON body
403forbiddenWrong role (e.g. vendor calling claim)
404not_foundUnknown transaction / product / pairing
409conflictObject already live on another store
410expiredPairing code expired — touch to pair again
413payload_too_largeShrink body
429rate_limitedBack off
500internal_errorRetry; 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)

  1. 1Shopper pays the vendor in L$.
  2. 2Vendor POSTs /purchase (signed). Gets ok: 1 and deliver.mode.
  3. 3If mode is vendor: give items + pay splits, then /confirm.
  4. 4If mode is server: delivery server /deliveries/claim → give → /deliveries/complete.
  5. 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

Didn't find what you need? Open a ticket and the veedo team will reply in your dashboard and by IM.

Contact support