Skip to main content

Legacy Migration

Developer REST endpoints have a canonical home under /api/v1. Dashboard, staff and website endpoints have separate /api/control/v1, /api/staff/v1 and /api/site/v1 homes. The older unversioned developer routes (/api/agents/..., /api/generate/..., /api/encounters) remain available, as do the memory write routes published under /api/v1/agents/....

This page tells you what replaces what, what changes on the wire when you move, and how long the old routes will be around.

Why there are two sets of routes​

Estuary grew a REST surface before it had a versioning policy, so the same resource ended up reachable under more than one path, with different casing conventions and different status codes. /api/v1 is now the single versioned contract: one namespace, camelCase query parameters and request bodies, predictable status codes (201 create, 200 update, 204 delete one, 200 bulk delete), and a published OpenAPI document that cannot change in a breaking way without a new major version.

The older developer routes were not deleted. They are compatibility adapters that call the same code as their /api/v1 replacements, so the character, conversation and memory data you read and write through them is identical.

Compatibility today​

:::info The old routes keep working Legacy developer data routes are frozen, not removed. They keep their paths, payloads, status codes and authentication. Existing SDK releases continue to use them. Operational and staff routes have the changes listed below. :::

Removal is driven by evidence, never by a calendar alone:

  • A route is removed only after a long notice period, at minimum six months from the announcement and at least two shipped releases of every SDK that called it.
  • Routes called by shipped game-engine builds (Unity, Unreal, Lens Studio) get at least twelve months, because those builds cannot be force-upgraded. In practice a thin adapter costs almost nothing to keep, so they default to staying.
  • A sunset date, once published, never moves earlier.
  • Published /api/v1 behaviour is never changed silently either, including its known warts. Breaking cleanup would belong to a future /api/v2.

Headers you will see on a legacy route​

Once LEGACY_API_DEPRECATED_AT is configured, responses from deprecated routes carry standard deprecation signalling:

HTTP/1.1 200 OK
Deprecation: @1789000000
Link: <https://docs.estuary-ai.com/rest-api/legacy-migration>; rel="deprecation"; type="text/html",
</api/v1/characters/{character_id}/players>; rel="successor-version"
HeaderMeaning
DeprecationRFC 9745 structured-field date: @ followed by Unix seconds, the moment the route was marked deprecated.
Link with rel="deprecation"Points at this page.
Link with rel="successor-version"The canonical path that replaces this one. Omitted when no replacement exists yet.
SunsetRFC 8594 HTTP-date. Sent only once a removal date is final. No Sunset header means no date has been set.

:::warning These headers are informational They are not an error. Do not retry on them, do not fail the call, and do not warn on every request. Log them once if you want visibility, then plan the move. :::

Route mapping​

agent_id and character_id are the same value: the id the Characters API returns.

Characters​

LegacyCanonical
GET /api/agentsGET /api/v1/characters
POST /api/agentsPOST /api/v1/characters
GET /api/agents/{agent_id}GET /api/v1/characters/{character_id}
PUT /api/agents/{agent_id}PUT /api/v1/characters/{character_id}
DELETE /api/agents/{agent_id}DELETE /api/v1/characters/{character_id}
GET /api/agents/{agent_id}/documentsGET /api/v1/characters/{character_id}/documents
DELETE /api/agents/documents/{document_id}DELETE /api/v1/characters/{character_id}/documents/{document_id}

The multipart image and model parts of POST/PUT /api/agents are now their own endpoints: PUT /api/v1/characters/{character_id}/avatar and PUT /api/v1/characters/{character_id}/model-file.

Conversations​

LegacyCanonical
GET /api/agents/{agent_id}/playersGET /api/v1/characters/{character_id}/players
GET /api/agents/{agent_id}/players/statsGET /api/v1/characters/{character_id}/players/stats
GET /api/agents/{agent_id}/players/{player_id}GET /api/v1/characters/{character_id}/players/{player_id}
GET /api/agents/{agent_id}/players/{player_id}/messagesGET /api/v1/characters/{character_id}/players/{player_id}/messages
DELETE /api/agents/{agent_id}/players/{player_id}DELETE /api/v1/characters/{character_id}/players/{player_id}

Memories​

LegacyCanonical
GET /api/agents/{agent_id}/players/{player_id}/memoriesGET /api/v1/characters/{character_id}/players/{player_id}/memories
GET .../memories/timelineGET /api/v1/characters/.../memories/timeline
GET .../memories/statsGET /api/v1/characters/.../memories/stats
GET .../memories/core-factsGET /api/v1/characters/.../memories/core-facts
GET .../memories/graphGET /api/v1/characters/.../memories/graph
POST .../memories/searchPOST /api/v1/characters/.../memories/search
DELETE /api/agents/{agent_id}/players/{player_id}/memoriesDELETE /api/v1/characters/.../memories
POST /api/v1/agents/{agent_id}/players/{player_id}/memoriesPOST /api/v1/characters/.../memories
PATCH /api/v1/agents/{agent_id}/players/{player_id}/memories/{memory_id}PATCH /api/v1/characters/.../memories/{memory_id}
DELETE /api/v1/agents/{agent_id}/players/{player_id}/memories/{memory_id}DELETE /api/v1/characters/.../memories/{memory_id}

