Authentication

The Partner API uses a hybrid authentication model:

  • Every request must carry your API key in the X-API-Key header.
  • Mutating endpoints must additionally be signed with HMAC-SHA256.

Which Endpoints Require a Signature

EndpointMethodRequires HMAC signature
/v2/partners/ordersPOSTYes
/v2/partners/webhooksPOSTYes
/v2/partners/webhooks/:idPUT, DELETEYes
/v2/partners/webhooks/:id/testPOSTYes
All other endpoints (all GETs)GETNo, 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

HeaderRequired onDescription
X-API-KeyAll requestsYour organization API key
X-TimestampSigned requests onlyCurrent Unix timestamp in seconds (e.g., 1711000000)
X-NonceSigned requests onlyA unique string per request (UUID v4 recommended)
X-SignatureSigned requests onlyHMAC-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:

text
signingKey = SHA256(apiSecret).hexDigest()   // 64-char lowercase hex string

The 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:

text
message   = "{timestamp}.{nonce}.{method}.{path}.{body}"
signature = HMAC-SHA256(signingKey, message).hexDigest()

The message components, joined with dots (.):

  1. timestamp -- Unix timestamp in seconds (same value as the X-Timestamp header)
  2. nonce -- The same value sent in the X-Nonce header
  3. method -- The HTTP method in uppercase (e.g., POST)
  4. path -- The full request path (e.g., /v2/partners/orders)
  5. 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.parse followed by JSON.stringify preserves 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 to DELETE /v2/partners/webhooks/:id and POST /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

StatusCause
401Missing or invalid X-API-Key; missing X-Timestamp, X-Nonce, or X-Signature on a signed endpoint
403Signature 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-Timestamp must be within 5 minutes (300 seconds) of server time.
  • Each X-Nonce value is single-use. Reusing a nonce within its 5-minute tracking window results in a 403.
  • 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.