Integration Guide

This guide covers the public API for integrating external systems with Vectis OMS.

Support: support@stoalogistics.com
Terms of Service: stoalogistics.com/terms


Authentication

All API requests require a Bearer token in the Authorization header.

Obtaining a Token

Request an access token from the OAuth endpoint:

curl -X POST https://api.vectisoms.app/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET"

Response:

{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "orders:read orders:write inventory:read inventory:write products:read products:write"
}

Making Requests

curl https://acme.vectisoms.app/api/orders \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Organization owners create API Clients in the Vectis account portal (the Control Plane link in the app sidebar), under Organization Settings → Developer → API Clients. (The same tab also lists API Keys; for the Vectis API, use an API Client.) The secret is shown once. Contact support@stoalogistics.com if you need assistance.

API client tokens currently have read access to orders, products, and inventory, and can manage webhook subscriptions. To create or change orders, products, or inventory through the API, contact support@stoalogistics.com.


Ecommerce Integration

Push orders from your ecommerce platform to Vectis for fulfillment.

Check Inventory Before Selling

GET /api/inventory?sku=WIDGET-001
Authorization: Bearer YOUR_ACCESS_TOKEN

Response:

{
  "items": [{
    "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "sku": "WIDGET-001",
    "quantityAvailable": 150,
    "quantityReserved": 10,
    "warehouseId": "3f2b8c1e-9d4a-4b6e-8f1a-2c5d7e9b0a14"
  }]
}

Create an Order

POST /api/orders
Content-Type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN

{
  "externalId": "SHOPIFY-12345",
  "customerId": "cust_jane123",
  "customerEmail": "jane@example.com",
  "items": [
    { "sku": "WIDGET-001", "quantity": 2 }
  ],
  "shippingAddresses": [{
    "name": "Jane Smith",
    "line1": "123 Main St",
    "city": "Austin",
    "region": "TX",
    "postalCode": "78701",
    "country": "US"
  }],
  "billingAddress": {
    "name": "Jane Smith",
    "line1": "123 Main St",
    "city": "Austin",
    "region": "TX",
    "postalCode": "78701",
    "country": "US"
  }
}

Line prices come from your product catalog.

Response:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "pending",
  "total": 59.98,
  "itemCount": 2,
  "createdAt": "2026-01-26T12:00:00Z"
}

Get Order Status

GET /api/orders/{orderId}
Authorization: Bearer YOUR_ACCESS_TOKEN

Response:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "shipped",
  "total": 59.98,
  "trackingNumber": "1Z999AA10123456784",
  "carrier": "UPS",
  "items": [...],
  "createdAt": "2026-01-26T12:00:00Z"
}

Cancel an Order

Cancel an order before it ships:

POST /api/orders/{orderId}/cancel
Content-Type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN

{
  "reason": "Customer requested cancellation"
}

Bulk order import

When you need to load many orders at once (cutover, replatforming, or steady high volume), use POST /api/orders/bulk-import with a JSON body: {"format": "json", "data": [orders]} or {"format": "csv", "data": "<csv text>"}, up to 500 orders per request. Same Bearer token as the rest of the API.

Use POST /api/orders when you create one order at a time and want safe retries with an Idempotency-Key header. Use bulk import when the work is batch-shaped and you control file or payload layout.

CSV needs a header row with customerId, customerEmail, sku, quantity, shipName, shipLine1, shipCity, shipRegion, shipPostalCode, shipCountry, and optional externalId, billLine1, billCity, billRegion, billPostalCode, and billCountry columns (each billing field defaults to its shipping value; the billing name is always shipName); each row is one order with one line item. Every row is checked for required fields and formats before any order is created. A row can still fail when its order is created (for example, an unknown SKU) while the other rows succeed. The response lists created, failed, and per-row errors. Questions: support@stoalogistics.com.


Product Catalog Sync

Keep your product catalog in sync with Vectis.

Create a Product

POST /api/products
Content-Type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN

{
  "sku": "WIDGET-001",
  "name": "Premium Widget",
  "price": 29.99,
  "weight": 1.5,
  "weightUnit": "lb",
  "dimensions": {
    "length": 10, "width": 8, "height": 4, "unit": "in"
  }
}

Get Product by SKU

GET /api/products/sku/WIDGET-001
Authorization: Bearer YOUR_ACCESS_TOKEN

Update a Product

PATCH /api/products/{productId}
Content-Type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN

