External Record Store API
Five operations available over both MCP (for Claude Desktop, custom agents, and other MCP clients) and HTTP REST (for Zapier, n8n, curl, and any other generic HTTP client). The detailed sections below describe the MCP tool surface; the REST surface at the bottom of this page exposes the same operations as conventional HTTP endpoints. For a non-technical overview, see External Record Store. Namespaces are managed under Global Project Configuration → External Records.
Server limits
- Max payload size: 64 KB per record (65,536 bytes).
- Write rate: 60 writes per minute per tenant (put and delete combined).
- Max page size for query: 200 records.
Error envelope
Domain errors return a structured envelope: { "error": "<code>", "message": "...", "details": {...} }. The per-tool sections below list the codes each tool can raise. Any unhandled server-side throw returns { "error": "internal_error", "message": "<ExceptionType>: ..." } instead of leaking opaquely to the transport.
MCP external_records_put
Upsert a record. Returns the canonical row. A fresh insert returns created_at == updated_at; a subsequent put with the same external_id preserves created_at, advances updated_at, and replaces the payload entirely.
Parameters
| Parameter | Type | Description |
|---|---|---|
namespace | string | Registered namespace name. |
external_id | string | Your own key for the record. Used as the address for get and delete. |
payload | JSON string | JSON object as a string. Stored verbatim. Must parse as JSON. |
Example response
{
"namespace": "site_visits",
"external_id": "INC-4471",
"payload": { "status": "open", "title": "Acme Co - first visit" },
"created_at": "2026-05-19T12:00:00Z",
"updated_at": "2026-05-19T12:00:00Z"
}
Error responses
{ "error": "namespace_not_registered", "message": "Namespace 'ghost' is not registered for this tenant." }
{ "error": "invalid_payload", "message": "Payload is not valid JSON: ..." }
{ "error": "payload_too_large", "message": "Payload size 84320 bytes exceeds limit 65536 bytes.", "details": { "actual_bytes": 84320, "limit_bytes": 65536 } }
{ "error": "rate_limited", "message": "Write rate limit exceeded for this tenant.", "details": { "retry_after_seconds": 60 } }
{ "error": "forbidden_read_only", "message": "Read-only role cannot create or update records." }
MCP external_records_get
Fetch a single record by namespace + external_id.
Parameters
| Parameter | Type | Description |
|---|---|---|
namespace | string | Registered namespace name. |
external_id | string | Your own key. |
Example response
{
"namespace": "site_visits",
"external_id": "INC-4471",
"payload": { "status": "open", "title": "Acme Co - first visit" },
"created_at": "2026-05-19T12:00:00Z",
"updated_at": "2026-05-19T12:00:00Z"
}
Error responses
{ "error": "record_not_found", "message": "Record not found." }
MCP external_records_query
List records in a namespace updated after an optional watermark, ordered by updated_at ascending. Page size is capped at the server limit. The watermark uses strict > semantics: a record with updated_at == since is excluded.
Parameters
| Parameter | Type | Description |
|---|---|---|
namespace | string | Registered namespace name. |
since | ISO-8601 string (optional) | Returns records with updated_at > since. Omit for the oldest page. |
page_size | integer (optional) | Page size; capped at 200. |
Example response
{
"namespace": "site_visits",
"records": [
{ "external_id": "INC-4470", "payload": {...}, "created_at": "2026-05-19T11:55:00Z", "updated_at": "2026-05-19T11:58:00Z" },
{ "external_id": "INC-4471", "payload": {...}, "created_at": "2026-05-19T12:00:00Z", "updated_at": "2026-05-19T12:00:00Z" }
],
"next_since": "2026-05-19T12:00:00Z"
}
next_since is null when fewer than page_size records were returned (no more pages). Otherwise pass it as the next since to continue.
Error responses
{ "error": "invalid_since", "message": "'not-a-date' is not a valid ISO-8601 timestamp." }
MCP external_records_delete
Hard delete. No undelete. Recovery is from the source system, not AVstackr. No bulk variant — one record per call.
Parameters
| Parameter | Type | Description |
|---|---|---|
namespace | string | Registered namespace name. |
external_id | string | Your own key for the record to remove. |
Example response
{ "deleted": true, "namespace": "site_visits", "external_id": "INC-4471" }
Deleting a record that doesn't exist within a registered namespace is idempotent: returns { "deleted": false, ... } without raising an error. Deleting from a namespace that isn't registered raises namespace_not_registered so a typo'd namespace doesn't silently look like a missing record.
Error responses
{ "error": "namespace_not_registered", "message": "Namespace 'ghost' is not registered for this tenant." }
{ "error": "forbidden_read_only", "message": "Read-only role cannot delete records." }
MCP external_records_list_namespaces
Returns the namespaces registered for the calling tenant, with a live record_count per namespace. Call this first when you don't already know what's valid.
Example response
{
"namespaces": [
{ "name": "site_visits", "display_name": "Site Visits", "record_count": 12, "created_at": "2026-05-19T10:00:00Z" },
{ "name": "tickets", "display_name": null, "record_count": 0, "created_at": "2026-05-22T08:00:00Z" }
]
}
HTTP REST API
Same operations, same tenant + read-only-role behaviour, exposed as conventional HTTP endpoints under /api/v1/external-records. Use these from any client that cannot speak MCP — Zapier, n8n, scheduled scripts, ad-hoc curl.
Authentication
Send an Authorization: Bearer <token> header. Two token types are accepted:
- OAuth JWT — issued via the AVstackr OAuth server. The token's
tenant_idand user claims determine which records you can see. - API key (
avs_*prefix) — issued under Settings → API Keys. The key is scoped to the issuing user's tenant and role.
Endpoints
| Method | Path | Equivalent MCP tool |
|---|---|---|
GET | /api/v1/external-records/namespaces | external_records_list_namespaces |
GET | /api/v1/external-records/{namespace}/{external_id} | external_records_get |
GET | /api/v1/external-records/{namespace}?since=&page_size= | external_records_query |
PUT | /api/v1/external-records/{namespace}/{external_id} | external_records_put |
DELETE | /api/v1/external-records/{namespace}/{external_id} | external_records_delete |
Request body for PUT
Unlike the MCP put tool (which takes the payload as a JSON-string parameter), the REST PUT endpoint takes the JSON object directly as the request body. Send Content-Type: application/json.
PUT /api/v1/external-records/site_visits/INC-4471
Authorization: Bearer avs_...
Content-Type: application/json
{ "status": "open", "title": "Acme Co - first visit" }
Success response envelope
Successful REST responses are wrapped in the standard AVstackr envelope (consistent with the rest of /api/v1/*):
{
"success": true,
"data": {
"namespace": "site_visits",
"external_id": "INC-4471",
"payload": { "status": "open", "title": "Acme Co - first visit" },
"created_at": "2026-05-23T19:17:24Z",
"updated_at": "2026-05-23T19:17:24Z"
},
"timestamp": "2026-05-23T19:17:24.123456Z"
}
Error response envelope
Errors return the standard AVstackr error envelope with the same error codes as the MCP surface. HTTP status codes map to the error code:
| HTTP status | Error code(s) |
|---|---|
| 400 | invalid_payload, invalid_since, namespace_name_invalid |
| 403 | forbidden_read_only |
| 404 | namespace_not_registered, record_not_found |
| 409 | namespace_already_exists |
| 413 | payload_too_large |
| 429 | rate_limited |
| 500 | internal_error |
{
"success": false,
"error": {
"code": "namespace_not_registered",
"message": "Namespace 'ghost' is not registered for this tenant.",
"details": null
},
"timestamp": "2026-05-23T19:17:24.123456Z"
}
Query example
GET /api/v1/external-records/site_visits?since=2026-05-23T12:00:00Z&page_size=100
Authorization: Bearer avs_...
Returns up to page_size records ordered by updated_at ascending. Pass the returned next_since as the next since to continue; next_since is null when there are no more pages.
For tool connection setup, see MCP Server.
