Apenas por conviteO RunAV é atualmente apenas por convite. Precisa de um convite para se juntar.

API do Website

Alimente o seu próprio website com o catálogo de equipamento, disponibilidade, estimativas de preço, pedidos de aluguer e reservas diretas em tempo real. O catálogo e a disponibilidade são só de leitura; também pode submeter pedidos de aluguer e criar reservas — tudo restrito ao seu workspace por uma chave de API.

Início rápido

Tudo o que precisa para começar a integrar o seu catálogo no seu site.

  1. No RunAV, abra Definições → API para Programadores e defina o slug da sua loja.
  2. Crie uma chave de API e copie-a (é mostrada apenas uma vez).
  3. Chame a API com a sua chave no cabeçalho X-Api-Key, usando o seu slug no caminho.

URL base (substitua {slug} pelo slug da sua loja):

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

Autenticação

Cada pedido (exceto as imagens do catálogo) deve incluir a sua chave secreta no cabeçalho X-Api-Key. As chaves são criadas e revogadas em Definições → API para Programadores.

X-Api-Key: rfk_live_xxxxxxxxxxxxxxxxxxxxxxxx

As chaves estão associadas ao seu workspace e plano. Se o seu plano deixar de incluir acesso à API, ou se uma chave for revogada, os pedidos devolvem 401/403. Nunca exponha uma chave em código de cliente que não controla — pode submeter pedidos de aluguer em seu nome.

Âmbitos

Cada chave de API tem âmbitos que limitam o que pode fazer. Conceda apenas o que a integração precisa.

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

Endpoints

O array items

Os endpoints /quote, /rental-requests e /bookings recebem items como um array de { "model": string, "quantity": number }. O model tem de ser um valor de GET /catalog — o seu model_key (recomendado) ou o seu name exato. Outros nomes de chave (name, product, sku, qty) ou strings simples não são reconhecidos. A quantity é opcional e limitada a 1–999 (predefinição 1); o array está limitado a 100 artigos. Um kit reserva-se da mesma forma — passe o seu model_key como model; um kit é um conjunto único, por isso a sua quantity é sempre tratada como 1. Se um nome for partilhado por um modelo de equipamento e um kit, o modelo de equipamento prevalece; adicione "type": "kit" (ou "equipment") a um artigo para forçar qual deles é resolvido.

Atenção: em /rental-requests e /bookings, qualquer artigo com um model em falta, vazio ou com o nome de chave errado é descartado silenciosamente — o pedido devolve success na mesma, mas guarda uma lista de artigos vazia. Construa items a partir dos valores model_key/name de GET /catalog e confirme que o pedido guardado mostra as suas linhas. Em /quote, os modelos não reconhecidos vêm como linhas unavailable, por isso a discrepância é visível.

GET/catalog

Devolve o seu equipamento publicado, uma entrada por modelo, com um URL de imagem e uma tarifa diária ou um indicador de preço sob consulta. O campo category é um array ordenado da raiz até à folha (por exemplo ["Video", "Camera"]), ou null. O stock aparece apenas como um booleano in_stock, nunca como uma contagem de unidades. O campo availability_display reflete o GET /availability: quando a exibição de disponibilidade está definida como oculta, é "hidden" e o indicador in_stock é omitido de todas as entradas. Os serviços publicados são anexados quando ativados. Os kits publicados (conjuntos) também são anexados, cada um com "type": "kit" e sem indicador in_stock (verifique a disponibilidade de um kit via GET /availability). Um kit inclui também um array items com o seu conteúdo agrupado por modelo — [{ name, brand, model, quantity, image_url }] — para poder mostrar o que está dentro do conjunto (seguro para o público: apenas nomes, quantidades e imagens dos itens, nunca números de série ou preços por unidade). O image_url de um kit é a sua imagem carregada, se tiver uma; caso contrário, um mosaico 2x2 que a API compõe a partir das imagens dos primeiros itens (imagem própria, senão mosaico, senão null) — utilizável como um único img ou og:image. Um kit pode ser definido para excluir as suas unidades, caso em que essas unidades deixam de aparecer como entradas individuais — mas quaisquer unidades extra do mesmo modelo que não estejam no kit continuam a aparecer.

O campo description é escrito em Markdown (negrito, itálico, títulos, listas numeradas e com marcadores). É devolvido tal como foi escrito, por isso apresente-o através do seu próprio renderizador de Markdown e sanitize o resultado antes de o mostrar.

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

Resposta

