Loc It UpVault API

Vault · Developer docs

Public API v1

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.

Authentication

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.

Errors

Failures return a consistent envelope:

{ "error": { "code": "NOT_FOUND", "message": "Space not found." } }
StatusCodeMeaning
401UNAUTHORIZEDMissing, invalid or revoked API key.
403FORBIDDENThe key lacks the required scope.
404NOT_FOUNDUnknown id, or an id belonging to another organisation.
422VALIDATIONThe request body failed validation; the message lists what is wrong.

Pagination

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
}

Endpoints

All paths are relative to /api/v1 and only ever return data belonging to the key's organisation.

MethodPathDescriptionScope
GET/sitesList sites.read
GET/spacesList spaces. Filters: siteId, status, spaceTypeId.read
GET/spaces/{id}Fetch one space.read
GET/customersList customers. Filter: q (name/email/company contains).read
GET/customers/{id}Fetch one customer.read
GET/agreementsList rental agreements. Filters: siteId, status.read
GET/agreements/{id}Fetch one agreement.read
GET/invoicesList invoices. Filters: siteId, status, customerId.read
GET/invoices/{id}Fetch one invoice, including its lines.read
GET/paymentsList payments. Filters: status, customerId.read
GET/leadsList leads. Filter: status.read
POST/leadsCreate a lead (status NEW). Body: source, name, email?, phone?, siteId?, desiredSpaceTypeId?, notes?. Fires lead.created.write
GET/reservationsList reservations. Filter: status.read
POST/reservationsHold 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/availabilityAvailable-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

Availability pricing

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
    }
  }]
}

Webhooks

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
}

Events

Verifying signatures

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.