Wabbus Vendor API

The Wabbus Vendor API gives marketplace vendors programmatic access to their orders, shipments, products, analytics, and webhooks. It is a REST API authenticated with API keys, available at:

https://api.wabbus.com/api/v1

All requests and responses use JSON. Dates are ISO 8601 strings in UTC.


Table of contents


Getting started

  1. Apply for API access. In your vendor dashboard, navigate to Account Settings > API Keys and submit an application. An admin will review it.
  2. Create an API key. Once approved, create a key and select the permissions it needs. Copy the key immediately — it is shown only once.
  3. Make your first request.
curl -H "X-API-Key: your_key_here" \
     https://api.wabbus.com/api/v1/orders?limit=5
  1. Check the response. Every successful response wraps data in a data field. List endpoints also include nextCursor and hasMore for pagination.
  2. Explore interactively. The API also serves an interactive Swagger UI at https://api.wabbus.com/docs where you can browse all endpoints, view request/response schemas, and try requests directly from your browser.

Authentication

All API endpoints (except /health) require an API key passed in the X-API-Key header:

X-API-Key: wabbus_live_abc123...

Keys are scoped to a single vendor account. Every request is automatically filtered to only return data belonging to your store. There is no way to access another vendor's data.

Keys are hashed before storage — Wabbus staff cannot see your key. If you lose it, revoke it and create a new one.

Your API key will stop working if:

  • The key is revoked by you or an admin.
  • Your vendor account is suspended or banned. Keys are frozen (not deleted) and resume working if the suspension is lifted.
  • Your API access approval is revoked by an admin.

Only the store owner can create, view, and revoke API keys. Vendor staff members cannot access API keys regardless of their permissions.


Permissions

Each API key is created with a set of permissions that control which endpoints it can access. Requesting an endpoint without the required permission returns 403 INSUFFICIENT_PERMISSIONS.

PermissionAccess
orders:readView orders and order details
shipments:readView shipment details
shipments:writeCreate shipments, bulk import tracking, update tracking numbers
products:readView products and product details
products:writeUpdate variant inventory
analytics:readView sales analytics, daily breakdowns, and product stats
webhooks:manageCreate, update, delete, and test webhooks

The GET /jobs/:id endpoint requires a valid API key but no specific permission — you can always poll the status of your own import jobs.


Rate limits

Requests are rate-limited per vendor account using a sliding window of 60 seconds.

TierRequests per minute
Standard60
Premium300
Enterprise1,000

Every response includes rate limit headers:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1710950400
  • X-RateLimit-Limit — Your per-minute allowance.
  • X-RateLimit-Remaining — Requests left in the current window.
  • X-RateLimit-Reset — Unix timestamp when the window resets.

When the limit is exceeded, the API returns 429 Too Many Requests with a Retry-After header indicating how many seconds to wait:

{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Rate limit exceeded. Try again in 12 seconds.",
    "status": 429,
    "requestId": "7f9c2a8e-4c1b-4d3a-9f2a-91cbb5d7e123"
  }
}

Pagination

List endpoints use cursor-based pagination. This provides stable results even when new records are created between pages.

Request

ParameterTypeDefaultDescription
cursorstringOpaque cursor from a previous response
limitinteger25Results per page (1–100)

Response

{
  "data": [ ... ],
  "nextCursor": "eyJjcmVhdGVkQXQiOi...",
  "hasMore": true
}
  • nextCursor — Pass this as the cursor query parameter to fetch the next page. null when there are no more results.
  • hasMoretrue if more pages exist.

Example: paginate through all orders

# Page 1
curl -H "X-API-Key: ..." "https://api.wabbus.com/api/v1/orders?limit=50"
# Response includes "nextCursor": "abc123..."

# Page 2
curl -H "X-API-Key: ..." "https://api.wabbus.com/api/v1/orders?limit=50&cursor=abc123..."
# Continue until "hasMore": false

Idempotency

Write operations (POST and PATCH) support idempotency keys to safely retry requests without creating duplicate resources.

How it works

Include an Idempotency-Key header with a unique identifier (e.g., a UUID). If you send the same request with the same key within 24 hours, the API returns the original response without re-executing the operation.

curl -X POST \
  -H "X-API-Key: ..." \
  -H "Idempotency-Key: order-ship-abc123-2026-03-21" \
  -H "Content-Type: application/json" \
  -d '{"orderId":"pub_abc","items":["item_1"],"carrier":"usps","trackingNumber":"9400111899223"}' \
  https://api.wabbus.com/api/v1/shipments

Rules

  • Required for POST requests. The API returns 400 IDEMPOTENCY_KEY_REQUIRED if omitted.
  • Optional for PATCH requests. If provided, it is honored.
  • Format: 1–128 characters, alphanumeric plus -, _, :, .
  • Scope: Keys are scoped per HTTP method and endpoint path. The same key on different endpoints does not collide.
  • TTL: 24 hours. After that, the key can be reused.
  • In-progress requests: If a retry arrives while the original request is still processing, the API returns 409 IDEMPOTENT_REQUEST_IN_PROGRESS. Wait and retry.

Errors

Every error response follows the same structure:

