Skip to main content
Read API · v1 · read-onlyWebhooks · HMAC-SHA256 signedAuth · org API keys

The API, documented like the floor.

Read API v1 + signed webhooks for organization accounts: poll your orders, custody records, certifications, and our live inventory, or receive signed events as they happen. Everything an integration needs is on this page — request shapes, field types, enum values, limits, retries, and working verification code.

Access

API access requires an organization account. Keys are created in the client portal — Client portal › Organization › API keys — by an organization admin, and shown exactly once. If organizations are not yet enabled for your account, keyed endpoints return 503 api_unavailable until they are; the surface documented below is stable to build against either way. Register or contact us to get set up.

60 req/min per key6 delivery attempts · ~14.6 h300 s signature windowv1 additive-only
01 · Quickstart

First call in two minutes.

One key from the portal, one curl. Every endpoint speaks the same envelope, so this is the whole learning curve.

1 — Create a key. Sign in to the client portal as an organization admin and open Organization › API keys › Create key. The raw key — vig_k_ plus 40 random characters — is shown exactly once; we store only its SHA-256 hash. Keep it in a secret manager, never in client-side code.

2 — Call the API. Send the key as a Bearer token over HTTPS:

curl
# Your key is shown exactly once, at creation, in the client portal.
export VIG_API_KEY="vig_k_..."

curl "https://www.vigitad.com/api/v1/orders?limit=2" \
  -H "Authorization: Bearer $VIG_API_KEY"
JSON · 200 OK
{
  "data": [
    {
      "id": "8b1f6f2a-4c0d-4e8a-9b2f-3d5a1c7e9f01",
      "type": "auction",
      "lot_id": "VIG-AUC-2026-0341",
      "title": "Dell Latitude 5440 - 24-unit lot",
      "status": "paid",
      "currency": "USD",
      "total": "3277.50",
      "amounts": {
        "hammer_price": "2850.00",
        "premium_pct": 15,
        "premium_amount": "427.50"
      },
      "created_at": "2026-07-09T21:05:03.512Z",
      "paid_at": "2026-07-11T15:40:12.008Z"
    },
    {
      "id": "2e9c8d17-65b3-4f21-8a4e-0c6b2d9e5a77",
      "type": "wholesale",
      "lot_id": "VIG-WS-2026-0187",
      "title": "HP EliteDesk 800 G6 - pallet of 40",
      "status": "issued",
      "currency": "USD",
      "total": "4600.00",
      "amounts": {
        "unit_price": "115.00",
        "quantity": 40
      },
      "created_at": "2026-07-06T16:12:44.031Z",
      "paid_at": null
    }
  ],
  "meta": {
    "next_cursor": "MjAyNi0wNy0wNlQxNjoxMjo0NC4wMzFafDJlOWM4ZDE3LTY1YjMtNGYyMS04YTRlLTBjNmIyZDllNWE3Nw"
  }
}

3 — Read the envelope. Every success is { "data": ..., "meta": { "next_cursor": ... } }; every failure is { "error": { "code": ..., "message": ... } }. Timestamps are ISO-8601 UTC, money fields are decimal strings in USD, and enum values are listed verbatim with each endpoint below. Within v1 the surface only grows — ignore JSON fields you do not recognize.

02 · Authentication

Bearer keys, org-scoped.

Every key belongs to one organization and reads only that org’s data. All v1 keys are read-only — no key can mutate anything.

Authenticate every request with Authorization: Bearer vig_k_… over HTTPS. Keys are identified in the portal by their display prefix (vig_k_ + the first 8 random characters); the full key is never shown again after creation and never appears in audit trails.

