Invite onlyRunAV is currently invite-only. You'll need a workspace invitation to join.

Website API

Power your own website with your live equipment catalog, availability, price estimates, rental requests, and direct bookings. Catalog and availability are read-only; you can also submit rental requests and create bookings — all scoped to your workspace by an API key.

Quick start

Everything you need to start pulling your catalog into your own site.

  1. In RunAV, open Settings → Developer API and set your storefront slug.
  2. Create an API key and copy it (it is shown only once).
  3. Call the API with your key in the X-Api-Key header, using your slug in the path.

Base URL (replace {slug} with your storefront slug):

https://app.runav.io/api/public/v1/{slug}
curl -H "X-Api-Key: YOUR_KEY" https://app.runav.io/api/public/v1/{slug}/catalog

Authentication

Every request (except catalog images) must include your secret key in the X-Api-Key header. Keys are created and revoked from Settings → Developer API.

X-Api-Key: rfk_live_xxxxxxxxxxxxxxxxxxxxxxxx

Keys are tied to your workspace and plan. If your plan no longer includes API access, or a key is revoked, requests return 401/403. Never expose a key in client-side code you don't control — it can submit rental requests on your behalf.

Scopes

Each API key carries scopes that limit what it can do. Grant only what an integration needs.

catalog:read     read catalog, availability, quote
requests:write   submit rental requests
bookings:read    read a booking's status
bookings:write   create bookings

Endpoints

The items array

The /quote, /rental-requests and /bookings endpoints all take items as an array of { "model": string, "quantity": number }. model must be a value from GET /catalog — either its model_key (recommended) or its exact name. Other key names (name, product, sku, qty) or bare strings are not recognized. quantity is optional and clamped to 1–999 (default 1); the array is capped at 100 items. A kit is bookable the same way — pass its model_key as model; a kit is a single bundle, so its quantity is always treated as 1. If a name is shared by an equipment model and a kit, the equipment model wins; add "type": "kit" (or "equipment") to an item to force which one resolves.

Heads up: on /rental-requests and /bookings, any item with a missing, blank, or wrongly-named model is silently dropped — the request still returns success but stores an empty item list. Build items from GET /catalog model_key/name values and confirm the saved request shows your lines. On /quote, unrecognized models come back as unavailable lines instead, so the mismatch is visible.

GET/catalog