{
  "error": {
    "code": "ORDER_NOT_FOUND",
    "message": "Order pub_xyz not found",
    "status": 404,
    "requestId": "7f9c2a8e-4c1b-4d3a-9f2a-91cbb5d7e123"
  }
}
  • code — A machine-readable error code. Use this for programmatic handling.
  • message — A human-readable explanation.
  • status — The HTTP status code.
  • requestId — The unique request ID (see Request IDs). Include this when contacting support.

Validation errors

When request body validation fails, the response includes a details array:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request body validation failed",
    "status": 400,
    "requestId": "...",
    "details": [
      { "field": "trackingNumber", "issue": "invalid", "message": "trackingNumber must be between 4 and 80 characters" },
      { "field": "carrier", "issue": "invalid", "message": "carrier should not be empty" }
    ]
  }
}

Error codes

StatusCodeDescription
400BAD_REQUESTGeneral bad request
400VALIDATION_ERRORRequest body failed validation
400IDEMPOTENCY_KEY_REQUIREDPOST request missing Idempotency-Key header
400IDEMPOTENCY_KEY_INVALIDKey exceeds 128 chars or contains invalid characters
400BULK_TOO_MANY_ROWSBulk import exceeds 5,000 rows
400TOO_MANY_ACTIVE_JOBSAlready have 2 active import jobs
400ITEMS_NOT_FOUNDSpecified items don't belong to this vendor on this order
401UNAUTHORIZEDMissing, invalid, or frozen API key
403FORBIDDENValid key but vendor account suspended/banned
403INSUFFICIENT_PERMISSIONSKey lacks the required permission
404ORDER_NOT_FOUNDOrder does not exist or doesn't belong to your store
404SHIPMENT_NOT_FOUNDShipment does not exist or doesn't belong to your store
404PRODUCT_NOT_FOUNDProduct does not exist or doesn't belong to your store
404WEBHOOK_NOT_FOUNDWebhook does not exist or doesn't belong to your store
408REQUEST_TIMEOUTRequest took longer than 15 seconds
409CONFLICTGeneral conflict
409IDEMPOTENT_REQUEST_IN_PROGRESSDuplicate idempotency key with request still in flight
429RATE_LIMIT_EXCEEDEDRate limit exceeded
500INTERNAL_ERRORUnexpected server error

Request IDs

Every request is assigned a unique ID returned in the X-Request-Id response header. This ID is also included in every error response body.

If you send an X-Request-Id header with your request (1–64 alphanumeric characters or hyphens), the API will use it. Otherwise, a UUID is generated automatically.

Request IDs are invaluable for debugging. Always log them on your end and include them when contacting support about a failed request.


Versioning

The API is versioned with a URL prefix: /api/v1. The current version is v1.

Within a version, the API follows additive-only changes:

  • New fields may be added to responses.
  • New optional parameters may be added to requests.
  • New endpoints may be added.
  • Existing fields, parameters, and endpoints will not be removed or have their meaning changed.

Your integration should ignore unknown fields in responses to remain forward-compatible. Breaking changes will be introduced under a new version prefix (e.g., /api/v2) with a migration period.


Orders

List orders

GET /api/v1/orders

Permission: orders:read | Success: 200 OK

Returns a paginated list of orders that contain items from your store.

Query parameters:

ParameterTypeDefaultDescription
cursorstringPagination cursor
limitinteger25Results per page (1–100)
statusstringFilter by order status
createdAfterstringISO 8601 date, only orders created after this time
createdBeforestringISO 8601 date, only orders created before this time

Order statuses: PENDING, PAID, PROCESSING, SHIPPED, DELIVERED, COMPLETED, CANCELLED

Response:

{
  "data": [
    {
      "id": "pub_abc123",
      "orderNumber": "WB-10042",
      "status": "PROCESSING",
      "totalAmount": 79.99,
      "currency": "USD",
      "itemCount": 3,
      "createdAt": "2026-03-18T14:30:00.000Z",
      "paidAt": "2026-03-18T14:31:12.000Z",
      "shippedAt": null,
      "deliveredAt": null
    }
  ],
  "nextCursor": "eyJjcmVhdGVkQXQiOi...",
  "hasMore": true
}

Get order

GET /api/v1/orders/:publicId

Permission: orders:read | Success: 200 OK

Returns full order details including the shipping address, your items, and any shipments you've created.

Response:

{
  "data": {
    "id": "pub_abc123",
    "orderNumber": "WB-10042",
    "status": "SHIPPED",
    "totalAmount": 79.99,
    "currency": "USD",
    "createdAt": "2026-03-18T14:30:00.000Z",
    "paidAt": "2026-03-18T14:31:12.000Z",
    "shippedAt": "2026-03-19T09:15:00.000Z",
    "deliveredAt": null,
    "cancelledAt": null,
    "shippingAddress": {
      "fullName": "Jane Doe",
      "line1": "123 Main St",
      "line2": "Apt 4B",
      "city": "Brooklyn",
      "state": "NY",
      "postalCode": "11201",
      "country": "US"
    },
    "items": [
      {
        "id": "item_xyz789",
        "sku": "BLK-TEE-L",
        "productTitle": "Classic Black Tee",
        "quantity": 2,
        "unitPrice": 29.99,
        "subtotal": 59.98,
        "status": "SHIPPED"
      }
    ],
    "shipments": [
      {
        "id": 4521,
        "carrier": "usps",
        "trackingNumber": "9400111899223100001",
        "trackingUrl": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223100001",
        "status": "LABEL_CREATED",
        "shippedAt": "2026-03-19T09:15:00.000Z",
        "deliveredAt": null
      }
    ]
  }
}