API key lifecycle — managed in Client portal › Organization › API keys (org admins)
OperationBehavior
CreateUp to 10 active keys per organization, optional expiry date. The raw key is shown exactly once; only its SHA-256 hash is stored.
RevokeSoft revoke — the key stops working immediately and stays in the org audit trail.
RotateOne operation: creates a new key and revokes the old one immediately; the response carries the new raw key once. Need zero-downtime overlap? Create the new key first, migrate, then revoke the old one.
ExpiryKeys past their optional expires_at return 403.
Last usedlast_used_at is tracked (updated at most once per 5 minutes per key) and surfaced in the portal key list.

Failure semantics

401 unauthorized — the key is missing or unknown. 403 forbidden — the key is revoked or expired; no further detail is disclosed. 503 api_unavailable — the API is not yet enabled, or a temporary backend failure. Failed lookups never confirm whether a key exists.

error responses
HTTP/1.1 401 Unauthorized
{ "error": { "code": "unauthorized", "message": "Unknown API key." } }

HTTP/1.1 403 Forbidden
{ "error": { "code": "forbidden", "message": "This API key is revoked or expired." } }

HTTP/1.1 503 Service Unavailable
{ "error": { "code": "api_unavailable", "message": "API access is not yet enabled." } }

What is never in a response

  • Certificate documents. The certificates endpoint returns metadata/status only; certificate PDFs are not served by any API route.
  • Wire or payment details. The API returns amounts and statuses only. Payment instructions live behind the authenticated client portal — and are never sent by email.
  • Reserve prices, live bid maximums, other bidders, secrets or key hashes.
03 · Endpoints

Read API v1 reference.

Six read-only endpoints. Keyed responses are sent with Cache-Control: private, no-store; the public inventory feed is cacheable.

Endpoint overview
EndpointAuthReturns
GET /api/v1/orgAPI keyYour organization record
GET /api/v1/ordersAPI keyUnified auction + wholesale orders (paginated)
GET /api/v1/orders/{id}API keyOne order — 404 outside your org
GET /api/v1/custody-eventsAPI keyPickup / custody records, current state (paginated)
GET /api/v1/certificatesAPI keyCertification register — metadata/status only
GET /api/v1/inventoryPublic (key optional)Live auction + wholesale inventory
GET/api/v1/orgorg-scoped · keyed

The authenticated organization’s own record. data is a single object, not a list.

org_refstringDisplay reference, format VIG-ORG-YYYY-MMDD-XXXX.
namestringOrganization name as shown in the portal.
created_atstring · ISO-8601 UTCWhen the organization was created.
JSON · 200 OK
{
  "data": {
    "org_ref": "VIG-ORG-2026-0614-8C2A",
    "name": "Northwind Asset Recovery",
    "created_at": "2026-06-14T15:02:11.914Z"
  },
  "meta": { "next_cursor": null }
}
GET/api/v1/ordersorg-scoped · keyed · paginated

One unified, type-discriminated list of your org’s auction invoices (lots won by org members) and wholesale invoices (lots reserved by org members), newest first on a (created_at, id) keyset.

typequery · optionalFilter to auction or wholesale.
statusquery · optionalFilter to issued, paid, or cancelled.
limitquery · optionalPage size 1–100; default 25.
cursorquery · optionalOpaque cursor from meta.next_cursor — pass back verbatim.
idstring · UUIDStable order identity — use with /api/v1/orders/{id} and to reconcile webhook payloads.
typeenumauction · wholesale
lot_idstringPublic lot code: VIG-AUC-YYYY-XXXX (auction) or VIG-WS-YYYY-XXXX (wholesale).
titlestringLot title at invoice time.
statusenumissued · paid · cancelled — settlement is staff-confirmed wire payment.
currencystringAlways USD.
totaldecimal string | nullInvoice total, e.g. "3277.50".
amountsobjectAuction orders: hammer_price (decimal string), premium_pct (number), premium_amount (decimal string). Wholesale orders: unit_price (decimal string), quantity (number).
created_atstring · ISO-8601 UTCInvoice issue time — also the pagination sort key.
paid_atstring | nullSet when staff confirm settlement.
GET/api/v1/orders/{id}org-scoped · keyed