Returns your published equipment, one entry per model, with an image URL and either a daily rate or a price-on-request flag. The category field is an ordered array from root to leaf (e.g. ["Video", "Camera"]), or null. Stock shows only as an in_stock boolean, never a unit count. The availability_display field mirrors GET /availability: when availability display is set to hidden it is "hidden" and the in_stock flag is omitted from every entry. Published services are appended when enabled. Published kits (bundles) are appended too, each carrying "type": "kit" and no in_stock flag (check a kit's availability via GET /availability). A kit also carries an items array listing its contents grouped by model — [{ name, brand, model, quantity, image_url }] — so you can show what's inside the bundle (public-safe: names, quantities and member images only, never serials or per-unit prices). A kit's image_url is its uploaded image if it has one, otherwise a 2x2 mosaic the API composes from its first member images (own image, else mosaic, else null) — usable as a single img or og:image. A kit can be set to exclude its member units, in which case those units stop appearing as individual entries — but any extra units of the same model that are not in the kit still appear.

The description field is authored in Markdown (bold, italics, headings, numbered and bulleted lists). It is returned verbatim, so render it through your own Markdown renderer and sanitize the output before displaying it.

curl -H "X-Api-Key: YOUR_KEY" \
  https://app.runav.io/api/public/v1/{slug}/catalog

Response

{
  "storefront_enabled": true,
  "availability_display": "status",
  "models": [
    {
      "model_key": "shure-sm58--sm58-lc",
      "name": "Shure SM58",
      "brand": "Shure",
      "model": "SM58-LC",
      "category": ["Audio", "Microphones"],
      "description": "**Dynamic** vocal microphone — *industry standard* for live vocals.",
      "tags": ["vocal", "wired"],
      "featured": false,
      "in_stock": true,
      "daily_rate": 12.5,
      "price_display": "exact",
      "image_url": "https://app.runav.io/api/public/v1/{slug}/catalog/image?name=Shure%20SM58&model=SM58-LC"
    },
    {
      "model_key": "meyer-panther--panther-m",
      "name": "Meyer Panther",
      "brand": "Meyer Sound",
      "model": "PANTHER-M",
      "category": ["Audio", "Line Arrays"],
      "description": "Large-format line array",
      "tags": ["touring"],
      "featured": true,
      "in_stock": true,
      "price_on_request": true,
      "image_url": null
    },
    {
      "model_key": "video-summary",
      "type": "service",
      "name": "Video Summary",
      "category": ["Production", "Video"],
      "description": "Same-day highlights edit delivered within 48h.",
      "featured": true,
      "image_url": "https://app.runav.io/api/public/v1/{slug}/catalog/service-image?service=8f1e...c2",
      "price": 2300,
      "price_unit": "fixed"
    },
    {
      "model_key": "vocal-wireless-pack",
      "type": "kit",
      "name": "Vocal Wireless Pack",
      "category": ["Audio", "Packages"],
      "description": "Four-channel handheld wireless kit.",
      "featured": false,
      "daily_rate": 180,
      "price_display": "exact",
      "image_url": "https://app.runav.io/api/public/v1/{slug}/catalog/kit-image?kit=8f1e...c2",
      "items": [
        { "name": "Shure SM58", "brand": "Shure", "model": "SM58-LC", "quantity": 4, "image_url": "https://app.runav.io/api/public/v1/{slug}/catalog/image?name=Shure%20SM58&model=SM58-LC" },
        { "name": "Shure BLX88", "brand": "Shure", "model": "BLX88", "quantity": 1, "image_url": null }
      ]
    }
  ]
}
GET/catalog/image?name=NAME&model=MODELNo key required

Serves a model's image, strictly for the exact (name, model) — there is no name-level fallback, so two models that share a name never share an image. Public (no key) so you can embed it directly in an <img> tag; only published models are served. Prefer the image_url returned by GET /catalog (already the correct per-model URL) instead of building this URL yourself; it is null when that specific model has no image.

<img src="https://app.runav.io/api/public/v1/{slug}/catalog/image?name=Shure%20SM58&model=SM58-LC" />
GET/availability?start=YYYY-MM-DD&end=YYYY-MM-DD

Returns a per-model availability status (available, limited, or unavailable) for a date range, using the same booking logic as the in-app scheduler. Raw unit counts are never exposed. Optionally filter to one model with the model query parameter.

curl -H "X-Api-Key: YOUR_KEY" \
  "https://app.runav.io/api/public/v1/{slug}/availability?start=2026-07-01&end=2026-07-05"

Response

{
  "start": "2026-07-01",
  "end": "2026-07-05",
  "availability_display": "status",
  "models": [
    { "model_key": "shure-sm58--sm58-lc", "name": "Shure SM58", "model": "SM58-LC", "status": "available" },
    { "model_key": "meyer-panther--panther-m", "name": "Meyer Panther", "model": "PANTHER-M", "status": "limited" }
  ]
}
GET/request-form

Returns the request-form contract: which fields each requester type (individual or company) must send, the message/terms configuration, and the delivery offer with its geo restriction (whitelist or blacklist of countries and cities). Read it up front so your form only offers served places and validates before submitting.

curl -H "X-Api-Key: YOUR_KEY" \
  https://app.runav.io/api/public/v1/{slug}/request-form

Response

{
  "requester_types": ["individual", "company"],
  "required": {
    "individual": ["contact_name", "contact_email", "contact_phone", "vat_number"],
    "company": ["contact_name", "contact_email", "contact_phone", "company", "vat_number", "company_address"]
  },
  "message": "optional",
  "require_terms": false,
  "terms_text": {},
  "intro": {},
  "delivery": {
    "enabled": true,
    "restriction": { "mode": "whitelist", "zones": [{ "country": "PT" }, { "country": "ES", "city": "Madrid" }] }
  },
  "rules": { "requireDates": true, "leadTimeDays": 0, "maxAdvanceDays": null, "maxQtyPerModel": null, "maxItemsPerRequest": null, "blackoutDates": [] }
}
POST/quote

Returns a non-binding price estimate for a selection of models over a date range, plus any rental-rule violations. Models with hidden pricing are flagged price_on_request.

curl -X POST -H "X-Api-Key: YOUR_KEY" -H "Content-Type: application/json" \
  -d '{ "start": "2026-07-01", "end": "2026-07-05", "items": [{ "model": "shure-sm58--sm58-lc", "quantity": 2 }] }' \
  https://app.runav.io/api/public/v1/{slug}/quote

Response

{
  "estimate": true,
  "price_on_request": false,
  "days": 4,
  "currency": { "symbol": "€", "code": "EUR" },
  "lines": [{ "model": "Shure SM58", "quantity": 2, "daily_rate": 12.5 }],
  "subtotal": 100.0,
  "vat_rate": 23,
  "vat": 23.0,
  "total": 123.0,
  "rule_violations": []
}
POST/rental-requests

Submits a rental request. It appears in your in-app Rental Requests inbox for review and confirmation. The requester submits as an individual (name, email, phone and VAT number required) or a company (additionally the company name, VAT number and address, each address being { country, city, address, postal_code } with an ISO alpha-2 country). An optional delivery block asks for delivery to that address (validated against the delivery restriction — see GET /request-form); omit it for warehouse pickup. Validated against your storefront rules and rate limited.

curl -X POST -H "X-Api-Key: YOUR_KEY" -H "Content-Type: application/json" \
  -d '{ "requester_type": "company", "contact_name": "Jane Doe", "contact_email": "jane@example.com", "contact_phone": "+351 900 000 000", "vat_number": "PT123456789", "company": "Doe Productions", "company_address": { "country": "PT", "city": "Lisboa", "address": "Rua Exemplo 1", "postal_code": "1000-001" }, "delivery": { "country": "PT", "city": "Lisboa", "address": "Av. da Entrega 42", "postal_code": "1000-002" }, "message": "Need these for a weekend shoot.", "start": "2026-07-01", "end": "2026-07-05", "items": [{ "model": "shure-sm58--sm58-lc", "quantity": 2 }] }' \
  https://app.runav.io/api/public/v1/{slug}/rental-requests

Response

{ "success": true, "id": "…" }

On rejection (400)

Validation runs against your storefront rules. A rejected submission returns 400 with a rule_violations array — each entry has a code (and a human-readable message). Possible codes: requester_type_invalid, phone_required, vat_required, company_required, company_address_required, delivery_not_offered, delivery_address_required, delivery_not_served, message_required, terms_required, dates_required, below_lead_time, above_max_advance, blackout, too_many_items, qty_too_high, exceeds_availability, unavailable_item. The availability codes (exceeds_availability, unavailable_item) only apply when start and end are supplied. A closed storefront returns 403.

{
  "error": "Your request could not be submitted.",
  "rule_violations": [
    { "code": "vat_required", "field": "vat_number", "message": "A VAT number is required." },
    { "code": "delivery_not_served", "field": "delivery", "message": "Sorry — we don't deliver to Faro, Portugal." },
    { "code": "exceeds_availability", "model": "shure-sm58--sm58-lc", "requested": 5, "available": 2 }
  ]
}
POST/bookings

Create a booking directly: re-checks availability, prices it, and creates a job. The contact block follows the same requester-identity contract as /rental-requests (type, phone and vat_number required; companies also send company + company_address), and an optional top-level delivery block is validated the same way — a delivery booking lands its address on the job location. Returns 400 with rule_violations for identity/delivery problems, and 409 with a shortfalls array if items are no longer available. Send an Idempotency-Key header so retries do not duplicate. Requires the bookings:write scope.

curl -X POST -H "X-Api-Key: YOUR_KEY" -H "Idempotency-Key: order-123" -H "Content-Type: application/json" \
  -d '{ "start": "2026-07-01", "end": "2026-07-05", "items": [{ "model": "shure-sm58--sm58-lc", "quantity": 2 }], "contact": { "type": "individual", "name": "Jane Doe", "email": "jane@example.com", "phone": "+351 900 000 000", "vat_number": "PT123456789" }, "delivery": null }' \
  https://app.runav.io/api/public/v1/{slug}/bookings

Response

{
  "success": true,
  "id": "…",
  "client_id": "…",
  "status": "pending",
  "total": 123.0,
  "currency": { "symbol": "€", "code": "EUR" },
  "days": 4
}
GET/bookings/:id

Read a booking current status. Requires the bookings:read scope.

curl -H "X-Api-Key: YOUR_KEY" https://app.runav.io/api/public/v1/{slug}/bookings/JOB_ID

Response

{ "id": "…", "status": "pending", "start_date": "2026-07-01", "end_date": "2026-07-05", "total": 100.0 }

Webhooks

Subscribe a URL (in Settings → Developer API) to events. We POST a signed JSON payload when each event happens, with automatic retries.

Events

rental_request.created
booking.created
booking.confirmed
quote.accepted

Each request carries Webhook-Id, Webhook-Timestamp and Webhook-Signature headers. The signature is base64 HMAC-SHA256 over `{id}.{timestamp}.{rawBody}` using your endpoint signing secret.

POST  https://your-app.example.com/webhooks/runav
Webhook-Id:         <delivery id>
Webhook-Timestamp:  <unix seconds>
Webhook-Signature:  v1,<base64 HMAC-SHA256>
Content-Type:       application/json

{ "id": "…", "type": "booking.created", "created": 1730000000, "data": { … } }

Verify a signature

// Node — verify the signature
import { createHmac, timingSafeEqual } from 'crypto';

const secret = process.env.RUNAV_WEBHOOK_SECRET.replace(/^whsec_/, '');
const signed = `${webhookId}.${webhookTimestamp}.${rawBody}`;
const expected = createHmac('sha256', Buffer.from(secret, 'base64')).update(signed).digest('base64');
const provided = signatureHeader.split(',')[1]; // after "v1,"
const ok = timingSafeEqual(Buffer.from(expected), Buffer.from(provided));

Non-2xx responses are retried with exponential backoff (up to 6 attempts). Respond 2xx quickly and do heavy work asynchronously.

Rate limits & errors

Errors return a JSON body with a single message field:

{ "error": "A human-readable message" }
400Bad request — a required field or query parameter is missing or invalid. Rental-request rule violations are returned as a rule_violations array (see the endpoint above).
401Unauthorized — the API key is missing, invalid, or revoked.
403Forbidden — your plan doesn't include API access, the slug doesn't match the key, the key lacks the required scope, or a plan limit (e.g. monthly bookings) was reached.
404Not found — the booking doesn't exist or wasn't created via the API, or a catalog image is unavailable.
409Conflict — the requested items aren't available for those dates (a shortfalls array is returned), or a booking with the same Idempotency-Key is still being processed.
429Too many requests — the rental-request rate limit was exceeded. Try again later.
500Server error — something went wrong on our side.

Rental requests are limited to roughly 10 submissions per 10 minutes per key and IP.

CORS

The public API is browser-callable from any origin, so you can call it directly from your website's frontend. The X-Api-Key header is the only credential — no cookies are used.