Only items and shipments belonging to your store are included. Other vendors' items on the same order are not visible.


Shipments

Create shipment

POST /api/v1/shipments

Permission: shipments:write | Success: 201 Created

Creates a shipment for one or more items on an order.

Request body:

FieldTypeRequiredDescription
orderIdstringyesThe order's public ID
itemsstring[]yesArray of item public IDs to ship
carrierstringyesCarrier name (e.g., usps, ups, fedex, dhl)
trackingNumberstringyesTracking number (4–80 characters)
trackingUrlstringnoCustom tracking URL. Auto-generated for USPS, UPS, FedEx, and DHL if omitted.

Example:

curl -X POST \
  -H "X-API-Key: ..." \
  -H "Idempotency-Key: ship-pub_abc123-2026-03-21" \
  -H "Content-Type: application/json" \
  -d '{
    "orderId": "pub_abc123",
    "items": ["item_xyz789"],
    "carrier": "usps",
    "trackingNumber": "9400111899223100001"
  }' \
  https://api.wabbus.com/api/v1/shipments

Response (201):

{
  "data": {
    "id": 4521,
    "orderId": "pub_abc123",
    "carrier": "usps",
    "trackingNumber": "9400111899223100001",
    "trackingUrl": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223100001",
    "status": "IN_TRANSIT",
    "items": ["item_xyz789"],
    "shippedAt": "2026-03-21T10:00:00.000Z"
  }
}

Notes:

  • Each tracking number can only be used once per vendor. Reusing a tracking number returns a 400 error.
  • Items must be in PROCESSING status. Already-shipped items cannot be re-shipped.
  • Orders within the buyer cancellation window cannot be shipped (standard listings: 45 minutes; custom orders: the seller's policy window).

Bulk import tracking

POST /api/v1/shipments/bulk

Permission: shipments:write | Success: 201 Created

Submits a bulk tracking import as an asynchronous job. This is the recommended approach when you need to ship many orders at once.

Request body:

FieldTypeRequiredDescription
rowsarrayyesArray of shipment rows (1–5,000)
rows[].orderNumberstringyesThe order number (e.g., WB-10042)
rows[].carrierstringyesCarrier name
rows[].trackingNumberstringyesTracking number (4–80 characters)

Example:

curl -X POST \
  -H "X-API-Key: ..." \
  -H "Idempotency-Key: bulk-2026-03-21-batch1" \
  -H "Content-Type: application/json" \
  -d '{
    "rows": [
      { "orderNumber": "WB-10042", "carrier": "usps", "trackingNumber": "9400111899223100001" },
      { "orderNumber": "WB-10043", "carrier": "ups", "trackingNumber": "1Z999AA10123456784" },
      { "orderNumber": "WB-10044", "carrier": "fedex", "trackingNumber": "794644790132" }
    ]
  }' \
  https://api.wabbus.com/api/v1/shipments/bulk

Response (201):

{
  "data": {
    "jobId": "clu8k9a2x0001...",
    "status": "PENDING",
    "totalRows": 3,
    "message": "Bulk import job created. Poll GET /api/v1/jobs/:jobId for status."
  }
}

The job is processed in the background. Poll GET /api/v1/jobs/:jobId to track progress. For each row, the system finds the order, identifies your unshipped items, and creates a shipment. Rows that fail (order not found, items already shipped, duplicate tracking, etc.) are recorded individually in the job results without affecting other rows.

You can have up to 2 active import jobs at a time. The maximum request body size is 5 MB.

Get shipment

GET /api/v1/shipments/:id

Permission: shipments:read | Success: 200 OK

Response:

{
  "data": {
    "id": 4521,
    "orderId": "pub_abc123",
    "orderNumber": "WB-10042",
    "carrier": "usps",
    "trackingNumber": "9400111899223100001",
    "trackingUrl": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223100001",
    "status": "LABEL_CREATED",
    "direction": "OUTBOUND",
    "purpose": "ORIGINAL",
    "items": ["item_xyz789"],
    "shippedAt": "2026-03-21T10:00:00.000Z",
    "deliveredAt": null,
    "createdAt": "2026-03-21T10:00:00.000Z"
  }
}

Update tracking

PATCH /api/v1/shipments/:id/tracking

Permission: shipments:write | Success: 200 OK

Updates the tracking number (and optionally the carrier) on an existing shipment.

Request body:

FieldTypeRequiredDescription
trackingNumberstringyesNew tracking number (4–80 characters)
carrierstringnoNew carrier name. Keeps the existing carrier if omitted.

Response:

