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/v1behaviour 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"
| Header | Meaning |
|---|---|
Deprecation | RFC 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. |
Sunset | RFC 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
| Legacy | Canonical |
|---|---|
GET /api/agents | GET /api/v1/characters |
POST /api/agents | POST /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}/documents | GET /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
| Legacy | Canonical |
|---|---|
GET /api/agents/{agent_id}/players | GET /api/v1/characters/{character_id}/players |
GET /api/agents/{agent_id}/players/stats | GET /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}/messages | GET /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
| Legacy | Canonical |
|---|---|
GET /api/agents/{agent_id}/players/{player_id}/memories | GET /api/v1/characters/{character_id}/players/{player_id}/memories |
GET .../memories/timeline | GET /api/v1/characters/.../memories/timeline |
GET .../memories/stats | GET /api/v1/characters/.../memories/stats |
GET .../memories/core-facts | GET /api/v1/characters/.../memories/core-facts |
GET .../memories/graph | GET /api/v1/characters/.../memories/graph |
POST .../memories/search | POST /api/v1/characters/.../memories/search |
DELETE /api/agents/{agent_id}/players/{player_id}/memories | DELETE /api/v1/characters/.../memories |
POST /api/v1/agents/{agent_id}/players/{player_id}/memories | POST /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
| Legacy | Canonical |
|---|---|
POST /api/generate/image-to-character | POST /api/v1/characters/from-image |
POST /api/generate/{agent_id}/generate-model | POST /api/v1/characters/{character_id}/model |
GET /api/generate/{agent_id}/model-status | GET /api/v1/characters/{character_id}/model |
POST /api/encounters | POST /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 name | Canonical name |
|---|---|
memory_type | memoryType |
start_date | startDate |
end_date | endDate |
group_by | groupBy |
include_entities | includeEntities |
include_character_memories | includeCharacterMemories |
sort_by | sortBy |
sort_order | sortOrder |
player_search | playerSearch |
min_messages | minMessages |
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
| Route | Legacy | Canonical |
|---|---|---|
| Delete a conversation | 200 with {"message": "Player conversation deleted"} | 204 with no body |
| Delete all memories | 200 with deletedCount and message | 200 with {"deletedCount": n} only |
| Create character from image | 200 with a camelCase agent object | 201 with the standard Character object |
| Start 3D model generation | 200 with agentId and provider task ids | 202 with {"characterId", "modelStatus", "rigged"} |
| Start an encounter | 202 with {"encounterId"} | unchanged |
The operational and staff routes have separate compatibility changes:
| Route | Change |
|---|---|
GET /api/health | Still 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-metrics | Requires staff authentication. Use GET /api/staff/v1/system-metrics for the staff-plane path. |
POST /api/v1/organizations/{org_id}/purge | Requires 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-imagetakes camelCase fields (ttsProvider,appearancePrompt,voicePrompt,personaPrompt,useDefaultVoice, andmimeTypeon the JSON variant) where the legacy route took snake_case.GET /api/v1/characters/{character_id}/modelreturns the same keys as the legacy model-status route, but it is player-scoped: a mismatchedX-Player-Idgets a404.
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.
| SDK | What to do |
|---|---|
| TypeScript | Upgrade to the next release. No code change needed. |
| Python | Upgrade to the next release. If you read provider task IDs from generate_model(), switch to status polling first. |
| Unity | Upgrade to the next release. No code change needed. |
| Lens Studio | Upgrade to the next release. No code change needed. |
| Unreal | Nothing 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 with405. If memory search has never returned results for you, upgrading is the fix. - SDK releases from this point on send an
X-Estuary-Clientheader 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.