OptiWMS API
Signing
HMAC over timestamp.method.path.body, a worked example, and the webhook scheme beside it.
Signing
Every OptiWMS API request carries three headers:
X-OptiWMS-Integration: the integration ID from the setup.X-OptiWMS-Timestamp: the current time in Unix seconds.X-OptiWMS-Signature:sha256=followed by the lowercase hex HMAC-SHA256 of the canonical string, keyed with the shared secret.
The canonical string is timestamp.method.path.body: the timestamp header's value, the method in capitals, the full request path as sent, and the exact body bytes, joined by single dots with no spaces. A GET has an empty body, so its string ends with a trailing dot.
Path rule
The path is the whole path of the request URL, starting with /store/v1 for calls into OptiWMS, exactly as it travels, percent-encoding included, and without the query string. An order id #1001 travels as /store/v1/orders/%231001, and that encoded form is what you sign.
Calls OptiWMS makes to your system are signed the same way over the full path your server receives, including any path your registered base URL starts with. If you registered https://shop.example.com/optiwms, a stock push arrives as POST /optiwms/stock and the signed path is /optiwms/stock. Verify with the same code you use for outbound calls.
Sign the bytes you send: serialize the body once and send those same bytes. Re-encoding JSON (field order, spaces, a trailing newline) changes the signature.
A timestamp more than 15 minutes from OptiWMS's clock is refused (401 with an empty body, same as a bad signature). OptiWMS does not say which check failed.
Worked example
Secret (32 characters): example-secret-0123456789abcdefg
Timestamp: 1793091600
Method and path: POST /store/v1/orders/%231001/cancel
Body bytes: {"reason":"Customer asked to cancel"}
Canonical string:
1793091600.POST./store/v1/orders/%231001/cancel.{"reason":"Customer asked to cancel"}
Signature header:
sha256=932847671952282fd0d743732b4ee5c765b59c48fde1ace1721dd343980ca4cc
That value is what auth.Sign produces for those inputs; the OpenAPI document carries the same example so it cannot drift.
PHP
<?php
$secret = 'example-secret-0123456789abcdefg';
$timestamp = '1793091600';
$method = 'POST';
$path = '/store/v1/orders/%231001/cancel';
$body = '{"reason":"Customer asked to cancel"}';
$canonical = $timestamp . '.' . $method . '.' . $path . '.' . $body;
$signature = 'sha256=' . hash_hmac('sha256', $canonical, $secret);
// $signature === 'sha256=932847671952282fd0d743732b4ee5c765b59c48fde1ace1721dd343980ca4cc'
Node.js
import crypto from 'node:crypto'
const secret = 'example-secret-0123456789abcdefg'
const timestamp = '1793091600'
const method = 'POST'
const path = '/store/v1/orders/%231001/cancel'
const body = '{"reason":"Customer asked to cancel"}'
const canonical = `${timestamp}.${method}.${path}.${body}`
const signature =
'sha256=' + crypto.createHmac('sha256', secret).update(canonical, 'utf8').digest('hex')
// signature === 'sha256=932847671952282fd0d743732b4ee5c765b59c48fde1ace1721dd343980ca4cc'
Python
import hashlib
import hmac
secret = b"example-secret-0123456789abcdefg"
timestamp = "1793091600"
method = "POST"
path = "/store/v1/orders/%231001/cancel"
body = b'{"reason":"Customer asked to cancel"}'
canonical = f"{timestamp}.{method}.{path}.".encode("utf-8") + body
signature = "sha256=" + hmac.new(secret, canonical, hashlib.sha256).hexdigest()
# signature == "sha256=932847671952282fd0d743732b4ee5c765b59c48fde1ace1721dd343980ca4cc"
Answers before the call runs
| Status | Body | Meaning |
|---|---|---|
| 401 | empty | Unknown integration, bad signature, or timestamp outside the 15-minute window. Check the integration ID, secret, signed path (full path, encoded as sent), body bytes and clock. The integration page has a signature checker. |
| 410 | empty | The integration is disabled or archived. Stop sending; an administrator can turn it back on. |
| 413 | {"error":"..."} or empty |
Body over 1 MiB. When Content-Length is set the answer is a short JSON error without a request id; a chunked body is cut off with an empty 413. |
| 429 | error envelope RATE_LIMITED |
More than 300 calls in a minute for this integration. Wait the seconds in Retry-After. |
Every answer after the signature verifies carries X-RateLimit-Limit (300) and X-RateLimit-Remaining.
Error envelope
Every other error is:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Fix the fields listed in details and send again.",
"details": [],
"requestId": "..."
}
}
| Code | Status | What to do |
|---|---|---|
VALIDATION_ERROR |
400 | Fix the fields listed in details and send again. |
NOT_FOUND |
404 | No call matches the method and path. |
ORDER_NOT_FOUND |
404 | No order with this id for this integration. Send it with PUT /orders/{orderId}. |
ORDER_IN_FULFILLMENT |
409 | The warehouse has started on the order. Release the pick, then send again; see Order rules. |
ORDER_SHIPPED |
409 | The order has shipped and can no longer change. |
ORDER_CANCELED |
409 | The order is canceled in OptiWMS and can no longer change. |
RATE_LIMITED |
429 | Wait the seconds in Retry-After, then send again. |
INTERNAL |
500 | A problem on OptiWMS's side. Send again in a minute; if it keeps failing, contact support with the request id. |
Dashboard notification webhooks (separate scheme)
Subscriptions you create in the dashboard (topics such as order.shipped) are not OptiWMS API calls. They POST a snake_case JSON body to your URL and sign only the body:
- Header:
X-OptiWMS-Hmac-Sha256 - Value: Base64 of HMAC-SHA256 over the raw body bytes, keyed with the subscription secret
- Also sent:
X-OptiWMS-Webhook-Version: 1
Do not mix the two schemes. OptiWMS API traffic (both directions) uses X-OptiWMS-Timestamp and X-OptiWMS-Signature over timestamp.method.path.body. Notification webhooks use body-only HMAC. Details and the store field are in Webhooks.
Connect your own store
Register the integration in the dashboard, then start with GET /ping.