Skip to main content

Authentication

The Media Agency API authenticates every request with a single long-lived API key presented on the X-API-Key request header. There are no OAuth dance steps; the key IS the credential. Keys are minted from the dashboard, carry a bcrypt hash on the server side, and can be rotated on a 30-day grace schedule when your systems need to cut over.

Key format

Keys are a fixed environment prefix followed by a random URL-safe token. The prefix tells you at a glance which environment the key belongs to:

sk_live_01HXA7N0VKRR52TE3J5Z0C4QT  <!-- pragma: allowlist secret -->
sk_test_01HXA7N0VKRR52TE3J5Z0C4QT <!-- pragma: allowlist secret -->

The strings above are structural placeholders, not real keys. Every ULID shown in this reference is a 26-character Crockford base32 value matching [0-9A-HJKMNP-TV-Z]{26}. Real keys use a random URL-safe base64 suffix rather than a ULID, but the placeholder shape is used throughout the doc for visual consistency with the other identifiers. An integrator who captures one of these values from the docs and tries it against the API will be rejected by the key lookup.

PrefixEnvironmentNotes
sk_live_...ProductionRoutes real listings, real money, real webhooks.
sk_test_...SandboxMints against the sandbox agency; safe for CI/integration runs.

The server stores only a bcrypt hash (cost 12) plus a SHA-256 fallback hash during the 30-day grace window after rotation. The plaintext key is shown exactly once: at mint time. Losing it means minting a new key; there is no recovery path.

Sending the key

Every request MUST present the key on the X-API-Key request header:

X-API-Key: sk_live_01HXA7N0VKRR52TE3J5Z0C4QT

TLS is mandatory. Plain HTTP requests are rejected at the load balancer before they reach the API. Never embed the key in the URL, a query string, or the request body; X-API-Key is the only accepted transport for programmatic callers.

Requests that omit the header return 401 api_key_missing. Requests whose key is unrecognised, revoked, or past the expiry timestamp return 401 api_key_invalid.

Dashboard exception: POST /api/v1/media/upload

The media-upload pre-signer is reached from two caller contexts: programmatic integrators and the dashboard UI. To avoid making the dashboard juggle a separate credential in the browser, this one endpoint additionally accepts a browser session cookie as an alternative to X-API-Key. Precedence when both are present: the session cookie wins so a logged-in dashboard user cannot accidentally charge another agency's key budget.

POST /api/v1/media/upload
X-API-Key: sk_live_01HXA7N0VKRR52TE3J5Z0C4QT
Content-Type: application/json
X-Idempotency-Key: 9f3a8f4d-5d28-4f2f-9f86-1d6a6e2a2e4b

For programmatic integrations, always send X-API-Key - including on /media/upload. The session-cookie path exists solely for the first-party dashboard UI.

Idempotency-Key header

Every mutating request (POST, PATCH) MUST carry an X-Idempotency-Key header. The server caches the full response for 24 hours keyed on (agency_id, idempotency_key); a retry with the same key returns the cached response unchanged and flags the repeat delivery with X-Idempotent-Replay: true.

Keys are opaque strings of at most 128 characters. UUIDv4 is the recommended format:

X-Idempotency-Key: 9f3a8f4d-5d28-4f2f-9f86-1d6a6e2a2e4b

Retrying with the same key but a different body returns 409 idempotency_key_conflict so silent data drift is impossible. If the first call is still in flight, a retry returns 409 idempotency_key_in_progress with a Retry-After: 5 header. Omitting the header on a mutating request returns 400 idempotency.missing.

Safe methods (GET, HEAD, OPTIONS) never inspect the header; the platform treats them as inherently idempotent.

Rate-limit response headers

Every response from a rate-limited endpoint carries three headers so integrators can tune retry behaviour without a second lookup:

HeaderMeaning
X-Rate-Limit-LimitMax requests permitted in the current window.
X-Rate-Limit-RemainingRequests still available before the next 429.
X-Rate-Limit-ResetUnix epoch seconds when the window refills.

On a 429 rate_limit.exceeded response the API also emits Retry-After with the number of seconds to wait. The full per-endpoint budget table is on the rate limits page.

Rotation with 30-day grace

Use rotation (not revocation) when your systems need to cut a key over without downtime. The flow:

  1. From the dashboard, click Rotate next to the key. A new plaintext secret is returned and the old key is flagged rotation_started_at.
  2. Both the old and new plaintext values authenticate for the next 30 days. Deploy the new key to every consumer during this window.
  3. When the rotation window expires, the old key is automatically deactivated by the nightly sweep task. Any remaining consumer still presenting the old secret starts receiving 401 api_key_invalid.

If a key is suspected compromised, use Revoke instead of rotation: revocation disables the key immediately with no grace window. Revoked keys cannot be restored; mint a new one.

Code samples

curl

curl -X POST "https://valara.cloud/api/v1/listings" \
-H "X-API-Key: $VALARA_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: $(uuidgen)" \
-d '{
"address": "123 Main St, Portland, OR 97201",
"owner_user_id": "user_01HXAGENCYOWNER",
"agent_one_id": "user_01HXAGENTONE"
}'

Python

import os
import uuid
import httpx

API_KEY = os.environ["VALARA_API_KEY"]
BASE_URL = "https://valara.cloud/api/v1"

response = httpx.post(
f"{BASE_URL}/listings",
headers={
"X-API-Key": API_KEY,
"Content-Type": "application/json",
"X-Idempotency-Key": str(uuid.uuid4()),
},
json={
"address": "123 Main St, Portland, OR 97201",
"owner_user_id": "user_01HXAGENCYOWNER",
"agent_one_id": "user_01HXAGENTONE",
},
timeout=10.0,
)
response.raise_for_status()
print(response.json())

Node (ESM only, per CLAUDE.md rule 2)

import { randomUUID } from "node:crypto";

const API_KEY = process.env.VALARA_API_KEY;
const BASE_URL = "https://valara.cloud/api/v1";

const response = await fetch(`${BASE_URL}/listings`, {
method: "POST",
headers: {
"X-API-Key": API_KEY,
"Content-Type": "application/json",
"X-Idempotency-Key": randomUUID(),
},
body: JSON.stringify({
address: "123 Main St, Portland, OR 97201",
owner_user_id: "user_01HXAGENCYOWNER",
agent_one_id: "user_01HXAGENTONE",
}),
});

if (!response.ok) {
throw new Error(`Valara API error: ${response.status} ${await response.text()}`);
}
console.log(await response.json());

Further reading