{
  "data": {
    "id": 4521,
    "carrier": "usps",
    "trackingNumber": "9400111899223100002",
    "trackingUrl": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223100002",
    "status": "LABEL_CREATED"
  }
}

Jobs

Get job status

GET /api/v1/jobs/:id

Permission: None (any valid API key) | Success: 200 OK

Poll this endpoint to track the progress of a bulk import job.

Response:

{
  "data": {
    "id": "clu8k9a2x0001...",
    "type": "TRACKING_CSV",
    "status": "COMPLETED",
    "totalRows": 3,
    "processedRows": 3,
    "successCount": 2,
    "failedCount": 1,
    "results": [
      { "row": 1, "orderNumber": "WB-10042", "trackingNumber": "9400111899223100001", "carrier": "usps", "status": "success" },
      { "row": 2, "orderNumber": "WB-10043", "trackingNumber": "1Z999AA10123456784", "carrier": "ups", "status": "success" },
      { "row": 3, "orderNumber": "WB-99999", "trackingNumber": "794644790132", "carrier": "fedex", "status": "failed", "error": "Order \"WB-99999\" not found." }
    ],
    "createdAt": "2026-03-21T10:00:00.000Z",
    "startedAt": "2026-03-21T10:00:01.000Z",
    "completedAt": "2026-03-21T10:00:05.000Z"
  }
}

Job statuses: PENDING, PROCESSING, COMPLETED, FAILED

While the job is PROCESSING, processedRows updates periodically so you can show progress. The results array is only populated when the job reaches COMPLETED.


Products

List products

GET /api/v1/products

Permission: products:read | Success: 200 OK

Returns a paginated list of your products.

Query parameters:

ParameterTypeDefaultDescription
cursorstringPagination cursor
limitinteger25Results per page (1–100)
statusstringFilter by review status
isActivebooleanFilter by active/inactive
searchstringSearch by title (case-insensitive, max 200 characters)

Product statuses: DRAFT, SUBMITTED, APPROVED, REJECTED, NEEDS_REVIEW

Response:

{
  "data": [
    {
      "id": "prod_abc123",
      "title": "Classic Black Tee",
      "status": "APPROVED",
      "isActive": true,
      "primaryImageUrl": "https://cdn.wabbus.com/images/abc123.jpg",
      "variantCount": 4,
      "createdAt": "2026-02-10T08:00:00.000Z",
      "updatedAt": "2026-03-15T12:30:00.000Z"
    }
  ],
  "nextCursor": "eyJjcmVhdGVkQXQiOi...",
  "hasMore": true
}

Get product

GET /api/v1/products/by-product-id/:productId

Permission: products:read | Success: 200 OK

Returns full product details including variants with inventory, images, and options.

Response:

{
  "data": {
    "id": "prod_abc123",
    "title": "Classic Black Tee",
    "description": "A comfortable everyday tee made from 100% organic cotton.",
    "brand": "Wabbus Essentials",
    "status": "APPROVED",
    "isActive": true,
    "condition": "new",
    "material": "100% Organic Cotton",
    "countryOfOrigin": "US",
    "upcGtin": "012345678901",
    "careInstructions": "Machine wash cold, tumble dry low",
    "keyFeatures": ["Organic cotton", "Pre-shrunk", "Relaxed fit"],
    "slug": "classic-black-tee",
    "category": "Clothing > T-Shirts",
    "variants": [
      {
        "id": "var_def456",
        "sku": "BLK-TEE-L",
        "title": "Large / Black",
        "price": 29.99,
        "compareAtPrice": 39.99,
        "currency": "USD",
        "inventory": {
          "quantity": 150,
          "reserved": 3,
          "available": 147
        },
        "options": [
          { "name": "Size", "value": "Large" },
          { "name": "Color", "value": "Black" }
        ]
      }
    ],
    "images": [
      {
        "id": 1,
        "url": "https://cdn.wabbus.com/images/abc123.jpg",
        "sortOrder": 0,
        "width": 1200,
        "height": 1600
      }
    ],
    "options": [
      { "name": "Size", "values": ["Small", "Medium", "Large", "XL"] },
      { "name": "Color", "values": ["Black", "White", "Navy"] }
    ],
    "createdAt": "2026-02-10T08:00:00.000Z",
    "updatedAt": "2026-03-15T12:30:00.000Z"
  }
}

Update variant inventory

PATCH /api/v1/products/:productId/variants/:variantId/inventory

Permission: products:write | Success: 200 OK

Sets the absolute inventory quantity for a variant. This replaces the current value — it is not an increment.

Request body:

FieldTypeRequiredDescription
quantityintegeryesNew inventory quantity (>= 0)

Example:

curl -X PATCH \
  -H "X-API-Key: ..." \
  -H "Content-Type: application/json" \
  -d '{"quantity": 200}' \
  https://api.wabbus.com/api/v1/products/prod_abc123/variants/var_def456/inventory

Response:

{
  "data": {
    "variantId": "var_def456",
    "productId": "prod_abc123",
    "quantity": 200
  }
}

Analytics

Overview

GET /api/v1/analytics/overview

Permission: analytics:read | Success: 200 OK

Returns aggregate metrics for your store over a date range.

