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.
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.
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:
# 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"{
"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.
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.
| Operation | Behavior |
|---|---|
| Create | Up to 10 active keys per organization, optional expiry date. The raw key is shown exactly once; only its SHA-256 hash is stored. |
| Revoke | Soft revoke — the key stops working immediately and stays in the org audit trail. |
| Rotate | One 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. |
| Expiry | Keys past their optional expires_at return 403. |
| Last used | last_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.
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.
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 | Auth | Returns |
|---|---|---|
GET /api/v1/org | API key | Your organization record |
GET /api/v1/orders | API key | Unified auction + wholesale orders (paginated) |
GET /api/v1/orders/{id} | API key | One order — 404 outside your org |
GET /api/v1/custody-events | API key | Pickup / custody records, current state (paginated) |
GET /api/v1/certificates | API key | Certification register — metadata/status only |
GET /api/v1/inventory | Public (key optional) | Live auction + wholesale inventory |
The authenticated organization’s own record. data is a single object, not a list.
VIG-ORG-YYYY-MMDD-XXXX.{
"data": {
"org_ref": "VIG-ORG-2026-0614-8C2A",
"name": "Northwind Asset Recovery",
"created_at": "2026-06-14T15:02:11.914Z"
},
"meta": { "next_cursor": null }
}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.
auction or wholesale.issued, paid, or cancelled.meta.next_cursor — pass back verbatim./api/v1/orders/{id} and to reconcile webhook payloads.auction · wholesaleVIG-AUC-YYYY-XXXX (auction) or VIG-WS-YYYY-XXXX (wholesale).issued · paid · cancelled — settlement is staff-confirmed wire payment.USD."3277.50".hammer_price (decimal string), premium_pct (number), premium_amount (decimal string). Wholesale orders: unit_price (decimal string), quantity (number).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 "https://www.vigitad.com/api/v1/orders/8b1f6f2a-4c0d-4e8a-9b2f-3d5a1c7e9f01" \
-H "Authorization: Bearer $VIG_API_KEY"{
"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"
}
}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.
pickup_id in webhook payloads.VIG-YYYY-MMDD-XXXX.submitted · quoted · scheduled · in_process · complete · cancelledthis_week · next_week · 30_days · flexible{
"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 }
}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.
{
"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."
}
}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.
laptops · desktops · monitors · accessoriesA · B · C · parts — cosmetic grades; see the condition grading page.lot_id, title, category, subcategory, manufacturer, model, quantity, condition_grade, data_wiped (boolean), starting_bid, current_bid (decimal strings), bid_count, closes_at, pickup_locationlot_id, title, category, subcategory, manufacturer, model, quantity, min_order_qty, condition_grade, data_wiped, unit_price (decimal string), pickup_location# No key required. A valid key upgrades you to the 60 req/min bucket.
curl "https://www.vigitad.com/api/v1/inventory"{
"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.
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.
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.
| Bucket | Limit | Applies to |
|---|---|---|
| Per API key | 60 req/min | All keyed requests (any /api/v1/* endpoint) |
| Per IP, unkeyed | 30 req/min | GET /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.
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;
}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 | Fires when | Payload data fields |
|---|---|---|
order.invoice_issued | An auction win is invoiced at lot close, or a wholesale reservation is invoiced | order_type, lot_id, title, total, status — plus hammer_price / premium_pct / premium_amount (auction) or unit_price / quantity (wholesale) |
order.paid | Staff confirm an invoice as settled | order_type, order_id, lot_id, title, total, status, paid_at |
order.cancelled | Staff cancel an invoice | order_type, order_id, lot_id, title, total, status, paid_at |
order.reserved | A wholesale lot is reserved by an org member | order_type, lot_id, title, unit_price, quantity, total |
lot.sold | An auction lot closes sold — delivered to the winning org only | lot_id, title, hammer_price, status |
lot.unsold | An auction lot closes unsold — delivered to the high-bidding org, if any | lot_id, title, hammer_price, status |
custody.status_changed | A pickup request transitions status | pickup_id, reference_code, status, changed_at |
webhook.test | You 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.
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | VIG-Webhooks/1.0 |
X-VIG-Event | The event name, e.g. order.paid |
X-VIG-Delivery | Delivery UUID — your dedupe key; delivery is at-least-once |
X-VIG-Signature | t=<unix seconds>,v1=<hex>[,v0=<hex>] |
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"
}
}{
"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.
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);
});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:
| Attempt | Scheduled | Elapsed (approx.) |
|---|---|---|
| 1 | On 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-Deliveryand treatoccurred_atas 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/ordersand/api/v1/custody-eventsto 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.
One envelope, six codes.
Every non-2xx response is { "error": { "code", "message" } }. Branch on code; message is human-readable and may change.
| HTTP | code | Meaning | What to do |
|---|---|---|---|
| 400 | invalid_request | Malformed query parameter or cursor | Fix the request; do not retry unchanged. |
| 401 | unauthorized | Missing or unknown API key | Check the Authorization header and the key value. |
| 403 | forbidden | Key revoked or expired (no further detail) | Create or rotate a key in the portal. |
| 404 | not_found | No such resource within your org scope | Existence is never confirmed across orgs. |
| 429 | rate_limited | Over the per-key (or per-IP) request rate | Wait Retry-After seconds, then back off. |
| 503 | api_unavailable | API not yet enabled, or a temporary backend failure | Retry with backoff; if it persists, API access may not be enabled for your org yet. |
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.
| Date | Change |
|---|---|
| 2026-07 | v1 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.