{
  "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=MODELSem chave

Serve a imagem de um modelo, estritamente para o (name, model) exato — não há fallback ao nível do nome, por isso dois modelos com o mesmo nome nunca partilham imagem. Pública (sem chave) para a poder incorporar diretamente numa tag <img>; só são servidos modelos publicados. Prefira o image_url devolvido por GET /catalog (já é o URL correto por modelo) em vez de construir este URL manualmente; é null quando esse modelo específico não tem imagem.

<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

Devolve um estado de disponibilidade por modelo (disponível, limitado ou indisponível) num intervalo de datas, usando a mesma lógica de reservas do agendador da aplicação. As contagens de unidades nunca são expostas. Opcionalmente, filtre para um modelo com o parâmetro de consulta model.

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

Resposta

{
  "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

Devolve o contrato do formulário de pedido: que campos cada tipo de requerente (particular ou empresa) tem de enviar, a configuração de mensagem/termos, e a oferta de entrega com a sua restrição geográfica (whitelist ou blacklist de países e cidades). Leia-o primeiro para que o seu formulário só ofereça locais servidos e valide antes de submeter.

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

Resposta

{
  "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

Devolve uma estimativa de preço não vinculativa para uma seleção de modelos num intervalo de datas, mais quaisquer violações de regras de aluguer. Os modelos com preço oculto são marcados como 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

Resposta

{
  "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

Submete um pedido de aluguer. Aparece na sua caixa de entrada de Pedidos de Aluguer para revisão e confirmação. O requerente submete como particular (nome, email, telefone e NIF obrigatórios) ou empresa (adicionalmente o nome, NIF e morada da empresa; cada morada é { country, city, address, postal_code } com país ISO alpha-2). Um bloco delivery opcional pede entrega nessa morada (validada contra a restrição de entrega — ver GET /request-form); omita-o para levantamento no armazém. Validado contra as regras da sua storefront e com limite de taxa.

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

Resposta

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

Em caso de recusa (400)

A validação usa as regras da sua storefront. Um pedido recusado devolve 400 com um array rule_violations — cada entrada tem um code (e uma mensagem legível). Códigos possíveis: 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. Os códigos de disponibilidade (exceeds_availability, unavailable_item) só se aplicam quando start e end são fornecidos. Uma storefront fechada devolve 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

Criar uma reserva diretamente: revalida a disponibilidade, calcula o preço e cria um trabalho. O bloco contact segue o mesmo contrato de identidade do requerente que /rental-requests (type, phone e vat_number obrigatórios; empresas enviam também company + company_address), e um bloco delivery opcional de topo é validado da mesma forma — uma reserva com entrega coloca a morada na localização do trabalho. Devolve 400 com rule_violations para problemas de identidade/entrega, e 409 com um array shortfalls se os artigos deixarem de estar disponíveis. Envie um cabeçalho Idempotency-Key para que as repetições não dupliquem. Requer o âmbito bookings:write.

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

Resposta

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

Ler o estado atual de uma reserva. Requer o âmbito bookings:read.

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

Resposta

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

Webhooks

Subscreva um URL (em Definições → API para Programadores) a eventos. Enviamos um payload JSON assinado (POST) quando cada evento ocorre, com repetições automáticas.

Eventos

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

Cada pedido inclui os cabeçalhos Webhook-Id, Webhook-Timestamp e Webhook-Signature. A assinatura é HMAC-SHA256 em base64 sobre `{id}.{timestamp}.{rawBody}` usando o segredo de assinatura do seu endpoint.

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": { … } }

Verificar uma assinatura

// 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));

Respostas não-2xx são repetidas com recuo exponencial (até 6 tentativas). Responda 2xx rapidamente e faça o trabalho pesado de forma assíncrona.

Limites e erros

Os erros devolvem um corpo JSON com um único campo de mensagem:

{ "error": "A human-readable message" }
400Pedido inválido — falta um campo obrigatório ou um parâmetro é inválido. As violações de regras de pedidos de aluguer são devolvidas num array rule_violations (ver o endpoint acima).
401Não autorizado — a chave de API está em falta, é inválida ou foi revogada.
403Proibido — o seu plano não inclui acesso à API, o slug não corresponde à chave, a chave não tem o âmbito necessário, ou foi atingido um limite do plano (por exemplo, reservas mensais).
404Não encontrado — a reserva não existe ou não foi criada através da API, ou uma imagem do catálogo não está disponível.
409Conflito — os artigos pedidos não estão disponíveis para essas datas (é devolvido um array shortfalls), ou uma reserva com o mesmo Idempotency-Key ainda está a ser processada.
429Demasiados pedidos — o limite de pedidos de aluguer foi excedido. Tente novamente mais tarde.
500Erro de servidor — algo correu mal do nosso lado.

Os pedidos de aluguer estão limitados a cerca de 10 submissões por 10 minutos por chave e IP.

CORS

A API pública pode ser chamada a partir de qualquer origem no browser, por isso pode chamá-la diretamente do frontend do seu website. O cabeçalho X-Api-Key é a única credencial — não são usados cookies.