Query parameters:

ParameterTypeRequiredDescription
startDatestringyesStart date (YYYY-MM-DD)
endDatestringyesEnd date (YYYY-MM-DD)

The date range cannot exceed 90 days.

Response:

{
  "data": {
    "startDate": "2026-03-01",
    "endDate": "2026-03-21",
    "grossRevenue": 12450.00,
    "netRevenue": 11205.00,
    "orderCount": 187,
    "unitsSold": 342,
    "refundCount": 8,
    "refundAmount": 1245.00,
    "returnCount": 5,
    "shipmentsCreated": 195,
    "shipmentsDelivered": 162
  }
}

Daily breakdown

GET /api/v1/analytics/daily

Permission: analytics:read | Success: 200 OK

Returns per-day metrics, cursor-paginated.

Query parameters:

ParameterTypeRequiredDescription
startDatestringyesStart date (YYYY-MM-DD)
endDatestringyesEnd date (YYYY-MM-DD)
cursorstringnoPagination cursor
limitintegernoResults per page (1–100, default 25)

Response:

{
  "data": [
    {
      "date": "2026-03-21",
      "grossRevenue": 650.00,
      "netRevenue": 585.00,
      "orderCount": 12,
      "unitsSold": 18,
      "refundCount": 1,
      "refundAmount": 65.00,
      "returnCount": 0,
      "shipmentsCreated": 13,
      "shipmentsDelivered": 10
    }
  ],
  "nextCursor": "eyJkYXRlIjoiMjAy...",
  "hasMore": true
}

Product stats

GET /api/v1/analytics/products

Permission: analytics:read | Success: 200 OK

Returns per-product metrics, cursor-paginated.

Query parameters:

ParameterTypeRequiredDescription
startDatestringyesStart date (YYYY-MM-DD)
endDatestringyesEnd date (YYYY-MM-DD)
cursorstringnoPagination cursor
limitintegernoResults per page (1–100, default 25)

Response:

{
  "data": [
    {
      "productId": "prod_abc123",
      "title": "Classic Black Tee",
      "unitsSold": 84,
      "revenue": 2519.16,
      "viewCount": 1240,
      "refundCount": 2,
      "refundAmount": 59.98,
      "returnCount": 1
    }
  ],
  "nextCursor": "eyJwaWQiOiJwcm9k...",
  "hasMore": true
}

Top products

GET /api/v1/analytics/products/top

Permission: analytics:read | Success: 200 OK

Returns your top-performing products ranked by units sold. Not paginated.

Query parameters:

ParameterTypeRequiredDescription
startDatestringyesStart date (YYYY-MM-DD)
endDatestringyesEnd date (YYYY-MM-DD)
limitintegernoNumber of products to return (1–50, default 10)

Response:

{
  "data": [
    {
      "rank": 1,
      "productId": "prod_abc123",
      "title": "Classic Black Tee",
      "unitsSold": 84,
      "revenue": 2519.16,
      "viewCount": 1240
    }
  ]
}

Webhooks

Webhooks let you receive real-time notifications when events happen on your orders. Instead of polling the API, you register a URL and the platform sends a POST request to it whenever a subscribed event occurs.

Event types

EventDescription
order.paidA customer completed checkout and payment was captured
order.cancelledAn order was cancelled
shipment.createdA shipment was created for one of your orders
shipment.deliveredA shipment was marked as delivered
refund.processedA refund was issued on one of your orders
*Subscribe to all events (current and future)

Create webhook

POST /api/v1/webhooks

Permission: webhooks:manage | Success: 201 Created

Request body:

FieldTypeRequiredDescription
urlstringyesHTTPS endpoint URL (max 500 characters)
eventsstring[]yesEvent types to subscribe to (1–6 items)
descriptionstringnoHuman-readable label (max 200 characters)

Example:

curl -X POST \
  -H "X-API-Key: ..." \
  -H "Idempotency-Key: webhook-create-prod" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourserver.com/webhooks/wabbus",
    "events": ["order.paid", "shipment.delivered"],
    "description": "Production order notifications"
  }' \
  https://api.wabbus.com/api/v1/webhooks

Response (201):

{
  "data": {
    "id": "wh_abc123",
    "url": "https://yourserver.com/webhooks/wabbus",
    "events": ["order.paid", "shipment.delivered"],
    "description": "Production order notifications",
    "isActive": true,
    "createdAt": "2026-03-21T10:00:00.000Z",
    "secret": "a1b2c3d4e5f6...64 character hex string"
  }
}

The secret is only returned on creation and when rotated. Store it securely — you need it to verify webhook signatures.

List webhooks

GET /api/v1/webhooks

Permission: webhooks:manage | Success: 200 OK

Response:

{
  "data": [
    {
      "id": "wh_abc123",
      "url": "https://yourserver.com/webhooks/wabbus",
      "events": ["order.paid", "shipment.delivered"],
      "description": "Production order notifications",
      "isActive": true,
      "failureCount": 0,
      "disabledAt": null,
      "totalDeliveries30d": 42,
      "createdAt": "2026-03-21T10:00:00.000Z",
      "updatedAt": "2026-03-21T10:00:00.000Z"
    }
  ]
}

