Skip to main content

Authentication

The Media Agency API authenticates every request with a single long-lived API key presented as a Bearer token. 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 48 characters of URL-safe base64 following a fixed three-part prefix that tells you at a glance which environment the key belongs to:

ma_live_ak_01HXA7N0VKRR52TE3J5Z0C4QTS
ma_test_ak_01HXA7N0VKTESTTESTTESTTESTT
PrefixEnvironmentNotes
ma_live_ak_...ProductionRoutes real listings, real money, real webhooks.
ma_test_ak_...SandboxMints against the sandbox agency; safe for CI/integration runs.

The 26 characters after the prefix are a ULID (Crockford base32). 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 Authorization header using the HTTP Bearer scheme:

Authorization: Bearer ma_live_ak_01HXA7N0VKRR52TE3J5Z0C4QTS

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, a cookie, or the request body; the Bearer header is the only accepted transport.

Requests that omit the header return 401 auth.missing_key. Requests whose key is unrecognised, revoked, or past the expiry timestamp return 401 auth.invalid_key.

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 auth.invalid_key.

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 "Authorization: Bearer $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={
"Authorization": f"Bearer {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: {
Authorization: `Bearer ${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());

Two-host reminder

Every example above uses https://valara.cloud as the base URL. The legacy host https://dash.jacoballenmedia.com also serves the API until the migration completes; any integration pinned to the legacy hostname continues to work without code changes. New integrations should target the primary host.

Further reading