One order by UUID. Returns 404 not_found for anything outside your organization — existence is never confirmed across orgs. The data object is identical to a list item; no envelope meta on single-resource reads.

curl
curl "https://www.vigitad.com/api/v1/orders/8b1f6f2a-4c0d-4e8a-9b2f-3d5a1c7e9f01" \
  -H "Authorization: Bearer $VIG_API_KEY"
JSON · 200 OK
{
  "data": {
    "id": "8b1f6f2a-4c0d-4e8a-9b2f-3d5a1c7e9f01",
    "type": "auction",
    "lot_id": "VIG-AUC-2026-0341",
    "title": "Dell Latitude 5440 - 24-unit lot",
    "status": "paid",
    "currency": "USD",
    "total": "3277.50",
    "amounts": {
      "hammer_price": "2850.00",
      "premium_pct": 15,
      "premium_amount": "427.50"
    },
    "created_at": "2026-07-09T21:05:03.512Z",
    "paid_at": "2026-07-11T15:40:12.008Z"
  }
}
GET/api/v1/custody-eventsorg-scoped · keyed · paginated

Pickup/custody records submitted by members of your org, newest first on a (submitted_at, id) keyset. Takes the same limit and cursor parameters as the orders list.

Granularity, stated honestly: these are current-state records, not a transition timeline — each row carries its present status and updated_at only. For a real-time timeline, subscribe to the custody.status_changed webhook and persist payloads on receipt; the portal delivery log retains deliveries for 30 days.

idstring · UUIDPickup request id — matches pickup_id in webhook payloads.
reference_codestringHuman-friendly reference, format VIG-YYYY-MMDD-XXXX.
statusenumsubmitted · quoted · scheduled · in_process · complete · cancelled
pickup_zipstringPickup ZIP code.
pickup_windowenumthis_week · next_week · 30_days · flexible
scheduled_pickup_datestring | nullDate (YYYY-MM-DD) once scheduled.
quote_amountdecimal string | nullQuoted amount once quoted.
submitted_atstring · ISO-8601 UTCSubmission time — the pagination sort key.
updated_atstring · ISO-8601 UTCLast status change.
JSON · 200 OK
{
  "data": [
    {
      "id": "6d2a9c40-91b7-4f83-a1c2-5e8f0b3d7a19",
      "reference_code": "VIG-2026-0712-9A3B",
      "status": "scheduled",
      "pickup_zip": "75204",
      "pickup_window": "next_week",
      "scheduled_pickup_date": "2026-07-28",
      "quote_amount": "1850.00",
      "submitted_at": "2026-07-12T14:03:27.480Z",
      "updated_at": "2026-07-15T19:22:05.112Z"
    }
  ],
  "meta": { "next_cursor": null }
}
GET/api/v1/certificateskeyed · fixed register

The certification register — metadata and status only, from the same fixed registry shown on the certifications page. Fields: id, title, standard, issuer, number, status (ACTIVE · SUPPORTING), valid_through. Certificate documents are never served by this API — no field links to or embeds a certificate file, and no such route exists. Sample below truncated to two records for brevity.

JSON · 200 OK (truncated)
{
  "data": [
    {
      "id": "r2v3",
      "title": "R2v3 — Responsible Recycling",
      "standard": "SERI R2v3 Standard · IT Asset Disposition & Refurbishment",
      "issuer": "Amtivo (ANAB-accredited) · SERI",
      "number": "#274644",
      "status": "ACTIVE",
      "valid_through": "March 2029"
    },
    {
      "id": "iso14001",
      "title": "ISO 14001:2015",
      "standard": "Environmental Management System",
      "issuer": "Amtivo",
      "number": "#USA/E/00021/I1683",
      "status": "ACTIVE",
      "valid_through": "October 2028"
    }
  ],
  "meta": {
    "next_cursor": null,
    "posture": "Certificate documents are not downloadable from this site; originals are available for inspection on request."
  }
}
GET/api/v1/inventorypublic · key optional

