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.