Partner API integration guide
Use the Vylvet Partner API to sync stores, products, and orders with your system. This document is the single source of truth for integrating with Vylvet—everything you need is here.
You will: list stores and categories, read or sync products, create orders (either by redirecting customers to Vylvet-hosted Stripe Checkout or by reporting already-paid orders), and receive delivery updates via webhooks or polling.
Quick start
- Get your API key — Vylvet creates a partner client and gives you an API key once. Store it securely (e.g. environment variable).
- Call the API — Send requests to
https://vylvet.co/apiwith headerx-api-key: <your_key>. - Verify —
GET https://vylvet.co/api/v1/healthreturns your client ID and confirms the key works. - Try live requests — Open the API Playground, paste your key, and exercise endpoints directly from your browser.
- Implement — Use this guide for auth, data formats, order flows, and each endpoint.
The Playground is for manual testing only. In production, call the Partner API from your backend, not directly from browser code.
Base URL
Use this URL for all requests. All routes live under /v1/.
| Environment | URL |
|---|---|
| Production | https://vylvet.co/api |
Example: GET https://vylvet.co/api/v1/stores
Test Mode
Vylvet supports a full sandbox environment using Stripe test keys. No real money is charged. No real deliveries are dispatched. Everything else — store, products, pricing, order format, webhooks — is identical to production.
Option 1 — Test API key (recommended)
Ask your Vylvet admin contact to generate a test API key for your partner client. It starts with sk_test_. Using this key automatically routes all requests through Stripe test mode — no changes required anywhere else in your integration.
// Use your test key in the header — everything else stays the same
x-api-key: sk_test_your_test_key_here
Option 2 — dev flag per request
If you want to use your live key but have a specific request use test mode, add "dev": true to the request body of POST /v1/checkout/session or POST /v1/orders/report:
{
"dev": true,
"store_id": "your_store_id",
"items": [{ "product_id": "abc", "quantity": 1 }],
"success_url": "https://yourapp.com/success",
"cancel_url": "https://yourapp.com/cancel"
}
Test Stripe card numbers
| Card number | Result |
|---|---|
4242 4242 4242 4242 | Payment succeeds |
4000 0000 0000 0002 | Payment declined |
4000 0025 0000 3155 | 3DS authentication required |
Use any future expiry date (e.g. 12/34) and any 3-digit CVC.
What's different in test mode
- Stripe uses test keys — payment intents are clearly marked in your Stripe test dashboard
- Orders appear in Vylvet merchant CMS with a TEST badge and
"dev": true - Uber Direct uses sandbox credentials — no real courier dispatched
- Merchants can refund, cancel, and manage test orders from the Payments dashboard
Stripe webhook for test events
Register the webhook URL in your Stripe test dashboard (under Developers → Webhooks) pointing to:
https://us-central1-revero-beautygogo.cloudfunctions.net/cms-stripe_webhook
The endpoint verifies both test and live signatures automatically.
Authentication
Every request must include your partner API key in a header. You receive it once when your partner client is created; store it in a secure secret store and never expose it in frontend code or source control.
| Header | Required | Description |
|---|---|---|
x-api-key |
Yes | Your partner API key (e.g. sk_live_...). Sent on every request. |
x-trace-id |
No | Your correlation ID. If sent, we echo it in the response for debugging. |
Idempotency-Key |
Yes for some writes | Required for POST /v1/orders/report and POST /v1/inventory/adjustments. Recommended for POST /v1/checkout/session. Use a unique value per logical operation (e.g. your order ID) to avoid duplicates. |
All responses include x-trace-id. Include it when contacting support.
Example: GET /v1/health
GET /v1/health HTTP/1.1 Host: vylvet.co x-api-key: sk_live_your_key_here x-trace-id: myapp-req-12345
{
"ok": true,
"status": "healthy",
"partner_client_id": "client_abc",
"timestamp": "2026-02-27T15:04:05.000Z",
"trace_id": "ABC123XYZ"
}
interface HealthResponse {
ok: boolean;
status: string;
partner_client_id: string;
timestamp: string; // ISO 8601 UTC
trace_id: string;
}
Data formats
Consistent formats across the API.
Currency
All monetary amounts are in the smallest currency unit. The API uses USD; amounts are in cents (e.g. 1999 = $19.99). Product price, order item price, total, subtotal, tax_amount, delivery_fee are all in cents. There is no separate currency field; the system is single-currency (USD).
Timestamps
- Numeric (API responses) —
created_on,updated_on, andupdated_since(query) are Unix time in milliseconds (UTC). Example:1709123456789. - Webhook payloads — The
timestampfield in webhook JSON is ISO 8601 UTC (e.g.2026-02-27T15:04:05.000Z).
Two order flows
There are two ways to create orders. Use the right one for how the customer pays.
Flow 1: Card payment — POST /v1/checkout/session
- You send cart, delivery address,
success_url,cancel_url. - We create an order in status
pending_paymentand a Stripe Checkout Session. We check product availability at this time; if insufficient we return 409. - You redirect the customer to the returned
checkout_url. The customer pays on Stripe. - On payment success, our Stripe webhook sets the order to
processed. Only then do we deduct inventory and create delivery. If the customer never pays, the order stayspending_paymentand inventory is not deducted.
Flow 2: Already paid — POST /v1/orders/report
- Use for orders already paid outside our system (e.g. cash, or payment taken by you under a separate agreement).
- You send
external_order_id,store_id,items(withpricein cents), and optionallydelivery_address. - We create the order in status
processedand deduct inventory in a single transaction. If stock is insufficient we return 409 and do not create the order. - For delivery orders we then create the Uber delivery. You receive
order_idand can poll or use webhooks for status.
Before creating any order, call POST /v1/products/availability with the cart to avoid 409. For checkout/session, availability is checked again when you create the session.
Order state machine
Order status values and how they relate. For partner Stripe flow, orders go pending_payment → processed on payment success (we do not use paid for partner orders). The paid status is used in the native app flow before moving to processed.
Statuses: pending_payment, paid, processed, failed_payment, shipped, out_for_delivery, delivered, ready_for_pickup, picked_up, cancelled, refunded, refund_requested. Terminal states: delivered, picked_up, cancelled, refunded.
| From | To | Trigger |
|---|---|---|
| — | pending_payment | POST /v1/checkout/session |
| — | processed | POST /v1/orders/report |
pending_payment | processed | Stripe payment success (webhook) |
pending_payment | failed_payment | Stripe session creation failed |
processed | out_for_delivery | Delivery picked up |
out_for_delivery | delivered | Delivery completed |
processed | ready_for_pickup | Pickup order ready |
ready_for_pickup | picked_up | Customer collected |
processed | cancelled | Cancellation |
delivered | refunded | Refund completed |
Stores & categories
List stores and categories to map locations and taxonomy.
Store access: Your API key can be restricted to specific stores (configured in the Vylvet admin). When restricted, GET /v1/stores returns only those stores, and any request that uses a store_id (categories, products, checkout, orders, inventory) returns 403 Access denied to this store if the store is not allowed for your key.
GET /v1/stores
Query: active (default true), limit (default 100, max 300).
{
"ok": true,
"count": 2,
"stores": [
{
"id": "store_1",
"name": "Main Store",
"slug": "main-store",
"active": true,
"open": true,
"city": "San Francisco",
"state": "CA",
"zip_code": "94102",
"address": "123 Market St",
"pickup_enabled": true,
"vylvet_delivery_enabled": true,
"updated_on": 1709123456789
}
]
}
interface StoresResponse {
ok: boolean;
count: number;
stores: Store[];
}
interface Store {
id: string;
name: string;
slug: string;
active: boolean;
open: boolean;
city: string;
state: string;
zip_code: string;
address: string;
pickup_enabled: boolean;
vylvet_delivery_enabled: boolean;
updated_on: number; // Unix ms UTC
}
GET /v1/categories
Query: store_id (optional), active (default true). When store_id is provided, the response also includes store_id.
{
"ok": true,
"count": 3,
"categories": [
{ "id": "cat_1", "name": "Hair Care", "active": true }
]
}
When store_id query is set, the response also includes store_id.
interface CategoriesResponse {
ok: boolean;
count: number;
categories: Category[];
store_id?: string; // present when store_id query param was provided
}
interface Category {
id: string;
name: string;
active: boolean;
}
GET /v1/stores/:store_id/categories
Categories that have products in this store. Response shape same as GET /v1/categories.
Products
Read and optionally sync the catalog. Check availability before creating orders.
GET /v1/products
Query: store_id, category_id, active (default true), available_only (default false), limit (default 100, max 300), updated_since (Unix ms), cursor (use next_cursor from previous response for pagination).
{
"ok": true,
"count": 10,
"next_cursor": "1709123456789",
"filters": { "store_id": "store_1" },
"products": [
{
"id": "prod_1",
"name": "Shampoo",
"description": "Gentle formula",
"store_id": "store_1",
"category_id": "cat_1",
"price": 1999,
"sku": "SH-001",
"active": true,
"available_quantity": 50,
"images": ["https://..."],
"updated_on": 1709123456789
}
]
}
interface ProductsResponse {
ok: boolean;
count: number;
next_cursor?: string;
filters?: Record<string, string>;
products: Product[];
}
interface Product {
id: string;
name: string;
description?: string;
store_id: string;
category_id?: string;
price: number; // cents USD
sku?: string;
active: boolean;
available_quantity: number;
featured_image?: string | null;
images?: string[];
updated_on: number; // Unix ms UTC
}
GET /v1/products/:product_id
Single product. Returns 404 if not found. Response is always { "ok": true, "product": Product } with shape matching Product above.
POST /v1/products/availability
Call before creating an order to avoid 409. Pass an optional store_id to scope the check — any product whose store_id doesn't match will come back with wrong_store: true and available: false.
{
"store_id": "store_1", // optional — scope the check
"items": [
{ "product_id": "prod_1", "quantity": 2 },
{ "product_id": "prod_2", "quantity": 1 }
]
}
{
"ok": true,
"store_id": "store_1",
"all_available": false,
"items": [
{
"product_id": "prod_1",
"found": true,
"store_id": "store_1",
"wrong_store": false,
"active": true,
"available": true,
"requested_quantity": 2,
"available_quantity": 50
},
{
"product_id": "prod_2",
"found": true,
"store_id": "store_2",
"wrong_store": true, // belongs to a different store
"active": true,
"available": false,
"requested_quantity": 1,
"available_quantity": 0
}
]
}
interface AvailabilityRequest {
store_id?: string;
items: { product_id: string; quantity: number }[];
}
interface AvailabilityResponse {
ok: boolean;
store_id: string | null;
all_available: boolean;
items: {
product_id: string;
found: boolean;
store_id?: string | null;
wrong_store?: boolean; // only when request specified store_id
available: boolean;
active?: boolean;
requested_quantity: number;
available_quantity: number;
}[];
}
POST /v1/products/upsert
Create or update a single product. Requires write_products scope and, if the API key has partner_store_ids, the target store must be allowed. Pass id to update an existing product; omit it to create a new one (a product_id is generated). The request body may be either the product object directly or wrapped as {"product": {...}}.
{
"store_id": "store_1",
"category_id": "cat_1",
"name": "Hydrating Serum",
"description": "Daily hyaluronic acid serum",
"price": 29.99,
"available_quantity": 25,
"active": true,
"hot": false,
"featured_image": "https://cdn.example.com/serum.jpg",
"images": ["https://cdn.example.com/serum.jpg"],
"sku": "SKU-001"
}
{
"ok": true,
"product_id": "prod_abc123",
"product": {
"id": "prod_abc123",
"name": "Hydrating Serum",
"description": "Daily hyaluronic acid serum",
"store_id": "store_1",
"category_id": "cat_1",
"featured_image": "https://cdn.example.com/serum.jpg",
"images": ["https://cdn.example.com/serum.jpg"],
"active": true,
"hot": false,
"price": 29.99,
"available_quantity": 25,
"sku": "SKU-001",
"sold_count": 0,
"sold_amount": 0,
"source": "myavana",
"created_on": 1713561600000,
"updated_on": 1713561600000
}
}
interface ProductUpsertRequest {
// Required
store_id: string;
category_id: string;
name: string;
// Optional
id?: string; // set to update a specific product
description?: string;
featured_image?: string;
images?: string[];
active?: boolean; // defaults to true
hot?: boolean;
price?: number; // major units (e.g. USD)
available_quantity?: number; // absolute quantity (not a delta)
sku?: string;
sold_count?: number;
sold_amount?: number;
created_on?: number; // epoch ms; defaults to now
}
// You may also wrap the payload: { "product": ProductUpsertRequest }
interface ProductUpsertResponse {
ok: boolean;
product_id: string;
product: {
id: string;
name: string;
description: string;
store_id: string;
category_id: string;
featured_image: string;
images: string[];
active: boolean;
hot: boolean;
price: number;
available_quantity: number;
sku: string;
sold_count: number;
sold_amount: number;
source: "myavana";
created_on: number;
updated_on: number;
};
}
Note: available_quantity is an absolute stock quantity (overwrites current stock). For deltas (restock / correction), use POST /v1/inventory/adjustments.
Errors: 400 if store_id, category_id, or name is missing; 403 if the key lacks write_products or the store is outside partner_store_ids.
Delivery address & quote
Before a customer checks out, let them choose a nearby store and preview the delivery fee. Both endpoints accept either a free-form address string (which we geocode) or an explicit { lat, lng } pair.
POST /v1/stores/nearby
Returns stores within a given radius of a delivery address, sorted by distance. Requires read_stores scope. Honours the API key's store restrictions (partner_store_ids) and each store's own delivery radius (vylvet_delivery_radius_miles) — the effective cutoff is the minimum of the store's radius and max_distance_miles.
{
"delivery_address": "1 Infinite Loop, Cupertino, CA 95014",
"max_distance_miles": 10,
"limit": 20
}
{
"lat": 37.3318,
"lng": -122.0312,
"max_distance_miles": 10
}
{
"ok": true,
"count": 2,
"query": {
"lat": 37.3318,
"lng": -122.0312,
"formatted_address": "1 Infinite Loop, Cupertino, CA 95014, USA",
"max_distance_miles": 10
},
"stores": [
{
"id": "store_1",
"name": "Main Store",
"slug": "main-store",
"address": "123 Market St",
"city": "San Francisco",
"state": "CA",
"zip_code": "94102",
"lat": 37.78,
"lng": -122.41,
"distance_miles": 2.45,
"vylvet_delivery_enabled": true,
"vylvet_delivery_radius_miles": 8,
"open": true
}
]
}
interface StoresNearbyRequest {
delivery_address?: string; // geocoded server-side
lat?: number; // alternative to delivery_address
lng?: number;
max_distance_miles?: number; // default 10
limit?: number; // default 20, max 50
}
interface StoresNearbyResponse {
ok: boolean;
count: number;
query: {
lat: number;
lng: number;
formatted_address?: string;
max_distance_miles: number;
};
stores: NearbyStore[];
}
interface NearbyStore {
id: string;
name: string;
slug: string;
address: string;
city: string;
state: string;
zip_code: string;
lat: number;
lng: number;
distance_miles: number;
vylvet_delivery_enabled: boolean;
vylvet_delivery_radius_miles?: number;
open: boolean;
}
Errors: 400 if neither delivery_address nor {lat,lng} is provided, or if the address cannot be geocoded.
POST /v1/delivery/quote
Get a live Uber Direct delivery fee and ETA from a specific store to a customer address. Requires read_delivery or write_orders scope. Quotes are short-lived — typically valid for a few minutes — so request one just before starting checkout.
{
"store_id": "store_1",
"delivery_address": {
"phone": "+15551234567",
"address": {
"line1": "1 Infinite Loop",
"city": "Cupertino",
"state": "CA",
"postal_code": "95014",
"country": "US"
}
},
"items": [ // optional — improves Uber's sizing
{ "product_id": "prod_1", "quantity": 1, "name": "Serum" }
],
"subtotal": 2999, // optional, cents
"dev": true // optional — use Uber sandbox
}
{
"ok": true,
"quote_id": "dqt_abc123",
"fee_cents": 599,
"currency": "usd",
"dropoff_eta": "2026-02-27T15:30:00.000Z", // ISO 8601 UTC
"eta_seconds": 1320, // seconds from now
"duration_seconds": 1200,
"pickup_duration_seconds": 600,
"expires": "2026-02-27T15:10:00.000Z", // quote expiry, ISO 8601 UTC
"distance_miles": 2.45,
"trace_id": "..."
}
interface DeliveryQuoteRequest {
store_id: string;
delivery_address: {
phone?: string;
address: {
line1: string;
line2?: string;
city: string;
state: string;
postal_code: string;
country: string;
};
};
items?: { product_id?: string; quantity?: number; name?: string }[];
subtotal?: number; // cents
dev?: boolean;
}
interface DeliveryQuoteResponse {
ok: boolean;
quote_id: string;
fee_cents: number;
currency: string; // e.g. "usd"
dropoff_eta: string; // ISO 8601 UTC
eta_seconds: number;
duration_seconds: number;
pickup_duration_seconds: number;
expires: string; // ISO 8601 UTC
distance_miles: number | null;
trace_id: string;
}
Errors: 400 for missing/invalid address or when Uber Direct cannot quote this route (out-of-zone, no couriers); 403 if the store is not allowed for your API key or lacks vylvet_delivery_enabled; 404 if the store is not found.
Orders & delivery
For card payments use POST /v1/checkout/session and redirect to Stripe. For already-paid orders use POST /v1/orders/report. Then poll or use webhooks for status.
POST /v1/checkout/preflight
One-shot eligibility check for a cart. Combines store status + fulfillment capability + item availability (+ a live delivery quote when fulfillment_type: "delivery") so you can decide whether to proceed to checkout without making three separate calls. Non-destructive: no order is written, no idempotency key required. Requires read_stores and read_products; for delivery also read_delivery or write_orders.
{
"store_id": "store_1",
"fulfillment_type": "delivery", // or "pickup"
"items": [
{ "product_id": "prod_1", "quantity": 2 }
],
"delivery_address": { // required for delivery
"phone": "+15551234567",
"address": {
"line1": "123 Main St",
"city": "San Francisco",
"state": "CA",
"postal_code": "94102",
"country": "US"
}
},
"subtotal": 5998, // optional — lets Uber weight the quote
"dev": true // optional — use Uber sandbox
}
{
"ok": true,
"eligible": true,
"reasons": [],
"fulfillment_type": "delivery",
"store": {
"id": "store_1",
"name": "Radiance Beauty Supply",
"active": true,
"open": true,
"city": "San Francisco",
"state": "CA",
"pickup_enabled": true,
"vylvet_delivery_enabled": true,
"vylvet_delivery_radius_miles": 15,
"distance_miles": 1.82
},
"availability": {
"all_available": true,
"items": [
{ "product_id": "prod_1", "found": true, "store_id": "store_1",
"wrong_store": false, "active": true, "available": true,
"requested_quantity": 2, "available_quantity": 50 }
]
},
"quote": {
"quote_id": "dqt_abc123",
"fee_cents": 799,
"currency": "usd",
"dropoff_eta": "2026-04-20T22:15:00Z",
"eta_seconds": 1800,
"duration_seconds": 1200,
"pickup_duration_seconds": 600,
"expires": "2026-04-20T22:00:00Z"
},
"trace_id": "..."
}
{
"ok": true,
"eligible": false,
"reasons": ["item_unavailable"], // e.g. also: "store_inactive",
// "store_delivery_disabled",
// "store_pickup_disabled",
// "out_of_radius",
// "quote_unavailable"
"fulfillment_type": "delivery",
"store": { "id": "store_1", /* ... */ },
"availability": { "all_available": false, "items": [ /* ... */ ] },
"quote": null,
"trace_id": "..."
}
Once eligible: true, you can safely call POST /v1/checkout/session (or POST /v1/orders/report for externally-paid orders). The quote returned here is for display — checkout/session and orders/report will re-quote when they create the delivery.
POST /v1/checkout/session
Creates order in pending_payment and a Stripe Checkout Session. Redirect the customer to the returned checkout_url. After payment we set the order to processed, deduct inventory, and create delivery. Requires write_orders. Headers: Idempotency-Key recommended.
{
"store_id": "store_1",
"items": [
{ "product_id": "prod_1", "quantity": 2 }
],
"fulfillment_type": "delivery",
"success_url": "https://yourapp.com/success",
"cancel_url": "https://yourapp.com/cancel",
"delivery_address": {
"name": "Jane Doe",
"email": "jane@example.com",
"phone": "+15551234567",
"address": {
"line1": "123 Main St",
"city": "San Francisco",
"state": "CA",
"postal_code": "94102",
"country": "US"
}
},
"delivery_fee": 500,
"customer_email": "jane@example.com",
"pickup_instructions": "",
"dropoff_instructions": "",
"dev": false
}
{
"ok": true,
"order_id": "ABC123XYZ",
"session_id": "cs_test_...",
"checkout_url": "https://checkout.stripe.com/c/pay/cs_test_...",
"trace_id": "TRACE_ABC"
}
interface CheckoutSessionRequest {
store_id: string;
items: { product_id: string; quantity: number }[];
fulfillment_type: "delivery" | "pickup";
success_url: string;
cancel_url: string;
delivery_address?: DeliveryAddress; // required if delivery
delivery_fee?: number; // cents
customer_email?: string;
pickup_instructions?: string;
dropoff_instructions?: string;
dev?: boolean;
}
interface DeliveryAddress {
name: string;
email?: string;
phone?: string;
address: {
line1: string;
line2?: string;
city: string;
state: string;
postal_code: string;
country: string;
};
}
interface CheckoutSessionResponse {
ok: boolean;
order_id: string;
session_id: string;
checkout_url: string;
trace_id: string;
}
Errors: 400 missing/invalid body or URLs; 409 insufficient stock.
success_url, then poll GET /v1/orders/:order_id/tracking to drive a live order-status screen. See Polling cadence for recommended intervals. For a preview of the courier's progress, embed the uber_tracking_url returned by /tracking in a webview.POST /v1/orders/report
Headers: Idempotency-Key required.
{
"external_order_id": "EXT_123",
"store_id": "store_1",
"items": [
{ "product_id": "prod_1", "quantity": 2, "price": 1999 }
],
"fulfillment_type": "delivery",
"total": 4500,
"subtotal": 4000,
"tax_amount": 400,
"tax_rate": 10,
"delivery_fee": 100,
"delivery_address": {
"name": "Customer Name",
"email": "customer@example.com",
"phone": "+15551234567",
"address": {
"line1": "123 Main St",
"city": "Los Angeles",
"state": "CA",
"postal_code": "90001",
"country": "US"
}
},
"pickup_instructions": "",
"dropoff_instructions": "",
"external_payment_id": "pay_abc",
"dev": false
}
{
"ok": true,
"order_id": "ORD_XYZ",
"external_order_id": "EXT_123",
"status": "processed",
"delivery": {
"created": true,
"uber_delivery_id": "ud_...",
"uber_tracking_url": "https://..."
},
"trace_id": "TRACE_ABC"
}
interface OrdersReportRequest {
external_order_id: string;
store_id: string;
items: { product_id: string; quantity: number; price: number }[];
fulfillment_type?: "delivery" | "pickup";
total?: number;
subtotal?: number;
tax_amount?: number;
tax_rate?: number;
delivery_fee?: number;
delivery_address?: DeliveryAddress;
pickup_instructions?: string;
dropoff_instructions?: string;
external_payment_id?: string;
dev?: boolean;
}
interface OrdersReportResponse {
ok: boolean;
order_id: string;
external_order_id: string;
status: string;
delivery?: {
created: boolean;
uber_delivery_id?: string;
uber_tracking_url?: string;
};
trace_id: string;
}
Errors: 409 insufficient stock; 400 invalid store/product or missing fields.
GET /v1/orders/:order_id
Full order details. You can only access orders that belong to your partner client.
{
"ok": true,
"order": {
"id": "ORD_XYZ",
"store_id": "store_1",
"created_on": 1709123456789,
"customer_id": "partner_client_ORD_XYZ",
"items": [
{ "product_id": "prod_1", "quantity": 2, "price": 2199, "original_price": 1999 }
],
"source": "myavana",
"partner_client_id": "client_abc",
"fulfillment_type": "delivery",
"delivery_address": { ... },
"delivery_fee": 500,
"subtotal": 4398,
"tax_amount": 440,
"total": 5338,
"status": "processed",
"updated_on": 1709123500000,
"uber_delivery_id": "ud_...",
"uber_tracking_url": "https://..."
}
}
interface OrderResponse {
ok: boolean;
order: Order;
}
interface Order {
id: string;
store_id: string;
created_on: number;
customer_id: string;
items: { product_id: string; quantity: number; price: number; original_price?: number }[];
source?: "vylvet" | "shopify" | "myavana";
partner_client_id?: string;
external_order_id?: string;
fulfillment_type: "delivery" | "pickup";
delivery_address?: DeliveryAddress | null;
delivery_fee: number;
subtotal: number;
tax_amount: number;
total: number;
status: OrderStatus;
updated_on: number;
uber_delivery_id?: string;
uber_tracking_url?: string;
// ... other fields
}
type OrderStatus =
| "pending_payment" | "paid" | "processed" | "failed_payment" | "shipped"
| "out_for_delivery" | "delivered" | "ready_for_pickup" | "picked_up"
| "cancelled" | "refunded" | "refund_requested";
GET /v1/orders/by-external/:external_order_id
Look up order by your external_order_id (for orders created via orders/report). Response shape: { "ok": true, "order": Order }.
GET /v1/orders/:order_id/ping
Lightweight order status.
{
"ok": true,
"order_id": "ORD_XYZ",
"external_order_id": "EXT_123",
"status": "processed",
"fulfillment_type": "delivery",
"uber_delivery_id": "ud_...",
"uber_tracking_url": "https://...",
"estimated_delivery_time": "2026-02-27T16:00:00Z",
"updated_on": 1709123500000
}
interface OrderPingResponse {
ok: boolean;
order_id: string;
external_order_id: string | null;
status: string;
fulfillment_type: string;
uber_delivery_id: string | null;
uber_tracking_url: string | null;
estimated_delivery_time: string | null;
updated_on: number;
}
GET /v1/orders/:order_id/driver-location
Requires read_delivery scope. Returns the latest known courier position and ETA. We serve from Firestore when the last update is fresh (<60s) and the delivery is not in a terminal state (delivered/cancelled). If the stored data is stale or missing, we fall through to a live Uber Direct call so the response always reflects the most recent courier state.
{
"ok": true,
"order_id": "ORD_XYZ",
"status": "out_for_delivery",
"uber_delivery_status": "en_route_to_dropoff",
"uber_delivery_id": "ud_...",
"uber_tracking_url": "https://trk.ubereats.com/...",
"courier_imminent": false,
"courier": {
"name": "Driver Name",
"phone_number": "+1555...",
"vehicle_type": "car",
"img_href": "https://img.uber.com/courier/abc.jpg",
"location": { "lat": 37.78, "lng": -122.41 }
},
"estimated_delivery_time": "2026-02-27T16:00:00Z",
"source": "live"
}
interface DriverLocationResponse {
ok: boolean;
order_id: string;
status: string; // order status
uber_delivery_status: string | null; // Uber-level: pickup, en_route_to_dropoff, delivered, ...
uber_delivery_id: string | null;
uber_tracking_url: string | null;
courier_imminent: boolean; // courier is close to the customer
courier: {
name?: string;
phone_number?: string;
vehicle_type?: string;
img_href?: string; // courier photo URL
location?: { lat: number; lng: number };
};
estimated_delivery_time: string | null;
source: "cache" | "live"; // where the data came from on this request
}
Tracking & timeline
Two endpoints to surface delivery progress in your UI. tracking is the single-call view optimised for customer-facing screens; timeline is an append-only log of every state change for audits or support tooling.
GET /v1/orders/:order_id/tracking
Unified response combining order status, delivery state, courier info, and the most recent timeline events. Requires read_orders. Falls through to a live Uber Direct call for non-terminal deliveries when the stored snapshot is stale.
{
"ok": true,
"order_id": "ORD_XYZ",
"status": "out_for_delivery",
"uber_delivery_status": "en_route_to_dropoff",
"uber_delivery_id": "ud_...",
"uber_tracking_url": "https://trk.ubereats.com/...",
"estimated_delivery_time": "2026-02-27T16:00:00Z",
"courier_imminent": false,
"courier": {
"name": "Driver Name",
"phone_number": "+1555...",
"vehicle_type": "car",
"img_href": "https://img.uber.com/courier/abc.jpg",
"location": { "lat": 37.78, "lng": -122.41 }
},
"recent_events": [
{ "event": "order.processed", "created_on": 1709123450000 },
{ "event": "delivery.created", "created_on": 1709123460000 },
{ "event": "delivery.status_changed", "uber_delivery_status": "en_route_to_dropoff", "created_on": 1709123700000 }
]
}
interface OrderTrackingResponse {
ok: boolean;
order_id: string;
status: string;
uber_delivery_status: string | null;
uber_delivery_id: string | null;
uber_tracking_url: string | null;
estimated_delivery_time: string | null;
courier_imminent: boolean;
courier: {
name?: string;
phone_number?: string;
vehicle_type?: string;
img_href?: string;
location?: { lat: number; lng: number };
};
recent_events: OrderTimelineEvent[];
}
GET /v1/orders/:order_id/timeline
Full append-only log of state changes — Stripe payment, inventory deduction, Uber delivery creation, every courier update, cancellations and refunds. Requires read_orders. Query: limit (default 100, max 500).
{
"ok": true,
"order_id": "ORD_XYZ",
"count": 5,
"events": [
{ "id": "evt_1", "event": "order.created", "status": "pending_payment", "created_on": 1709123440000, "message": "Checkout session created" },
{ "id": "evt_2", "event": "order.processed", "status": "processed", "created_on": 1709123450000, "data": { "stripe_payment_intent_id": "pi_..." } },
{ "id": "evt_3", "event": "delivery.created", "status": "processed", "created_on": 1709123460000, "data": { "uber_delivery_id": "ud_..." } },
{ "id": "evt_4", "event": "delivery.status_changed", "uber_delivery_status": "en_route_to_dropoff", "created_on": 1709123700000 },
{ "id": "evt_5", "event": "delivery.delivered", "status": "delivered", "created_on": 1709124000000 }
]
}
interface OrderTimelineResponse {
ok: boolean;
order_id: string;
count: number;
events: OrderTimelineEvent[];
}
interface OrderTimelineEvent {
id: string;
order_id: string;
event: OrderTimelineEventType;
status?: string | null;
uber_delivery_status?: string | null;
message?: string | null;
data?: Record<string, unknown> | null;
created_on: number; // Unix ms UTC
}
type OrderTimelineEventType =
| "order.created" | "order.pending_payment" | "order.paid" | "order.processed"
| "order.failed_payment" | "order.cancelled" | "order.refund_requested" | "order.refunded"
| "delivery.created" | "delivery.shipped" | "delivery.out_for_delivery" | "delivery.delivered"
| "delivery.cancelled" | "delivery.courier_assigned" | "delivery.courier_update"
| "delivery.status_changed";
Cancel & refund
Cancel or refund an order after it has been created. Both endpoints require write_orders. Both require an Idempotency-Key header — use a stable key per logical operation (e.g. your cancellation ticket ID).
POST /v1/orders/:order_id/cancel
Cancels the order and, if a delivery exists, cancels the Uber Direct delivery. If inventory had been deducted (status processed, shipped, or out_for_delivery), stock is restored atomically. We emit delivery.cancelled to your webhook when the delivery is cancelled.
Refused in terminal states: delivered, picked_up, cancelled, refunded. For delivered orders, use refund instead.
{
"reason": "customer_requested"
}
{
"ok": true,
"order_id": "ORD_XYZ",
"status": "cancelled",
"delivery_cancelled": true,
"inventory_restored": true
}
interface OrderCancelRequest {
reason?: string; // free text for audit log
}
interface OrderCancelResponse {
ok: boolean;
order_id: string;
status: "cancelled";
delivery_cancelled: boolean;
inventory_restored: boolean;
}
Errors: 400 missing Idempotency-Key; 404 order not found; 409 order is already in a terminal state.
POST /v1/orders/:order_id/refund
Issues a Stripe refund against the payment intent recorded on the order. Full refund by default; pass amount_cents for a partial refund. When the full order amount has been refunded, the order status becomes refunded.
{
"amount_cents": 1000,
"reason": "requested_by_customer"
}
{
"ok": true,
"order_id": "ORD_XYZ",
"stripe_refund_id": "re_...",
"amount_refunded_cents": 1000,
"total_refunded_cents": 1000,
"status": "processed"
}
interface OrderRefundRequest {
amount_cents?: number; // omit for full refund
reason?: "duplicate" | "fraudulent" | "requested_by_customer" | string;
}
interface OrderRefundResponse {
ok: boolean;
order_id: string;
stripe_refund_id: string;
amount_refunded_cents: number; // this call
total_refunded_cents: number; // cumulative for the order
status: string; // "refunded" once total equals order total
}
Errors: 400 missing Idempotency-Key or invalid amount; 404 order not found; 409 order has no Stripe payment intent (e.g. reported via orders/report with external payment), or the order is already fully refunded; 502 Stripe returned an error — inspect the response body for the Stripe message.
For orders created via POST /v1/orders/report (paid outside Vylvet), this endpoint returns 409 — refund the customer in your own payment system.
Polling cadence
Webhooks are the primary signal for order and delivery state changes. Polling is a fallback — use it when you can't receive webhooks, or alongside webhooks for screens the customer is actively watching.
| Order state | Recommended interval | Endpoint |
|---|---|---|
pending_payment | stop polling — wait for the Stripe webhook | — |
processed, shipped | every 30s | /tracking or /ping |
out_for_delivery | every 10s while the customer screen is visible | /tracking (or /driver-location for just courier position) |
delivered, picked_up, cancelled, refunded | stop polling — terminal | — |
- Stop on terminal states. Continued polling against
delivered/cancelled/refundedorders wastes quota and will not yield new information. - Back off on 429. Default rate limit is 240 req/min per API key. If you hit it, double your interval until 2xx.
- Prefer
/trackingover/ping+/driver-location. The tracking endpoint returns everything in one round-trip — status, courier, recent events. - When the app backgrounds, stop polling. Resume on foreground with a single
/trackingcall to sync.
Payment processing (Stripe)
Card payments are processed by Vylvet. You must not charge the customer in your own system and then call orders/report. Instead: call POST /v1/checkout/session, then redirect the customer to the returned checkout_url (or use Stripe.js / SDK with the returned session_id and our publishable key).
What we provide
- Stripe publishable key — Production and test keys. Use the test key in development and the production key in production. Contact Vylvet (tech@vylvet.co) to receive these; they are not in a developer dashboard.
- Product list —
GET /v1/products(price in cents USD). Use for display and cart; do not send to your own payment provider. - Checkout session —
POST /v1/checkout/sessionreturnscheckout_urlandsession_id. Redirect the customer; after payment we fulfill and send webhooks.
By platform
- Web —
window.location.href = checkout_urlor Stripe.jsredirectToCheckout({ sessionId }). - React Native —
@stripe/stripe-react-nativeor opencheckout_urlin WebView. Use our publishable key andsession_id. - Ionic / Capacitor — Open
checkout_urlin in-app or system browser, or Stripe SDK withsessionId+ our publishable key. - Flutter — Open
checkout_urlin WebView orurl_launcher, or Stripe Flutter SDK withsession_idif supported.
POST /v1/orders/report for that order. Use only the checkout session API for card payments.Inventory
Adjust stock (restock or correction). Requires write_inventory scope. Method and path: POST /v1/inventory/adjustments. Headers: Idempotency-Key required.
{
"adjustments": [
{ "product_id": "prod_1", "delta": 10, "reason": "restock", "external_reference": "PO-001" },
{ "product_id": "prod_2", "delta": -2, "reason": "correction" }
]
}
{
"ok": true,
"adjusted": 2,
"adjustments": [
{
"product_id": "prod_1",
"previous_quantity": 40,
"next_quantity": 50,
"delta": 10
},
{
"product_id": "prod_2",
"previous_quantity": 5,
"next_quantity": 3,
"delta": -2
}
]
}
interface InventoryAdjustmentsRequest {
adjustments: {
product_id: string;
delta: number; // positive = add, negative = subtract
reason?: string;
external_reference?: string;
}[];
}
interface InventoryAdjustmentsResponse {
ok: boolean;
adjusted: number;
adjustments: {
product_id: string;
previous_quantity: number;
next_quantity: number;
delta: number;
}[];
}
Negative delta reduces stock; the request fails if any result would go below zero. Atomicity: All adjustments in a single request are applied in one transaction. If any one adjustment fails (e.g. product not found or result < 0), the entire request fails and no changes are applied. Same Idempotency-Key returns the same response (idempotent).
Webhooks
Vylvet sends events to your URL when order and delivery status changes. Configure Webhook URL and optional Webhook secret on your partner client.
| Event | When |
|---|---|
order.accepted | Order created and inventory deducted (status is processed) |
delivery.created | Delivery created; includes uber_delivery_id, uber_tracking_url |
delivery.out_for_delivery | Driver is on the way |
delivery.delivered | Delivery completed |
delivery.cancelled | Delivery cancelled |
delivery.courier_update | Courier/location update |
Payload: JSON with event, order_id, timestamp (ISO 8601 UTC), trace_id, data. Return 2xx quickly so we can consider the event delivered.
{
"event": "delivery.created",
"order_id": "ORD_XYZ",
"timestamp": "2026-02-27T15:04:05.000Z",
"trace_id": "TRACE_ABC",
"data": {
"uber_delivery_id": "ud_...",
"uber_tracking_url": "https://..."
}
}
interface WebhookPayload {
event: string; // e.g. "order.accepted" | "delivery.created" | ...
order_id: string;
timestamp: string; // ISO 8601 UTC
trace_id: string | null;
data: Record<string, unknown>;
}
Signature verification
If you set a webhook secret, we send x-partner-signature: HMAC-SHA256 of the raw request body (UTF-8), hex-encoded. Verify before processing; use a timing-safe comparison to avoid timing attacks.
const crypto = require('crypto');
const rawBody = req.rawBody; // or req.body as string, exactly as received
const expected = crypto.createHmac('sha256', WEBHOOK_SECRET).update(rawBody).digest('hex');
const received = req.headers['x-partner-signature'] || '';
if (expected.length !== received.length || !crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(received, 'hex'))) {
return res.status(401).send('Invalid signature');
}
Delivery
Implement your handler to be idempotent (e.g. use order_id + event + timestamp to deduplicate). Events may arrive out of order; process based on order state if needed. For critical flows, poll GET /v1/orders/:order_id/ping as a backup.
Errors & rate limits
| Status | Meaning |
|---|---|
| 400 | Bad request — missing or invalid body, or missing Idempotency-Key where required. Body: { "ok": false, "error": "message" }. |
| 401 | Missing or invalid x-api-key. |
| 403 | Valid key but missing scope for this endpoint. |
| 404 | Resource not found (order, product, store). |
| 409 | Conflict — e.g. insufficient stock on POST /v1/orders/report or POST /v1/checkout/session. |
| 429 | Rate limit exceeded. See below. |
| 500 | Server error. Include x-trace-id when contacting support. |
All JSON error responses include ok: false and error (string).
{
"ok": false,
"error": "Insufficient stock for prod_1. requested=5, available=2",
"trace_id": "TRACE_ABC"
}
interface ErrorResponse {
ok: false;
error: string;
trace_id?: string | null;
}
Rate limiting
Limits are per API key (per partner client). Default is 240 requests per minute, configurable per client. The window is a fixed 1-minute bucket (rolling). On 429, implement exponential backoff and retry; include x-trace-id from the response when reporting issues.
Scopes
Your API key can be restricted to specific scopes. Full access is *.
read_stores,read_categories,read_products,write_productsread_orders,write_orders,read_delivery,write_inventory
API versioning
The API uses path versioning (/v1/). We aim for backward compatibility within the same major version: existing response fields and behavior will not change in breaking ways without a new version. Breaking changes will be introduced under a new path (e.g. /v2/) with advance notice. Deprecations will be communicated with sufficient time to migrate.
Support
For integration help, contact technical support. Include the following so we can debug quickly:
Technical support
x-trace-idfrom the API response (or request)- Your partner client name
- Endpoint and method (e.g.
POST /v1/orders/report) - Short description or error message
Keep your API key secure; it is shown only once when generated. For webhooks, ensure your endpoint returns 2xx quickly and verify x-partner-signature when a secret is configured.