The live sales floor — the same public data as the auctions and wholesale catalogs, public fields only: active auction lots (by closing time) and available wholesale lots (newest first), up to 500 each. No key is required; sending a valid key upgrades you from the 30 req/min per-IP bucket to the keyed 60 req/min bucket. Responses are cacheable (Cache-Control: public, max-age=60, stale-while-revalidate=300). Reserve prices and bidder identities are never included.

categoryenumlaptops · desktops · monitors · accessories
condition_gradeenumA · B · C · parts — cosmetic grades; see the condition grading page.
auction itemsfieldslot_id, title, category, subcategory, manufacturer, model, quantity, condition_grade, data_wiped (boolean), starting_bid, current_bid (decimal strings), bid_count, closes_at, pickup_location
wholesale itemsfieldslot_id, title, category, subcategory, manufacturer, model, quantity, min_order_qty, condition_grade, data_wiped, unit_price (decimal string), pickup_location
curl
# No key required. A valid key upgrades you to the 60 req/min bucket.
curl "https://www.vigitad.com/api/v1/inventory"
JSON · 200 OK (truncated)
{
  "data": {
    "auction": [
      {
        "lot_id": "VIG-AUC-2026-0355",
        "title": "Lenovo ThinkCentre M720q - 30-unit lot",
        "category": "desktops",
        "subcategory": "Tiny / USFF",
        "manufacturer": "Lenovo",
        "model": "M720q",
        "quantity": 30,
        "condition_grade": "B",
        "data_wiped": true,
        "starting_bid": "900.00",
        "current_bid": "1250.00",
        "bid_count": 7,
        "closes_at": "2026-07-24T19:00:00.000Z",
        "pickup_location": "Dallas, TX 75220"
      }
    ],
    "wholesale": [
      {
        "lot_id": "VIG-WS-2026-0201",
        "title": "Dell P2419H 24-inch monitors - Grade A",
        "category": "monitors",
        "subcategory": "24-inch",
        "manufacturer": "Dell",
        "model": "P2419H",
        "quantity": 120,
        "min_order_qty": 20,
        "condition_grade": "A",
        "data_wiped": false,
        "unit_price": "38.00",
        "pickup_location": "Dallas, TX 75220"
      }
    ]
  },
  "meta": { "next_cursor": null }
}

Pagination walkthrough

List endpoints use cursor pagination on a (created_at, id) keyset — stable under concurrent writes, no page drift. meta.next_cursor is an opaque token: pass it back verbatim as ?cursor=, never parse or construct it. null means you have everything.

Page size is limit 1–100 (default 25); invalid values fall back to the default, and an unparseable cursor returns 400 invalid_request.

Node.js · fetch all pages
let cursor = null;
do {
  const url = new URL('https://www.vigitad.com/api/v1/orders');
  url.searchParams.set('limit', '100');
  if (cursor) url.searchParams.set('cursor', cursor);

  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.VIG_API_KEY}` },
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);

  const page = await res.json();
  for (const order of page.data) handle(order);

  cursor = page.meta.next_cursor; // opaque - pass it back verbatim
} while (cursor);

Deliberately not in v1: bid feeds, member lists, messages, saved searches. The polling surface stays minimal until integrator demand exists — see the versioning promise below.

04 · Rate limits

60 a minute, honestly enforced.

Fixed one-minute windows. Every keyed response carries the limit headers, and 429s tell you exactly how long to wait.

Request rate limits
BucketLimitApplies to
Per API key60 req/minAll keyed requests (any /api/v1/* endpoint)
Per IP, unkeyed30 req/minGET /api/v1/inventory

Every keyed response includes X-RateLimit-Limit and X-RateLimit-Remaining. Going over returns 429 rate_limited with Retry-After — the seconds until the current window resets. Honor it and back off; do not hammer.

Burst honesty: fixed windows inherently allow up to 2× the limit across a window boundary, and enforcement is best-effort per serving instance rather than a globally synchronized counter. Treat the documented limits as the contract and any 429 as authoritative — the limiter is abuse-blunting, not a capacity guarantee to engineer against.

Node.js · 429 handling
async function vigFetch(url, attempt = 0) {
  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.VIG_API_KEY}` },
  });
  if (res.status === 429 && attempt < 3) {
    // Retry-After is seconds until the fixed window resets.
    const wait = Number(res.headers.get('Retry-After') ?? '60');
    await new Promise((r) => setTimeout(r, wait * 1000));
    return vigFetch(url, attempt + 1);
  }
  return res;
}
05 · Webhooks

