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. - Implement — Follow the sections below for auth, data formats, order flows, and each endpoint.
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
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.
{
"items": [
{ "product_id": "prod_1", "quantity": 2 },
{ "product_id": "prod_2", "quantity": 1 }
]
}
{
"ok": true,
"items": [
{
"product_id": "prod_1",
"found": true,
"available": true,
"active": true,
"requested_quantity": 2,
"available_quantity": 50
},
{
"product_id": "prod_2",
"found": true,
"available": false,
"active": true,
"requested_quantity": 1,
"available_quantity": 0
}
]
}
interface AvailabilityRequest {
items: { product_id: string; quantity: number }[];
}
interface AvailabilityResponse {
ok: boolean;
items: {
product_id: string;
found: boolean;
available: boolean;
active: boolean;
requested_quantity: number;
available_quantity: number;
}[];
}
POST /v1/products/upsert
Requires write_products scope. Body: { "product": { "store_id", "category_id", "name", "description", "price", "available_quantity", "sku", "active", "images", ... } }. Include id to update.
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/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.
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.
{
"ok": true,
"order_id": "ORD_XYZ",
"status": "out_for_delivery",
"uber_delivery_id": "ud_...",
"courier": {
"name": "Driver Name",
"phone_number": "+1555...",
"vehicle_type": "car",
"location": { "lat": 37.78, "lng": -122.41 }
},
"estimated_delivery_time": "2026-02-27T16:00:00Z"
}
interface DriverLocationResponse {
ok: boolean;
order_id: string;
status: string;
uber_delivery_id: string | null;
courier: {
name?: string;
phone_number?: string;
vehicle_type?: string;
location?: { lat: number; lng: number };
};
estimated_delivery_time: string | null;
}
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.