Agent Layer API Reference

Machine-readable contract notes for agents wiring against Phase-A API keys and Phase-B journey APIs.

Base URLs

Use the edge gateway for tenant data-plane calls:

{apiBaseUrl}/t/{tenantSlug}/v1/...

Use the identity worker for API-client administration:

{identityBaseUrl}/t/{tenantSlug}/admin/api-clients...

Tenant custom domains may omit /t/{tenantSlug} when the edge gateway can resolve the tenant from the host. Agent code should prefer the explicit tenant path unless operating on a tenant domain.

Authentication

API-key data-plane calls:

Authorization: Bearer vyg_live_xxx
Accept: application/json
Content-Type: application/json

The edge gateway also accepts X-API-Key: vyg_live_xxx, but the SDK uses Authorization: Bearer.

API client and key administration requires a live identity browser/session cookie for a tenant owner, tenant admin, or platform admin. API keys cannot mint or rotate other API keys.

API Key Format And Scopes

Accepted key formats:

vyg_live_{8-char-prefix}_{secret}
vyg_test_{8-char-prefix}_{secret}

Phase-A key scopes:

["journey.read", "journey.build", "registration.write"]

Current worker behavior:

  • /v1/journeys* admin routes require journey.build for API-client actors.
  • journey.read exists in the scope catalog but is not yet a separate read-only journey route gate.
  • /v1/journeys/public/* routes are public tenant routes so browser registration can work. Server agents should still send an API key with registration.write for identity, rate-limit dimensioning, and future fail-closed compatibility.

Error Envelope

Most data-plane errors:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid journey create payload",
    "details": {}
  }
}

Edge auth errors include a request ID inside the error object:

{
  "error": {
    "code": "AUTH_INVALID",
    "message": "The provided authentication token is invalid",
    "requestId": "req_..."
  }
}

Identity admin errors include requestId at top level:

{
  "error": {
    "code": "invalid_scopes",
    "message": "At least one valid API scope is required."
  },
  "requestId": "req_..."
}

Rate Limits

  • API-key authentication attempts: 60 attempts per 60 seconds by key prefix, with malformed keys grouped by IP.
  • Gateway default route profile: 100 requests per 60 seconds.
  • Once a key is verified, downstream gateway rate-limit keys include tenant, API client ID, and key prefix.
  • Responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset.

Identity Admin API

All routes require identity session cookies and api_client.manage.

Create API Client

POST /t/{tenantSlug}/admin/api-clients
Content-Type: application/json

Request:

{
  "name": "Agent builder",
  "description": "Server-side journey builder automation"
}

Response 201:

{
  "client": {
    "id": "client_00000000-0000-0000-0000-000000000000",
    "tenantId": "tenant_123",
    "name": "Agent builder",
    "description": "Server-side journey builder automation",
    "createdBy": "user_123",
    "status": "active",
    "createdAt": "2026-06-05T00:00:00.000Z",
    "updatedAt": "2026-06-05T00:00:00.000Z"
  }
}

List API Clients

GET /t/{tenantSlug}/admin/api-clients?page=1&limit=50

Response:

{
  "clients": [],
  "pagination": {
    "page": 1,
    "limit": 50,
    "total": 0,
    "hasMore": false
  }
}

Read API Client

GET /t/{tenantSlug}/admin/api-clients/{clientId}

Response:

{ "client": {} }

Update API Client

PATCH /t/{tenantSlug}/admin/api-clients/{clientId}
Content-Type: application/json

Request:

{
  "name": "Agent builder",
  "description": "Journey automation and registration capture",
  "status": "active"
}

Response:

{ "client": {} }

Disable API Client

POST /t/{tenantSlug}/admin/api-clients/{clientId}/disable

Response:

{ "client": { "status": "disabled" } }

Mint API Key

POST /t/{tenantSlug}/admin/api-clients/{clientId}/keys
Content-Type: application/json

Request:

{
  "scopes": ["journey.build", "registration.write"],
  "expiresAt": "2026-09-03T00:00:00.000Z"
}

Response 201:

{
  "key": {
    "id": "key_00000000-0000-0000-0000-000000000000",
    "clientId": "client_00000000-0000-0000-0000-000000000000",
    "tenantId": "tenant_123",
    "keyPrefix": "vyg_live_AbCdEf12",
    "scopes": ["journey.build", "registration.write"],
    "lastUsedAt": null,
    "expiresAt": "2026-09-03T00:00:00.000Z",
    "revokedAt": null,
    "createdAt": "2026-06-05T00:00:00.000Z"
  },
  "secret": "vyg_live_xxx"
}

secret is returned once. It is never returned by list, read, update, revoke, or audit responses.

List Keys

GET /t/{tenantSlug}/admin/api-clients/{clientId}/keys

Response:

{ "keys": [] }

Update Key

PATCH /t/{tenantSlug}/admin/api-clients/{clientId}/keys/{keyId}
Content-Type: application/json

Request:

{
  "scopes": ["journey.build"],
  "expiresAt": null
}

Response:

{ "key": {} }

expiresAt: null makes a key non-expiring only through explicit admin update. Mint requests default to 90 days and cannot exceed 365 days.

Revoke Key

POST /t/{tenantSlug}/admin/api-clients/{clientId}/keys/{keyId}/revoke

Response:

{ "key": { "revokedAt": "2026-06-05T00:00:00.000Z" } }

Rotate Key

POST /t/{tenantSlug}/admin/api-clients/{clientId}/keys/{keyId}/rotate
Content-Type: application/json

Request:

{
  "scopes": ["journey.build", "registration.write"],
  "expiresAt": "2026-09-03T00:00:00.000Z"
}

Response 201:

{
  "revokedKey": {},
  "key": {},
  "secret": "vyg_live_xxx"
}

Journey Admin API

All routes below are edge gateway routes and return { "data": ... } unless noted. Current API-client scope requirement: journey.build.

List Journeys

GET /t/{tenantSlug}/v1/journeys?page=1&limit=50&type=event&status=live
Authorization: Bearer vyg_live_xxx

Query:

{
  "page": "integer >= 1, default 1",
  "limit": "integer 1..100, default 50",
  "type": "appointment | event | queue",
  "status": "draft | live"
}

Response data:

{
  "surfaces": [],
  "liveVersions": [],
  "drafts": [],
  "page": 1,
  "limit": 50,
  "total": 0,
  "hasMore": false,
  "source": "d1"
}

Create Journey

POST /t/{tenantSlug}/v1/journeys
Authorization: Bearer vyg_live_xxx
Content-Type: application/json

Request:

{
  "type": "event",
  "name": "Private preview",
  "slug": "private-preview",
  "eventId": "evt_123",
  "config": {
    "type": "event",
    "name": "Private preview",
    "status": "draft",
    "layout": "steps",
    "brand": {},
    "progress": {},
    "header": {},
    "languages": ["en"],
    "order": ["tickets", "details", "payment", "confirmation"],
    "steps": {},
    "customText": {},
    "footer": {},
    "theme": {},
    "advanced": {},
    "publishTargets": [],
    "staff": {},
    "_eventId": "evt_123"
  }
}

The config object must satisfy @voyage/journey-runtime/core JourneyConfig. If config is omitted, the worker creates a default config for the requested type.

Response data:

{
  "id": "row_123",
  "tenantId": "tenant_123",
  "journeyId": "01J...",
  "version": 1,
  "status": "draft",
  "isLive": false,
  "slug": "private-preview",
  "configType": "event",
  "configJson": {},
  "eventId": "evt_123",
  "updatedAt": "2026-06-05T00:00:00.000Z",
  "publicPath": "/public/journeys/private-preview",
  "publicUrl": "https://booking.example.com/public/journeys/private-preview",
  "source": "d1"
}

Read Journey

GET /t/{tenantSlug}/v1/journeys/{journeyId}?version=1
Authorization: Bearer vyg_live_xxx

Response data: JourneySurfaceRow.

Update Journey

POST /t/{tenantSlug}/v1/journeys/{journeyId}
Authorization: Bearer vyg_live_xxx
Content-Type: application/json

Request:

{
  "config": {},
  "slug": "private-preview",
  "eventId": "evt_123"
}

Each update creates a new draft version.

Publish Journey

POST /t/{tenantSlug}/v1/journeys/{journeyId}/publish
Authorization: Bearer vyg_live_xxx
Content-Type: application/json

Request:

{
  "version": 2,
  "publishTargets": [
    { "id": "london-holborn", "label": "London - Holborn", "kind": "location" }
  ]
}

Response data: live JourneySurfaceRow.

GET /t/{tenantSlug}/v1/journeys/booking-links
PATCH /t/{tenantSlug}/v1/journeys/booking-links/{staffId}

Patch request:

{
  "slug": "ada-lovelace",
  "enabled": true
}

Public Journey API

Public journey routes are mounted under /v1/journeys/public. The SDK sends the API key when configured.

Public Render

GET /t/{tenantSlug}/v1/journeys/public/{slug}
Authorization: Bearer vyg_live_xxx

Response data: JourneySurfaceRow with live configJson and, for personal booking links, a personalLink object.

Record Visit

POST /t/{tenantSlug}/v1/journeys/public/{slug}/visit
Authorization: Bearer vyg_live_xxx
Content-Type: application/json

Request:

{
  "visitorToken": "visitor_abc123",
  "sessionId": "session_abc123",
  "referrer": "https://example.com"
}

Response data:

{
  "recorded": true,
  "deduped": false,
  "surfaceType": "journey",
  "surfaceId": "01J..."
}

Submit Public Registration

POST /t/{tenantSlug}/v1/journeys/public/{slug}/register
Authorization: Bearer vyg_live_xxx
Content-Type: application/json

Request:

{
  "selectedTickets": [
    { "tierId": "general", "quantity": 1 }
  ],
  "guest": {
    "firstName": "Ada",
    "lastName": "Lovelace",
    "email": "ada@example.com",
    "phone": "+15555550123"
  },
  "answers": {
    "accessibility": "Front-row seating",
    "interests": ["workshop", "networking"]
  },
  "promoCode": "EARLY",
  "agreedToTerms": true,
  "surfaceType": "journey"
}

Validation rules:

  • selectedTickets: 1 to 5 entries; each quantity 1 to 10.
  • guest.email: valid email, max 254 characters.
  • guest.firstName and guest.lastName: required by route-level validation after zod parsing.
  • guest.phone: if present, E.164 or US 10/11-digit normalized form.
  • answers: max 20 answers; values are string or string array.
  • agreedToTerms: must be literal true.

Response 201 data:

{
  "orderRef": "JRNI-ABC12345",
  "ticketLinks": [],
  "registration": {
    "guestId": "guest_01j...",
    "email": "ada@example.com",
    "status": "registered",
    "ticketTier": "general",
    "ticketName": "General admission",
    "customFields": {}
  },
  "event": {
    "eventId": "evt_123",
    "title": "Private preview",
    "registration_count": 1,
    "capacity": 100
  }
}

Read Public Registration

GET /t/{tenantSlug}/v1/journeys/public/{slug}/registrations/{guestId}?email=ada@example.com
Authorization: Bearer vyg_live_xxx

Response data:

{
  "registration": {},
  "journey": {}
}