A webhook is automatically disabled after repeated delivery failures. The failureCount and disabledAt fields indicate the health of each endpoint.

Get webhook

GET /api/v1/webhooks/:id

Permission: webhooks:manage | Success: 200 OK

Returns the same fields as the list response for a single webhook.

Update webhook

PATCH /api/v1/webhooks/:id

Permission: webhooks:manage | Success: 200 OK

Request body (all fields optional):

FieldTypeDescription
urlstringNew HTTPS endpoint URL
eventsstring[]New event subscriptions
descriptionstringNew description
isActivebooleanEnable or disable the webhook

Delete webhook

DELETE /api/v1/webhooks/:id

Permission: webhooks:manage | Success: 204 No Content

List deliveries

GET /api/v1/webhooks/:id/deliveries

Permission: webhooks:manage | Success: 200 OK

Returns recent delivery attempts for a webhook, cursor-paginated. Deliveries are retained for 30 days.

Query parameters:

ParameterTypeDefaultDescription
cursorstringPagination cursor
limitinteger25Results per page (1–50)

Response:

{
  "data": [
    {
      "id": "del_xyz789",
      "eventType": "order.paid",
      "eventId": "evt_abc123",
      "statusCode": 200,
      "latencyMs": 340,
      "attempt": 1,
      "success": true,
      "createdAt": "2026-03-21T10:05:00.000Z"
    }
  ],
  "nextCursor": "...",
  "hasMore": false
}

Test webhook

POST /api/v1/webhooks/:id/test

Permission: webhooks:manage | Success: 200 OK

Sends a test delivery to your webhook URL with a sample event payload. Use this to verify your endpoint is reachable and correctly verifying signatures.

Rotate secret

POST /api/v1/webhooks/:id/rotate-secret

Permission: webhooks:manage | Success: 200 OK

Generates a new signing secret for the webhook. The old secret is invalidated immediately. The new secret is returned in the response — store it securely.

Response:

{
  "data": {
    "id": "wh_abc123",
    "secret": "new64charhexstring..."
  }
}

Webhook payloads

Every webhook delivery sends a POST request with a JSON body in this structure:

{
  "event": "order.paid",
  "timestamp": "2026-03-21T10:05:00.000Z",
  "data": { ... }
}
  • event — The event type string.
  • timestamp — When the event was dispatched (ISO 8601).
  • data — Event-specific payload (see below).

order.paid

Fired when a customer completes checkout and payment is captured. You receive only the items from your store, not the full order.

{
  "event": "order.paid",
  "timestamp": "2026-03-21T10:05:00.000Z",
  "data": {
    "orderId": "pub_abc123",
    "orderNumber": "WB-10042",
    "vendorTotal": 59.98,
    "currency": "USD",
    "paidAt": "2026-03-21T10:04:58.000Z",
    "items": [
      {
        "id": "item_xyz789",
        "sku": "BLK-TEE-L",
        "productTitle": "Classic Black Tee",
        "quantity": 2,
        "unitPrice": 29.99,
        "subtotal": 59.98
      }
    ]
  }
}
FieldTypeDescription
orderIdstringOrder public ID
orderNumberstringHuman-readable order number
vendorTotalnumberTotal for your items only (not the full order total)
currencystringISO 4217 currency code
paidAtstringWhen payment was captured
itemsarrayYour items on this order
items[].idstringItem public ID
items[].skustring or nullVariant SKU
items[].productTitlestring or nullProduct title
items[].quantityintegerQuantity ordered
items[].unitPricenumberPrice per unit
items[].subtotalnumberLine total

order.cancelled

Fired when an order is cancelled.

{
  "event": "order.cancelled",
  "timestamp": "2026-03-21T11:00:00.000Z",
  "data": {
    "orderId": "pub_abc123",
    "orderNumber": "WB-10042",
    "cancelledAt": "2026-03-21T10:59:30.000Z",
    "reason": "CUSTOMER_REQUEST",
    "note": "Changed my mind"
  }
}
FieldTypeDescription
orderIdstringOrder public ID
orderNumberstringHuman-readable order number
cancelledAtstringWhen the order was cancelled
reasonstring or nullCancellation reason code
notestring or nullOptional cancellation note

shipment.created

Fired when a shipment is created for one of your orders (whether via the API, bulk import, or vendor dashboard).

{
  "event": "shipment.created",
  "timestamp": "2026-03-21T12:00:00.000Z",
  "data": {
    "shipmentId": 4521,
    "orderId": "pub_abc123",
    "orderNumber": "WB-10042",
    "carrier": "usps",
    "trackingNumber": "9400111899223100001",
    "trackingUrl": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223100001",
    "status": "LABEL_CREATED",
    "shippedAt": "2026-03-21T12:00:00.000Z",
    "items": ["item_xyz789"]
  }
}
FieldTypeDescription
shipmentIdintegerShipment ID
orderIdstringOrder public ID
orderNumberstringHuman-readable order number
carrierstringCarrier name
trackingNumberstringTracking number
trackingUrlstring or nullTracking URL
statusstringShipment status
shippedAtstring or nullWhen the shipment was created
itemsstring[]Array of item public IDs included in this shipment

