Vault APIVault · Developer docs
The Vault REST API exposes your organisation's sites, spaces, customers, agreements, invoices, payments, leads and reservations as JSON. Base URL /api/v1. Money is always integer pence (*Minor fields); dates are ISO 8601 strings; enums are uppercase strings.
Mint keys in Settings → API & Webhooks. A key is shown once at creation; store it somewhere safe. Send it as a bearer token on every request:
curl https://<your-vault-host>/api/v1/sites \ -H "Authorization: Bearer vk_…"
Keys carry read and/or write scopes: GET endpoints need read, POST endpoints need write.
Failures return a consistent envelope:
{ "error": { "code": "NOT_FOUND", "message": "Space not found." } }| Status | Code | Meaning |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or revoked API key. |
| 403 | FORBIDDEN | The key lacks the required scope. |
| 404 | NOT_FOUND | Unknown id, or an id belonging to another organisation. |
| 422 | VALIDATION | The request body failed validation; the message lists what is wrong. |
List endpoints accept ?limit (default 50, max 200) and ?offset (default 0), and return an envelope with the total row count:
{
"data": [ … ],
"total": 137,
"limit": 50,
"offset": 0
}All paths are relative to /api/v1 and only ever return data belonging to the key's organisation.
| Method | Path | Description | Scope |
|---|---|---|---|
| GET | /sites | List sites. | read |
| GET | /spaces | List spaces. Filters: siteId, status, spaceTypeId. | read |
| GET | /spaces/{id} | Fetch one space. | read |
| GET | /customers | List customers. Filter: q (name/email/company contains). | read |
| GET | /customers/{id} | Fetch one customer. | read |
| GET | /agreements | List rental agreements. Filters: siteId, status. | read |
| GET | /agreements/{id} | Fetch one agreement. | read |
| GET | /invoices | List invoices. Filters: siteId, status, customerId. | read |
| GET | /invoices/{id} | Fetch one invoice, including its lines. | read |
| GET | /payments | List payments. Filters: status, customerId. | read |
| GET | /leads | List leads. Filter: status. | read |
| POST | /leads | Create a lead (status NEW). Body: source, name, email?, phone?, siteId?, desiredSpaceTypeId?, notes?. Fires lead.created. | write |
| GET | /reservations | List reservations. Filter: status. | read |
| POST | /reservations | Hold an AVAILABLE space (flips it to RESERVED). Body: spaceId plus exactly one of customerId/leadId; optional expiresAt (ISO, default +14 days), depositMinor (integer pence). | write |
| GET | /availability | Available-space counts per space type, with up to 50 bookable spaces each and a live pricing block (list rate, rule-applied current rate, weekly/monthly gross, promotion hint). Filters: siteId, spaceTypeId. | read |
GET /availability includes a live pricing block per space type. Rates are monthly net pence unless stated; currentRateMinor is the list rate with the site's active revenue-management rules applied (at most one rule, rounded to 50p), and weekly/monthly carry the VAT-inclusive gross a customer would see on the storefront. The promotion is a hint; checkout computes exact totals.
{
"data": [{
"spaceTypeId": "…", "name": "50 sq ft unit", "siteId": "…",
"availableCount": 4,
"available": [{ "id": "…", "code": "A-12", "label": "…" }, …],
"pricing": {
"baseRateMinor": 12000, // monthly list rate, net of VAT
"currentRateMinor": 12600, // with active pricing rules applied
"rule": { // null when no rule fires
"ruleId": "…", "direction": "UP", "adjustmentPct": 5,
"occupancyPct": 92, "occupancyThresholdPct": 85
},
"vatRate": 0.2,
"weekly": { "netMinor": 2908, "vatMinor": 582, "grossMinor": 3490 },
"monthly": { "netMinor": 12600, "vatMinor": 2520, "grossMinor": 15120 },
"promotion": { // null when nothing applies today
"code": "FIRST50", "description": "50% off your first month",
"discountPct": 50, "amountMinor": null,
"firstPeriodDiscountMinor": 6300,
"validFrom": "2026-08-01T00:00:00.000Z", "validTo": null
},
"pricingMode": "FLAT", // PER_METRE prices by vehicle length;
"ratePerMetreMinor": null // baseRateMinor is then the "from" rate
}
}]
}Subscribe HTTPS endpoints in Settings → API & Webhooks. Vault POSTs a JSON envelope to every active subscription of the event:
{
"id": "9b1d…", // unique delivery id (uuid)
"event": "lead.created",
"createdAt": "2026-08-05T12:00:00.000Z",
"data": { … } // the affected object, same shape as the API
}Each subscription has a secret (whsec_…). Every delivery carries an X-Vault-Signature header: sha256= plus the HMAC-SHA256 hex of the exact raw request body under your secret. Always compare against the raw body, before JSON parsing:
import { createHmac, timingSafeEqual } from 'node:crypto';
function verifyVaultSignature(secret, rawBody, header) {
const expected =
'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(header ?? '');
return a.length === b.length && timingSafeEqual(a, b);
}
// Express: app.post('/hooks/vault', express.raw({ type: 'application/json' }),
// (req, res) => {
// if (!verifyVaultSignature(process.env.VAULT_WEBHOOK_SECRET,
// req.body, req.get('X-Vault-Signature'))) return res.sendStatus(401);
// const event = JSON.parse(req.body.toString('utf8'));
// ...
// });Deliveries time out after 5 seconds and are not retried; the recent-attempts log for each endpoint is visible in Settings → API & Webhooks.