{
  "name": "Premium Widget v2",
  "weight": 1.2,
  "weightUnit": "lb"
}

Inventory Adjustments

Adjust inventory levels when stock changes outside of Vectis.

PATCH /api/inventory/{id}/adjust
Content-Type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN

{
  "adjustment": -5,
  "reason": "Damaged goods removed"
}

Webhook Subscriptions

Subscribe to real-time events for order and inventory changes.

Create a Webhook Subscription

POST /api/webhooks
Content-Type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN

{
  "url": "https://your-system.com/webhooks/vectis",
  "events": ["OrderShipped", "OrderDelivered", "InventoryAdjusted"],
  "secret": "your-webhook-secret"
}

Available Events

EventDescription
OrderCreatedNew order received
OrderConfirmedOrder confirmed for fulfillment
OrderShippedOrder has shipped (includes tracking)
OrderDeliveredOrder delivered to customer
OrderCancelledOrder was cancelled
InventoryAdjustedInventory level changed
PackageCreatedPackage created for order
PackageShippedPackage shipped with tracking
PackageDeliveredPackage delivered

Use * to receive every event. See the Webhooks Guide for the full list.

Webhook Payload Example

{
  "event": "OrderShipped",
  "timestamp": "2026-01-27T14:30:00Z",
  "data": {
    "orderId": "550e8400-e29b-41d4-a716-446655440000",
    "trackingNumber": "1Z999AA10123456784",
    "carrier": "UPS"
  }
}

data also carries organizationId, eventId, and other fields. Ignore fields you don’t use.

Verify each delivery with the HMAC-SHA256 signature in the X-Vectis-Signature header.


Pagination

The order and product lists return paginated results:

GET /api/orders?limit=50&offset=100
ParameterTypeDefaultMaxDescription
limitinteger20100Items per page
offsetinteger0-Items to skip

Response includes pagination metadata. The pagination object always includes total, limit, and offset.

{
  "orders": [...],
  "pagination": {
    "limit": 50,
    "offset": 100,
    "total": 523
  }
}

Idempotency

To retry order creation safely, send an Idempotency-Key header with a value unique to the order. A repeat request with the same key within 24 hours returns the original order (status 201) instead of creating a new one. externalId is stored for your reference but is not checked for duplicates.

# First request (creates order)
POST /api/orders
Idempotency-Key: SHOP-12345
{ "externalId": "SHOP-12345", ... }
# Response: 201 Created

# Retry with the same key (returns the original order)
POST /api/orders
Idempotency-Key: SHOP-12345
{ "externalId": "SHOP-12345", ... }
# Response: 201 Created (same order returned)

Webhook Signature Verification

Outbound webhooks include an HMAC-SHA256 signature for verification.

Header: X-Vectis-Signature: t=<timestamp>,v1=<signature> (Unix seconds and a hex HMAC-SHA256). Deliveries also carry X-Vectis-Event and X-Vectis-Delivery-Id.

Verification (Node.js):

const crypto = require('crypto');

function verifyWebhook(rawBody, header, secret) {
  // Parse t (timestamp) and v1 (signature) from X-Vectis-Signature
  const { t, v1 } = Object.fromEntries(header.split(',').map(p => p.split('=')));

  // Reject old timestamps (replay protection)
  const age = Date.now() / 1000 - parseInt(t);
  if (age > 300) return false; // 5 minute window

  // Compute expected signature
  const signedPayload = `${t}.${rawBody}`;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  // Timing-safe comparison (lengths must match first, or timingSafeEqual throws)
  const received = Buffer.from(v1 || '');
  const computed = Buffer.from(expected);
  return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}

Rate Limits

Rate limits are not published for the tenant API. Some features, such as AI demand forecasting, have their own limits. A 429 whose body has "error": "LIMIT_EXCEEDED" means a plan limit (for example warehouses, carrier accounts or sales channels); retrying won’t clear it. Any other 429 is temporary: wait and retry with backoff.


Error Handling

Errors return a non-2xx status and a JSON body with an error field. Depending on the endpoint, error is a message string, an object with code and message, or, for request validation failures (HTTP 400), an object with an issues list. Branch on the HTTP status code.

Status CodeMeaning
400Bad request / validation error
401Invalid or missing access token
403Authenticated but not permitted
404Resource not found
409Conflict (e.g., order already cancelled)
429Too many requests, or a plan limit reached (see Rate Limits)
500Internal server error

API Reference

Full OpenAPI specification available at the API Reference page.

Need Help?