shipment.delivered

Fired when a shipment is confirmed as delivered (typically via carrier tracking webhook).

{
  "event": "shipment.delivered",
  "timestamp": "2026-03-24T15:30:00.000Z",
  "data": {
    "shipmentId": 4521,
    "orderId": "pub_abc123",
    "orderNumber": "WB-10042",
    "carrier": "usps",
    "trackingNumber": "9400111899223100001",
    "trackingUrl": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223100001",
    "status": "DELIVERED",
    "deliveredAt": "2026-03-24T15:28:00.000Z",
    "items": ["item_xyz789"]
  }
}

The fields are the same as shipment.created, with deliveredAt replacing shippedAt.

refund.processed

Fired when a refund is issued on one of your orders.

{
  "event": "refund.processed",
  "timestamp": "2026-03-25T09:00:00.000Z",
  "data": {
    "orderId": "pub_abc123",
    "orderNumber": "WB-10042",
    "refundAmountCents": 2999,
    "refundAmount": 29.99,
    "currency": "USD",
    "isPartial": true,
    "executedAt": "2026-03-25T08:59:45.000Z"
  }
}
FieldTypeDescription
orderIdstringOrder public ID
orderNumberstringHuman-readable order number
refundAmountCentsintegerRefund amount in cents
refundAmountnumberRefund amount in dollars
currencystringISO 4217 currency code
isPartialbooleanWhether this is a partial refund
executedAtstringWhen the refund was processed

Webhook delivery and retries

Delivery behavior

When an event occurs, the platform sends a POST request to each matching webhook endpoint with a 10-second timeout. Your endpoint must respond with a 2xx status code to indicate success. Any other status code (or a timeout) is treated as a failure.

Retry schedule

Failed deliveries are retried up to 5 times with increasing delays:

AttemptDelay after failure
1Immediate
21 minute
35 minutes
430 minutes
52 hours

After 5 failed attempts, the delivery is marked as permanently failed and no further retries are attempted for that specific event.

Automatic disabling

If a webhook accumulates 10 consecutive failures (across any deliveries, not just retries of a single event), it is automatically disabled. When this happens:

  • The webhook's isActive field becomes false.
  • The disabledAt timestamp is set.
  • No further events are dispatched to that endpoint.

You can re-enable a disabled webhook via PATCH /api/v1/webhooks/:id with {"isActive": true} after fixing your endpoint. The failure counter resets on the next successful delivery.

Redirects

HTTP redirects (3xx responses) are not followed. They are treated as permanent failures and are not retried. If your endpoint has moved, update the webhook URL directly.

Best practices

  • Respond quickly. Return 200 as soon as you receive the event, then process it asynchronously. If your handler takes longer than 10 seconds, the delivery will time out and be retried.
  • Handle duplicates. Due to retries, your endpoint may receive the same event more than once. Use the X-Wabbus-Delivery-Id header to deduplicate.
  • Monitor delivery health. Use GET /api/v1/webhooks/:id/deliveries to check for failures. If your webhook gets auto-disabled, you'll stop receiving events until you re-enable it.

Webhook verification

Every webhook delivery is signed so you can verify it came from Wabbus and was not tampered with.

Signature headers

Each delivery includes these headers:

HeaderDescription
X-Wabbus-EventThe event type (e.g., order.paid)
X-Wabbus-Delivery-IdUnique delivery ID
X-Wabbus-TimestampUnix timestamp (seconds) when the delivery was sent
X-Wabbus-SignatureHMAC-SHA256 signature in the format sha256={hex}

Verification steps

  1. Extract the timestamp and signature from the headers.
  2. Build the signed payload: concatenate the timestamp, a period, and the raw request body: {timestamp}.{body}
  3. Compute the expected signature: HMAC-SHA256 of the signed payload using your webhook secret.
  4. Compare signatures using a constant-time comparison to prevent timing attacks.
  5. Reject stale deliveries (optional but recommended): if the timestamp is more than 5 minutes old, reject it to prevent replay attacks.

Example (Node.js)

const crypto = require('crypto');

function verifyWebhook(secret, timestamp, body, signature) {
  const signedPayload = `${timestamp}.${body}`;
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  // Constant-time comparison
  if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
    throw new Error('Invalid webhook signature');
  }

  // Optional: reject stale deliveries
  const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
  if (age > 300) {
    throw new Error('Webhook delivery is too old');
  }

  return true;
}

// In your Express handler:
app.post('/webhooks/wabbus', express.raw({ type: 'application/json' }), (req, res) => {
  try {
    verifyWebhook(
      process.env.WABBUS_WEBHOOK_SECRET,
      req.headers['x-wabbus-timestamp'],
      req.body.toString(),
      req.headers['x-wabbus-signature'],
    );

    const event = JSON.parse(req.body);
    // Handle the event...

    res.sendStatus(200);
  } catch (err) {
    res.sendStatus(400);
  }
});

Example (Python)

import hmac
import hashlib
import time

