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
- Authentication
- Permissions
- Rate limits
- Pagination
- Idempotency
- Errors
- Request IDs
- Versioning
- Orders
- Shipments
- Jobs
- Products
- Analytics
- Webhooks
- Webhook payloads
- Webhook delivery and retries
- Webhook verification
- Limits
- Integration recipes
- Changelog
Getting started
- Apply for API access. In your vendor dashboard, navigate to Account Settings > API Keys and submit an application. An admin will review it.
- Create an API key. Once approved, create a key and select the permissions it needs. Copy the key immediately — it is shown only once.
- Make your first request.
curl -H "X-API-Key: your_key_here" \
https://api.wabbus.com/api/v1/orders?limit=5
- Check the response. Every successful response wraps data in a
datafield. List endpoints also includenextCursorandhasMorefor pagination. - Explore interactively. The API also serves an interactive Swagger UI at
https://api.wabbus.com/docswhere 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.
| Permission | Access |
|---|---|
orders:read | View orders and order details |
shipments:read | View shipment details |
shipments:write | Create shipments, bulk import tracking, update tracking numbers |
products:read | View products and product details |
products:write | Update variant inventory |
analytics:read | View sales analytics, daily breakdowns, and product stats |
webhooks:manage | Create, 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.
| Tier | Requests per minute |
|---|---|
| Standard | 60 |
| Premium | 300 |
| Enterprise | 1,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
| Parameter | Type | Default | Description |
|---|---|---|---|
cursor | string | — | Opaque cursor from a previous response |
limit | integer | 25 | Results per page (1–100) |
Response
{
"data": [ ... ],
"nextCursor": "eyJjcmVhdGVkQXQiOi...",
"hasMore": true
}
nextCursor— Pass this as thecursorquery parameter to fetch the next page.nullwhen there are no more results.hasMore—trueif 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_REQUIREDif 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
| Status | Code | Description |
|---|---|---|
| 400 | BAD_REQUEST | General bad request |
| 400 | VALIDATION_ERROR | Request body failed validation |
| 400 | IDEMPOTENCY_KEY_REQUIRED | POST request missing Idempotency-Key header |
| 400 | IDEMPOTENCY_KEY_INVALID | Key exceeds 128 chars or contains invalid characters |
| 400 | BULK_TOO_MANY_ROWS | Bulk import exceeds 5,000 rows |
| 400 | TOO_MANY_ACTIVE_JOBS | Already have 2 active import jobs |
| 400 | ITEMS_NOT_FOUND | Specified items don't belong to this vendor on this order |
| 401 | UNAUTHORIZED | Missing, invalid, or frozen API key |
| 403 | FORBIDDEN | Valid key but vendor account suspended/banned |
| 403 | INSUFFICIENT_PERMISSIONS | Key lacks the required permission |
| 404 | ORDER_NOT_FOUND | Order does not exist or doesn't belong to your store |
| 404 | SHIPMENT_NOT_FOUND | Shipment does not exist or doesn't belong to your store |
| 404 | PRODUCT_NOT_FOUND | Product does not exist or doesn't belong to your store |
| 404 | WEBHOOK_NOT_FOUND | Webhook does not exist or doesn't belong to your store |
| 408 | REQUEST_TIMEOUT | Request took longer than 15 seconds |
| 409 | CONFLICT | General conflict |
| 409 | IDEMPOTENT_REQUEST_IN_PROGRESS | Duplicate idempotency key with request still in flight |
| 429 | RATE_LIMIT_EXCEEDED | Rate limit exceeded |
| 500 | INTERNAL_ERROR | Unexpected 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:
| Parameter | Type | Default | Description |
|---|---|---|---|
cursor | string | — | Pagination cursor |
limit | integer | 25 | Results per page (1–100) |
status | string | — | Filter by order status |
createdAfter | string | — | ISO 8601 date, only orders created after this time |
createdBefore | string | — | ISO 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:
| Field | Type | Required | Description |
|---|---|---|---|
orderId | string | yes | The order's public ID |
items | string[] | yes | Array of item public IDs to ship |
carrier | string | yes | Carrier name (e.g., usps, ups, fedex, dhl) |
trackingNumber | string | yes | Tracking number (4–80 characters) |
trackingUrl | string | no | Custom 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
400error. - Items must be in
PROCESSINGstatus. 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:
| Field | Type | Required | Description |
|---|---|---|---|
rows | array | yes | Array of shipment rows (1–5,000) |
rows[].orderNumber | string | yes | The order number (e.g., WB-10042) |
rows[].carrier | string | yes | Carrier name |
rows[].trackingNumber | string | yes | Tracking 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:
| Field | Type | Required | Description |
|---|---|---|---|
trackingNumber | string | yes | New tracking number (4–80 characters) |
carrier | string | no | New 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:
| Parameter | Type | Default | Description |
|---|---|---|---|
cursor | string | — | Pagination cursor |
limit | integer | 25 | Results per page (1–100) |
status | string | — | Filter by review status |
isActive | boolean | — | Filter by active/inactive |
search | string | — | Search 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:
| Field | Type | Required | Description |
|---|---|---|---|
quantity | integer | yes | New 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
startDate | string | yes | Start date (YYYY-MM-DD) |
endDate | string | yes | End 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
startDate | string | yes | Start date (YYYY-MM-DD) |
endDate | string | yes | End date (YYYY-MM-DD) |
cursor | string | no | Pagination cursor |
limit | integer | no | Results 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
startDate | string | yes | Start date (YYYY-MM-DD) |
endDate | string | yes | End date (YYYY-MM-DD) |
cursor | string | no | Pagination cursor |
limit | integer | no | Results 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
startDate | string | yes | Start date (YYYY-MM-DD) |
endDate | string | yes | End date (YYYY-MM-DD) |
limit | integer | no | Number 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
| Event | Description |
|---|---|
order.paid | A customer completed checkout and payment was captured |
order.cancelled | An order was cancelled |
shipment.created | A shipment was created for one of your orders |
shipment.delivered | A shipment was marked as delivered |
refund.processed | A 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:
| Field | Type | Required | Description |
|---|---|---|---|
url | string | yes | HTTPS endpoint URL (max 500 characters) |
events | string[] | yes | Event types to subscribe to (1–6 items) |
description | string | no | Human-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):
| Field | Type | Description |
|---|---|---|
url | string | New HTTPS endpoint URL |
events | string[] | New event subscriptions |
description | string | New description |
isActive | boolean | Enable 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:
| Parameter | Type | Default | Description |
|---|---|---|---|
cursor | string | — | Pagination cursor |
limit | integer | 25 | Results 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
}
]
}
}
| Field | Type | Description |
|---|---|---|
orderId | string | Order public ID |
orderNumber | string | Human-readable order number |
vendorTotal | number | Total for your items only (not the full order total) |
currency | string | ISO 4217 currency code |
paidAt | string | When payment was captured |
items | array | Your items on this order |
items[].id | string | Item public ID |
items[].sku | string or null | Variant SKU |
items[].productTitle | string or null | Product title |
items[].quantity | integer | Quantity ordered |
items[].unitPrice | number | Price per unit |
items[].subtotal | number | Line 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"
}
}
| Field | Type | Description |
|---|---|---|
orderId | string | Order public ID |
orderNumber | string | Human-readable order number |
cancelledAt | string | When the order was cancelled |
reason | string or null | Cancellation reason code |
note | string or null | Optional 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"]
}
}
| Field | Type | Description |
|---|---|---|
shipmentId | integer | Shipment ID |
orderId | string | Order public ID |
orderNumber | string | Human-readable order number |
carrier | string | Carrier name |
trackingNumber | string | Tracking number |
trackingUrl | string or null | Tracking URL |
status | string | Shipment status |
shippedAt | string or null | When the shipment was created |
items | string[] | 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"
}
}
| Field | Type | Description |
|---|---|---|
orderId | string | Order public ID |
orderNumber | string | Human-readable order number |
refundAmountCents | integer | Refund amount in cents |
refundAmount | number | Refund amount in dollars |
currency | string | ISO 4217 currency code |
isPartial | boolean | Whether this is a partial refund |
executedAt | string | When 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:
| Attempt | Delay after failure |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 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
isActivefield becomesfalse. - The
disabledAttimestamp 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
200as 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-Idheader to deduplicate. - Monitor delivery health. Use
GET /api/v1/webhooks/:id/deliveriesto 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:
| Header | Description |
|---|---|
X-Wabbus-Event | The event type (e.g., order.paid) |
X-Wabbus-Delivery-Id | Unique delivery ID |
X-Wabbus-Timestamp | Unix timestamp (seconds) when the delivery was sent |
X-Wabbus-Signature | HMAC-SHA256 signature in the format sha256={hex} |
Verification steps
- Extract the timestamp and signature from the headers.
- Build the signed payload: concatenate the timestamp, a period, and the raw request body:
{timestamp}.{body} - Compute the expected signature: HMAC-SHA256 of the signed payload using your webhook secret.
- Compare signatures using a constant-time comparison to prevent timing attacks.
- 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
| Resource | Limit |
|---|---|
| API keys per vendor | 5 |
| Webhooks per vendor | 5 |
| Webhook event subscriptions per webhook | 6 |
| Bulk import rows per request | 5,000 |
| Concurrent import jobs | 2 |
| Request body size | 5 MB |
| Analytics date range | 90 days |
| Webhook delivery retention | 30 days |
| Idempotency key TTL | 24 hours |
| API timeout | 15 seconds |
| Tracking number length | 4–80 characters |
| Webhook URL length | 500 characters |
| Product search query | 200 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.