Signed events, at least once.

HTTPS endpoints, HMAC-SHA256 signatures, six retries over ~14.6 hours, and a delivery log in the portal. Verify, ack fast, dedupe.

Org admins configure endpoints in Client portal › Organization › Webhooks: an HTTPS URL, the events to subscribe to, and a server-generated signing secret (visible to org admins with settings access; rotatable per endpoint). A test button sends a webhook.test ping through the real delivery pipeline. Up to 10 endpoints per organization.

Event catalog — v1
EventFires whenPayload data fields
order.invoice_issuedAn auction win is invoiced at lot close, or a wholesale reservation is invoicedorder_type, lot_id, title, total, status — plus hammer_price / premium_pct / premium_amount (auction) or unit_price / quantity (wholesale)
order.paidStaff confirm an invoice as settledorder_type, order_id, lot_id, title, total, status, paid_at
order.cancelledStaff cancel an invoiceorder_type, order_id, lot_id, title, total, status, paid_at
order.reservedA wholesale lot is reserved by an org memberorder_type, lot_id, title, unit_price, quantity, total
lot.soldAn auction lot closes sold — delivered to the winning org onlylot_id, title, hammer_price, status
lot.unsoldAn auction lot closes unsold — delivered to the high-bidding org, if anylot_id, title, hammer_price, status
custody.status_changedA pickup request transitions statuspickup_id, reference_code, status, changed_at
webhook.testYou press the test button in the portal (not subscribable; always deliverable)message

Not in v1: bid.outbid is deliberately excluded (per-bid fan-out volume). Planned, not promised: certificate.issued and asset-level custody.asset_* events depend on warehouse-system integration that lives outside this backend — they will appear in the changelog when real, and additive-only rules mean unknown event types must be ignored, not errored on.

The delivery request

Deliveries are HTTPS POSTs with a JSON body { "event", "occurred_at", "data" }. Payloads carry ids, statuses, and amounts only — never wire details, documents, or secrets.

Delivery headers
HeaderValue
Content-Typeapplication/json
User-AgentVIG-Webhooks/1.0
X-VIG-EventThe event name, e.g. order.paid
X-VIG-DeliveryDelivery UUID — your dedupe key; delivery is at-least-once
X-VIG-Signaturet=<unix seconds>,v1=<hex>[,v0=<hex>]
HTTP · example delivery
POST /your/webhook/path HTTP/1.1
Host: example.com
Content-Type: application/json
User-Agent: VIG-Webhooks/1.0
X-VIG-Event: order.invoice_issued
X-VIG-Delivery: 0f6a2c9e-4d17-4b52-8e33-b1a9c5d2e7f4
X-VIG-Signature: t=1783717504,v1=5c9c17b3e4f2...a94d

