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

  1. Get your API key — Vylvet creates a partner client and gives you an API key once. Store it securely (e.g. environment variable).
  2. Call the API — Send requests to https://vylvet.co/api with header x-api-key: <your_key>.
  3. VerifyGET https://vylvet.co/api/v1/health returns your client ID and confirms the key works.
  4. 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/.

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

HeaderRequiredDescription
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
Response (200)
{
  "ok": true,
  "status": "healthy",
  "partner_client_id": "client_abc",
  "timestamp": "2026-02-27T15:04:05.000Z",
  "trace_id": "ABC123XYZ"
}
TypeScript interface
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, and updated_since (query) are Unix time in milliseconds (UTC). Example: 1709123456789.
  • Webhook payloads — The timestamp field 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.

flowchart LR subgraph Card["Card payment (Vylvet processes)"] A[POST /v1/checkout/session] --> B[Order pending_payment] B --> C[Redirect to Stripe] C --> D[Customer pays] D --> E[Webhook: order → processed] E --> F[Inventory deducted] F --> G[Delivery created] end subgraph Other["Already paid (e.g. cash)"] H[POST /v1/orders/report] --> I[Order processed] I --> J[Inventory deducted in same request] J --> K[Delivery created if delivery] end

Flow 1: Card payment — POST /v1/checkout/session

  • You send cart, delivery address, success_url, cancel_url.
  • We create an order in status pending_payment and 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 stays pending_payment and 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 (with price in cents), and optionally delivery_address.
  • We create the order in status processed and 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_id and 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_paymentprocessed 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.

stateDiagram-v2 [*] --> pending_payment: checkout/session pending_payment --> processed: Stripe payment success pending_payment --> failed_payment: session creation failed [*] --> processed: orders/report processed --> shipped: fulfillment processed --> out_for_delivery: delivery picked up out_for_delivery --> delivered: delivery completed processed --> ready_for_pickup: pickup ready ready_for_pickup --> picked_up: customer picked up processed --> cancelled: cancelled delivered --> refunded: refunded processed --> refund_requested

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.

FromToTrigger
pending_paymentPOST /v1/checkout/session
processedPOST /v1/orders/report
pending_paymentprocessedStripe payment success (webhook)
pending_paymentfailed_paymentStripe session creation failed
processedout_for_deliveryDelivery picked up
out_for_deliverydeliveredDelivery completed
processedready_for_pickupPickup order ready
ready_for_pickuppicked_upCustomer collected
processedcancelledCancellation
deliveredrefundedRefund 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

GET /v1/stores

Query: active (default true), limit (default 100, max 300).

Response (200)
{
  "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
    }
  ]
}
TypeScript interfaces
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

GET /v1/categories

Query: store_id (optional), active (default true). When store_id is provided, the response also includes store_id.

Response (200)
{
  "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.

TypeScript interfaces
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

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

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

Response (200)
{
  "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
    }
  ]
}
TypeScript interfaces
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

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

POST /v1/products/availability

Call before creating an order to avoid 409.

Request body
{
  "items": [
    { "product_id": "prod_1", "quantity": 2 },
    { "product_id": "prod_2", "quantity": 1 }
  ]
}
Response (200)
{
  "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
    }
  ]
}
TypeScript interfaces
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

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

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.

Request body
{
  "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
}
Response (201)
{
  "ok": true,
  "order_id": "ABC123XYZ",
  "session_id": "cs_test_...",
  "checkout_url": "https://checkout.stripe.com/c/pay/cs_test_...",
  "trace_id": "TRACE_ABC"
}
TypeScript interfaces
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

POST /v1/orders/report

Headers: Idempotency-Key required.

Request body
{
  "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
}
Response (201)
{
  "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"
}
TypeScript interfaces
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

GET /v1/orders/:order_id

Full order details. You can only access orders that belong to your partner client.

Response (200)
{
  "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://..."
  }
}
TypeScript interface
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

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

GET /v1/orders/:order_id/ping

Lightweight order status.

Response (200)
{
  "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
}
TypeScript interface
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

GET /v1/orders/:order_id/driver-location

Requires read_delivery scope.

Response (200)
{
  "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"
}
TypeScript interface
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 listGET /v1/products (price in cents USD). Use for display and cart; do not send to your own payment provider.
  • Checkout sessionPOST /v1/checkout/session returns checkout_url and session_id. Redirect the customer; after payment we fulfill and send webhooks.

By platform

  • Webwindow.location.href = checkout_url or Stripe.js redirectToCheckout({ sessionId }).
  • React Native@stripe/stripe-react-native or open checkout_url in WebView. Use our publishable key and session_id.
  • Ionic / Capacitor — Open checkout_url in in-app or system browser, or Stripe SDK with sessionId + our publishable key.
  • Flutter — Open checkout_url in WebView or url_launcher, or Stripe Flutter SDK with session_id if supported.
Do not create your own Stripe Payment Intents or charge in your Stripe account and then call 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.

Request body
{
  "adjustments": [
    { "product_id": "prod_1", "delta": 10, "reason": "restock", "external_reference": "PO-001" },
    { "product_id": "prod_2", "delta": -2, "reason": "correction" }
  ]
}
Response (200)
{
  "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
    }
  ]
}
TypeScript interfaces
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.

EventWhen
order.acceptedOrder created and inventory deducted (status is processed)
delivery.createdDelivery created; includes uber_delivery_id, uber_tracking_url
delivery.out_for_deliveryDriver is on the way
delivery.deliveredDelivery completed
delivery.cancelledDelivery cancelled
delivery.courier_updateCourier/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.

Webhook payload example
{
  "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://..."
  }
}
TypeScript interface
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.

Example (Node.js)
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

StatusMeaning
400Bad request — missing or invalid body, or missing Idempotency-Key where required. Body: { "ok": false, "error": "message" }.
401Missing or invalid x-api-key.
403Valid key but missing scope for this endpoint.
404Resource not found (order, product, store).
409Conflict — e.g. insufficient stock on POST /v1/orders/report or POST /v1/checkout/session.
429Rate limit exceeded. See below.
500Server error. Include x-trace-id when contacting support.

All JSON error responses include ok: false and error (string).

Error response body (4xx / 5xx)
{
  "ok": false,
  "error": "Insufficient stock for prod_1. requested=5, available=2",
  "trace_id": "TRACE_ABC"
}
TypeScript interface
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_products
  • read_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

tech@vylvet.co

  • x-trace-id from 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.