Authentication
The Partner API uses a hybrid authentication model:
- Every request must carry your API key in the
X-API-Keyheader. - Mutating endpoints must additionally be signed with HMAC-SHA256.
Which Endpoints Require a Signature
| Endpoint | Method | Requires HMAC signature |
|---|---|---|
/v2/partners/orders | POST | Yes |
/v2/partners/webhooks | POST | Yes |
/v2/partners/webhooks/:id | PUT, DELETE | Yes |
/v2/partners/webhooks/:id/test | POST | Yes |
| All other endpoints (all GETs) | GET | No, API key only |
For GET requests (products, orders, wallet, webhook listing) you only need X-API-Key. The signing headers are ignored if sent.
Required Headers
| Header | Required on | Description |
|---|---|---|
X-API-Key | All requests | Your organization API key |
X-Timestamp | Signed requests only | Current Unix timestamp in seconds (e.g., 1711000000) |
X-Nonce | Signed requests only | A unique string per request (UUID v4 recommended) |
X-Signature | Signed requests only | HMAC-SHA256 hex signature of the request |
Signing Key Derivation
The HMAC signing key is not your raw API secret. It is the SHA-256 hex digest of your API secret:
signingKey = SHA256(apiSecret).hexDigest() // 64-char lowercase hex stringThe server stores and verifies against this hash, so signatures computed with the raw secret will always be rejected with a 403.
Signature Construction
Build a dot-separated message string and sign it with the derived key:
message = "{timestamp}.{nonce}.{method}.{path}.{body}"
signature = HMAC-SHA256(signingKey, message).hexDigest()The message components, joined with dots (.):
- timestamp -- Unix timestamp in seconds (same value as the
X-Timestampheader) - nonce -- The same value sent in the
X-Nonceheader - method -- The HTTP method in uppercase (e.g.,
POST) - path -- The full request path (e.g.,
/v2/partners/orders) - body -- The JSON body string (see below)
The resulting HMAC digest must be encoded as lowercase hex.
Body Serialization
The server verifies the signature against JSON.stringify of the parsed request body, not the raw bytes you sent. To make both sides match:
- Send compact JSON (no extra whitespace, no pretty-printing).
- Keep a stable key order: sign the exact string you send.
JSON.parsefollowed byJSON.stringifypreserves key order, so signing your own compact serialization is safe. - Do not re-order or re-format the body between signing and sending.
- Requests without a body (for example
DELETE /webhooks/:id): use the empty string for the body segment, so the message ends with a trailing dot β"{timestamp}.{nonce}.DELETE.{path}.". For compatibility, the API also accepts""and{}in this position, but new integrations should use the empty string. This applies toDELETE /v2/partners/webhooks/:idandPOST /v2/partners/webhooks/:id/test.
Examples
API_KEY="your-api-key"
API_SECRET="your-api-secret"
# Signing key = SHA-256 hex digest of your API secret (as an ASCII string)
# $NF, not $2: OpenSSL prints "SHA2-256(stdin)= <hash>" but LibreSSL (the macOS
# default) prints the bare hash, where $2 is empty β that yields an empty signing
# key and a 401 with nothing to indicate why.
SIGNING_KEY=$(echo -n "${API_SECRET}" | openssl dgst -sha256 | awk '{print $NF}')
TIMESTAMP=$(date +%s)
NONCE=$(uuidgen | tr '[:upper:]' '[:lower:]')
METHOD="POST"
REQ_PATH="/v2/partners/webhooks"
# Compact JSON, no whitespace. Sign the exact string you send.
BODY='{"url":"https://yourapp.com/webhooks/vignetim","events":["order.completed"]}'
MESSAGE="${TIMESTAMP}.${NONCE}.${METHOD}.${REQ_PATH}.${BODY}"
SIGNATURE=$(echo -n "${MESSAGE}" | \
openssl dgst -sha256 -hmac "${SIGNING_KEY}" | \
awk '{print $NF}')
curl -X POST "https://api.vignetim.com${REQ_PATH}" \
-H "Content-Type: application/json" \
-H "X-API-Key: ${API_KEY}" \
-H "X-Timestamp: ${TIMESTAMP}" \
-H "X-Nonce: ${NONCE}" \
-H "X-Signature: ${SIGNATURE}" \
-d "${BODY}"Error Semantics
| Status | Cause |
|---|---|
401 | Missing or invalid X-API-Key; missing X-Timestamp, X-Nonce, or X-Signature on a signed endpoint |
403 | Signature mismatch, expired timestamp, reused nonce, IP not on the allowlist, endpoint not allowed for this key, or organization not active |
All authentication failures return the same generic message, Authentication failed. The API deliberately does not disclose which check failed. When debugging a 403 on a signed request, verify in order: signing key derivation (SHA-256 of the secret), timestamp freshness, nonce uniqueness, message construction, and body serialization.
Important Notes
- The
X-Timestampmust be within 5 minutes (300 seconds) of server time. - Each
X-Noncevalue is single-use. Reusing a nonce within its 5-minute tracking window results in a403. - The signature must be lowercase hex-encoded. Do not use base64.
- API keys are environment-scoped:
vgn_live_...keys hit live mode,vgn_test_...keys hit sandbox mode on the same endpoints. - Keys may optionally be restricted to specific IPs and endpoints; requests outside those allowlists return
403.