Beta, invite only

BulkVault API

Show your BulkVault inventory on your own site, sell it with your own checkout, and keep stock in sync with eBay automatically.

Quickstart

  1. Request access in Settings > API. Once approved, create a key with the permissions you need.
  2. Keep the key on your server (env var). Never ship it to the browser.
  3. Fetch your cards: GET /api/v1/listings. Each entry is one physical copy with its own photos, condition and price.
  4. When your checkout takes payment, report it: POST /api/v1/orders. Those copies are marked sold and, depending on your setting, their eBay listings are ended.
  5. Add a webhook so your site hears when stock changes anywhere (eBay sales, in-person sales, edits).
curl https://bulkvault.app/api/v1/listings?limit=20 \
  -H "Authorization: Bearer $BULKVAULT_API_KEY"

Limits and errors

120 requests per minute per key. Every response includes X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; a 429 also has Retry-After. Errors are { "error": { "code", "message" } }.

Selling with Stripe

Report the order from your Stripe webhook. Use the Checkout Session id as external_order_id: retries are safe and never sell twice. If any copy is gone you get a 409 and nothing is marked sold.

// Your Stripe webhook handler, after verifying Stripe's signature
if (event.type === 'checkout.session.completed') {
  const session = event.data.object
  // You put the BulkVault listing ids in metadata when creating the session
  const items = JSON.parse(session.metadata.bulkvault_items) // [{ listing_id, price }]

  const res = await fetch('https://bulkvault.app/api/v1/orders', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.BULKVAULT_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ external_order_id: session.id, items }),
  })
  if (res.status === 409) {
    // Someone bought it elsewhere first: refund this session
  }
}

Verifying webhooks

Each delivery has a BulkVault-Signature header, t=<unix>,v1=<hex>, an HMAC-SHA256 of `${t}.${rawBody}` with your endpoint secret. Failed deliveries are retried up to 3 times.

import crypto from 'node:crypto'

// rawBody: the exact request body string (don't re-serialize parsed JSON)
export function verifyBulkVault(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
  const t = Number(parts.t)
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex')
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ''))
}

Reference

Raw spec: /api/v1/openapi.json