def verify_webhook(secret: str, timestamp: str, body: str, signature: str) -> bool:
    signed_payload = f"{timestamp}.{body}"
    expected = "sha256=" + hmac.new(
        secret.encode(), signed_payload.encode(), hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(expected, signature):
        raise ValueError("Invalid webhook signature")

    age = int(time.time()) - int(timestamp)
    if age > 300:
        raise ValueError("Webhook delivery is too old")

    return True

Limits

ResourceLimit
API keys per vendor5
Webhooks per vendor5
Webhook event subscriptions per webhook6
Bulk import rows per request5,000
Concurrent import jobs2
Request body size5 MB
Analytics date range90 days
Webhook delivery retention30 days
Idempotency key TTL24 hours
API timeout15 seconds
Tracking number length4–80 characters
Webhook URL length500 characters
Product search query200 characters

Health check

GET /health

No authentication required. Returns the health status of the API service.

{
  "status": "ok",
  "checks": {
    "db": "ok",
    "redis": "ok"
  }
}

This endpoint is intended for load balancers and uptime monitors, not for vendor integrations. It is not versioned and lives outside the /api/v1 prefix.


Integration recipes

Common patterns for building on the Wabbus API.

Sync new orders to your warehouse

Poll for new orders periodically and forward them to your fulfillment system.

async function syncOrders(apiKey, lastSyncDate) {
  let cursor = null;
  let hasMore = true;

  while (hasMore) {
    const url = new URL('https://api.wabbus.com/api/v1/orders');
    url.searchParams.set('status', 'PROCESSING');
    url.searchParams.set('createdAfter', lastSyncDate);
    url.searchParams.set('limit', '100');
    if (cursor) url.searchParams.set('cursor', cursor);

    const res = await fetch(url, {
      headers: { 'X-API-Key': apiKey },
    });
    const json = await res.json();

    for (const order of json.data) {
      await sendToWarehouse(order);
    }

    cursor = json.nextCursor;
    hasMore = json.hasMore;
  }
}

Automate shipping from your fulfillment system

When your warehouse ships a package, push the tracking number to Wabbus so customers get notified automatically.

async function reportShipment(apiKey, orderId, items, carrier, trackingNumber) {
  const res = await fetch('https://api.wabbus.com/api/v1/shipments', {
    method: 'POST',
    headers: {
      'X-API-Key': apiKey,
      'Content-Type': 'application/json',
      'Idempotency-Key': `ship-${orderId}-${trackingNumber}`,
    },
    body: JSON.stringify({ orderId, items, carrier, trackingNumber }),
  });

  if (!res.ok) {
    const error = await res.json();
    console.error('Failed to create shipment:', error.error.code, error.error.message);
    return null;
  }

  return await res.json();
}

Keep inventory in sync

If you manage inventory in an external system, push updates to Wabbus whenever stock changes.

async function updateStock(apiKey, productId, variantId, quantity) {
  const res = await fetch(
    `https://api.wabbus.com/api/v1/products/${productId}/variants/${variantId}/inventory`,
    {
      method: 'PATCH',
      headers: {
        'X-API-Key': apiKey,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ quantity }),
    },
  );

  if (!res.ok) {
    const error = await res.json();
    throw new Error(`Inventory update failed: ${error.error.message}`);
  }
}

React to events with webhooks

Instead of polling, use webhooks to trigger actions in real time. This example processes incoming webhook deliveries in an Express server.

const express = require('express');
const crypto = require('crypto');
const app = express();

app.post('/webhooks/wabbus', express.raw({ type: 'application/json' }), (req, res) => {
  const secret = process.env.WABBUS_WEBHOOK_SECRET;
  const timestamp = req.headers['x-wabbus-timestamp'];
  const signature = req.headers['x-wabbus-signature'];
  const deliveryId = req.headers['x-wabbus-delivery-id'];

  // Verify signature
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${req.body.toString()}`)
    .digest('hex');

  if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
    return res.sendStatus(400);
  }

  // Respond immediately, process async
  res.sendStatus(200);

  const event = JSON.parse(req.body);
  switch (event.event) {
    case 'order.paid':
      console.log(`New order ${event.data.orderNumber} — $${event.data.vendorTotal}`);
      // Forward to fulfillment system...
      break;
    case 'shipment.delivered':
      console.log(`Shipment ${event.data.shipmentId} delivered for ${event.data.orderNumber}`);
      // Update internal records...
      break;
    case 'refund.processed':
      console.log(`Refund of $${event.data.refundAmount} on ${event.data.orderNumber}`);
      // Adjust accounting...
      break;
  }
});

app.listen(3000);

Changelog

All notable changes to the Wabbus Vendor API will be documented here.

v1 — March 2026

  • Initial release of the Wabbus Vendor API.
  • 22 endpoints across Orders, Shipments, Jobs, Products, Analytics, and Webhooks.
  • API key authentication with 7 granular permissions.
  • Cursor-based pagination, idempotency keys, structured error envelope.
  • 3 rate limit tiers: Standard (60/min), Premium (300/min), Enterprise (1,000/min).
  • Bulk tracking import supporting up to 5,000 rows per request.
  • Webhook delivery with HMAC-SHA256 signing and automatic retry.