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 requirejourney.buildfor API-client actors.journey.readexists 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 withregistration.writefor 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, andX-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.
Booking Links
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.firstNameandguest.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 literaltrue.
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": {}
}