{
  "event": "order.invoice_issued",
  "occurred_at": "2026-07-09T21:05:04.118Z",
  "data": {
    "order_type": "auction",
    "lot_id": "VIG-AUC-2026-0341",
    "title": "Dell Latitude 5440 - 24-unit lot",
    "hammer_price": "2850.00",
    "premium_pct": 15,
    "premium_amount": "427.50",
    "total": "3277.50",
    "status": "issued"
  }
}
JSON · custody.status_changed payload
{
  "event": "custody.status_changed",
  "occurred_at": "2026-07-15T19:22:05.244Z",
  "data": {
    "pickup_id": "6d2a9c40-91b7-4f83-a1c2-5e8f0b3d7a19",
    "reference_code": "VIG-2026-0712-9A3B",
    "status": "scheduled",
    "changed_at": "2026-07-15T19:22:05.112Z"
  }
}

Verifying signatures

v1 is HMAC-SHA256(secret, "{delivery_id}.{timestamp}.{raw_body}") as lowercase hex, where delivery_id is the X-VIG-Delivery header, timestamp is the t value, and raw_body is the exact request body bytes. Verify with a constant-time compare, reject when |now − t| > 300 s, and always sign-check the raw body — re-serializing parsed JSON will not round-trip byte-for-byte.

After a secret rotation there is a 24-hour grace during which deliveries carry v1 (new secret) and v0 (previous secret) — accept if any candidate matches the secret you hold.

Node.js · verify + receive
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';

const WINDOW_SECONDS = 300; // reject signatures older/newer than 5 minutes

function verifyVigSignature(header, deliveryId, rawBody, secret) {
  // Parse "t=...,v1=...[,v0=...]" into { t: [...], v1: [...], v0: [...] }.
  const parts = {};
  for (const piece of header.split(',')) {
    const i = piece.indexOf('=');
    if (i <= 0) return false;
    const key = piece.slice(0, i).trim();
    (parts[key] ??= []).push(piece.slice(i + 1).trim());
  }

  const t = Number(parts.t?.[0]);
  if (!Number.isInteger(t)) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > WINDOW_SECONDS) return false;

  // Signed message: "{delivery_id}.{timestamp}.{raw_body}".
  // v1 = current secret; v0 appears only during the 24 h rotation grace.
  const expected = createHmac('sha256', secret)
    .update(`${deliveryId}.${t}.${rawBody}`)
    .digest();
  return [...(parts.v1 ?? []), ...(parts.v0 ?? [])].some((sig) => {
    const candidate = Buffer.from(sig, 'hex');
    return candidate.length === expected.length && timingSafeEqual(candidate, expected);
  });
}

const app = express();

// Keep the RAW body: the signature covers the exact bytes we sent.
app.post('/webhooks/vig', express.raw({ type: 'application/json' }), (req, res) => {
  const deliveryId = req.get('X-VIG-Delivery') ?? '';
  const ok = verifyVigSignature(
    req.get('X-VIG-Signature') ?? '',
    deliveryId,
    req.body.toString('utf8'),
    process.env.VIG_WEBHOOK_SECRET,
  );
  if (!ok) return res.status(400).end();

  res.status(200).end(); // ack fast (well under 10 s) - do real work async

  // Delivery is AT-LEAST-ONCE: dedupe on deliveryId before processing.
  const payload = JSON.parse(req.body.toString('utf8'));
  handleEvent(deliveryId, payload);
});
Python · verify
import hashlib
import hmac
import time

WINDOW_SECONDS = 300  # reject signatures older/newer than 5 minutes

def verify_vig_signature(header: str, delivery_id: str, raw_body: bytes, secret: str) -> bool:
    parts: dict[str, list[str]] = {}
    for piece in header.split(","):
        key, sep, value = piece.partition("=")
        if not sep:
            return False
        parts.setdefault(key.strip(), []).append(value.strip())

    try:
        t = int(parts["t"][0])
    except (KeyError, ValueError):
        return False
    if abs(time.time() - t) > WINDOW_SECONDS:
        return False

    # Signed message: "{delivery_id}.{timestamp}.{raw_body}".
    message = f"{delivery_id}.{t}.".encode() + raw_body
    expected = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()

    # v1 = current secret; v0 appears only during the 24 h rotation grace.
    candidates = parts.get("v1", []) + parts.get("v0", [])
    return any(hmac.compare_digest(sig, expected) for sig in candidates)

