Documentation

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

ParameterTypeDescription
namespacestringRegistered namespace name.
external_idstringYour own key for the record. Used as the address for get and delete.
payloadJSON stringJSON 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

ParameterTypeDescription
namespacestringRegistered namespace name.
external_idstringYour 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

ParameterTypeDescription
namespacestringRegistered namespace name.
sinceISO-8601 string (optional)Returns records with updated_at > since. Omit for the oldest page.
page_sizeinteger (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

ParameterTypeDescription
namespacestringRegistered namespace name.
external_idstringYour 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_id and 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

MethodPathEquivalent MCP tool
GET/api/v1/external-records/namespacesexternal_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 statusError code(s)
400invalid_payload, invalid_since, namespace_name_invalid
403forbidden_read_only
404namespace_not_registered, record_not_found
409namespace_already_exists
413payload_too_large
429rate_limited
500internal_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.