The three /api/v1/agents/... memory write routes are deprecated aliases: the paths are still published and behave identically to their /characters/... counterparts.

Generation and encounters​

LegacyCanonical
POST /api/generate/image-to-characterPOST /api/v1/characters/from-image
POST /api/generate/{agent_id}/generate-modelPOST /api/v1/characters/{character_id}/model
GET /api/generate/{agent_id}/model-statusGET /api/v1/characters/{character_id}/model
POST /api/encountersPOST /api/v1/encounters

What changes on the wire​

Moving a call is usually a path change and nothing else. These are the exceptions.

Query parameter names are camelCase​

On the canonical routes every query parameter is camelCase. Unknown query parameters are silently ignored, which is the one thing that can bite you: a leftover memory_type=fact does not error, it just stops filtering.

Legacy nameCanonical name
memory_typememoryType
start_datestartDate
end_dateendDate
group_bygroupBy
include_entitiesincludeEntities
include_character_memoriesincludeCharacterMemories
sort_bysortBy
sort_ordersortOrder
player_searchplayerSearch
min_messagesminMessages

Enum values stay snake_case. sortBy=last_activity is correct.

One parameter disappeared rather than moved: the memory list no longer accepts sortBy or sortOrder. The legacy route accepted them and ignored them, so dropping them changes nothing about your results.

Memory search takes a POST body​

Search has always been a POST with a JSON body. If your code sends GET .../memories/search?q=..., it has never worked and will get a 405.

curl -X POST "https://api.estuary-ai.com/api/v1/characters/{character_id}/players/{player_id}/memories/search" \
-H "X-API-Key: est_your_api_key" \
-H "Content-Type: application/json" \
-d '{"query": "favourite tea", "limit": 5}'

The response is {"results": [{"memoryId", "similarity", "content"}], "query", "total"}.

Status codes and response bodies​

RouteLegacyCanonical
Delete a conversation200 with {"message": "Player conversation deleted"}204 with no body
Delete all memories200 with deletedCount and message200 with {"deletedCount": n} only
Create character from image200 with a camelCase agent object201 with the standard Character object
Start 3D model generation200 with agentId and provider task ids202 with {"characterId", "modelStatus", "rigged"}
Start an encounter202 with {"encounterId"}unchanged

The operational and staff routes have separate compatibility changes:

RouteChange
GET /api/healthStill returns 200 and status/service, but dependency details and queue depth moved to staff-only GET /api/staff/v1/admin/health. Monitoring that reads services must migrate.
GET /api/system-metricsRequires staff authentication. Use GET /api/staff/v1/system-metrics for the staff-plane path.
POST /api/v1/organizations/{org_id}/purgeRequires a JSON body with confirmOrgId. Purge is allowed after 30 days of soft deletion, or earlier with force: true and a nonempty reason.

Two more details on generation:

  • POST /api/v1/characters/from-image takes camelCase fields (ttsProvider, appearancePrompt, voicePrompt, personaPrompt, useDefaultVoice, and mimeType on the JSON variant) where the legacy route took snake_case.
  • GET /api/v1/characters/{character_id}/model returns the same keys as the legacy model-status route, but it is player-scoped: a mismatched X-Player-Id gets a 404.

Unknown fields are rejected on some bodies​

Some canonical request bodies reject unknown keys with a 422 instead of ignoring them, including the memory search body and the encounter body. A typo that used to pass silently now fails loudly, which is the point.

Upgrading each SDK​

Existing SDK releases keep using the legacy routes. When upgrading, SDK method names and signatures stay the same. The Python SDK preserves the legacy conversation and memory deletion messages when it calls v1, but generate.generate_model() no longer returns provider task IDs such as meshyPreviewTaskId: v1 does not expose them. If your code reads a task ID, switch to generate.get_model_status() or generate.wait_for_model() for progress before upgrading.

SDKWhat to do
TypeScriptUpgrade to the next release. No code change needed.
PythonUpgrade to the next release. If you read provider task IDs from generate_model(), switch to status polling first.
UnityUpgrade to the next release. No code change needed.
Lens StudioUpgrade to the next release. No code change needed.
UnrealNothing to do. The Unreal SDK makes no REST calls.

Two notes for SDK users:

  • The TypeScript and Python SDKs previously sent memory search as GET ?q=..., which the gateway answered with 405. If memory search has never returned results for you, upgrading is the fix.
  • SDK releases from this point on send an X-Estuary-Client header on every REST request. It identifies the SDK and its version so old routes can be retired on real usage data rather than guesswork. See Authentication if you call the API directly and want to send it too.