Retries, ordering, auto-disable

A delivery succeeds on any 2xx within 10 seconds. Anything else — non-2xx, timeout, or a redirect — counts as a failed attempt and is retried on this schedule:

Retry schedule — 6 attempts, then dead
AttemptScheduledElapsed (approx.)
1On enqueue — picked up by the next dispatch sweep (≤ 60 s; typically seconds)~0 min
2+1 min after attempt 1 fails~1 min
3+5 min~6 min
4+30 min~36 min
5+2 h~2.6 h
6+12 h~14.6 h
  • At-least-once, unordered. Retries and redeliveries mean you can see the same delivery twice and events out of order — dedupe on X-VIG-Delivery and treat occurred_at as the event clock.
  • Dead deliveries (all 6 attempts failed) stay visible in the portal delivery log — endpoint, event, attempt count, response code, latency, next retry — retained for 30 days.
  • Auto-disable: 20 consecutive failed deliveries flips the endpoint to inactive and records an org-audit event; re-enable it in the portal after fixing your receiver. A successful delivery resets the count.
  • Missed a window? The Read API is the reconciliation path — poll /api/v1/orders and /api/v1/custody-events to catch up.

Endpoint requirements

  • HTTPS only, default port 443. Plain http://, custom ports, credentials in the URL, URL fragments, and raw IP-literal hosts are rejected at registration.
  • Publicly resolvable host. The hostname must resolve to public (global-unicast) addresses only — private, loopback, link-local, carrier-NAT, multicast, and reserved ranges are rejected, including IPv6 forms that embed such an IPv4 address. Checked when you save the endpoint and before every delivery; the delivery socket dials only the validated addresses, so DNS rebinding between check and connect does not work.
  • No redirects. 3xx responses are not followed and count as failed attempts.
  • Ack fast. Respond 2xx within 10 seconds and process asynchronously. Only the first 1 KB of your response body is read (and logged truncated on failure).
  • Body size. Delivery bodies are capped at 64 KB.
06 · Errors

One envelope, six codes.

Every non-2xx response is { "error": { "code", "message" } }. Branch on code; message is human-readable and may change.

Error codes — Read API v1
HTTPcodeMeaningWhat to do
400invalid_requestMalformed query parameter or cursorFix the request; do not retry unchanged.
401unauthorizedMissing or unknown API keyCheck the Authorization header and the key value.
403forbiddenKey revoked or expired (no further detail)Create or rotate a key in the portal.
404not_foundNo such resource within your org scopeExistence is never confirmed across orgs.
429rate_limitedOver the per-key (or per-IP) request rateWait Retry-After seconds, then back off.
503api_unavailableAPI not yet enabled, or a temporary backend failureRetry with backoff; if it persists, API access may not be enabled for your org yet.
07 · Changelog

The versioning promise.

v1 only grows. Anything that would break you ships as /api/v2, with v1 kept running through the migration.

Within v1, changes are additive-only: new endpoints, new optional parameters, new response fields, and new webhook event types may appear; existing fields are never removed, renamed, or retyped, and documented enum values are never repurposed. Build tolerant clients: ignore unknown JSON fields and unknown event types.

Breaking changes mean /v2. A breaking change ships as a new version prefix announced here, with both versions running side by side during the migration window.

Changelog
DateChange
2026-07v1 initial release: GET /api/v1/org, /orders, /orders/{id}, /custody-events, /certificates, /inventory; org API keys; webhooks with 7 subscribable events, HMAC-SHA256 signatures (t/v1, rotation grace v0), 6-attempt retry, portal delivery log.

Questions this page failed to answer are a documentation bug — tell us via the contact page and we will fix the page, not just the answer.

Get a quote