mapis mock APIs, over REST

Define a fake backend with a few HTTP calls and point Studio Chat's api-tools at it. No repo, no deploy: an endpoint saved through the management API answers the next request.

Two kinds of things, one URL space. Mocks are served at https://mapis.stdcht.io/<project>/…. The management API lives at https://mapis.stdcht.io/api/… and takes X-Admin-Key.

For agents: https://mapis.stdcht.io/llms.txt is the complete operating guide as text (mental model, API reference, templates, demo recipes, a full example) — the raw material for a skill.

Quickstart

A="X-Admin-Key: $ADMIN_KEY"; M=https://mapis.stdcht.io/api

# 1. A project (it gets its own API key)
curl -s -X POST $M/projects -H "$A" -H 'content-type: application/json' \
  -d '{"name": "Demo Banco", "slug": "banco"}'
# → { "base_url": "https://mapis.stdcht.io/banco", "auth": { "header": "X-API-Key", "key": "mk_…" }, … }

# 2. An endpoint with a curated deck: two DNIs answer differently, the rest derive from the DNI
curl -s -X POST $M/projects/banco/endpoints -H "$A" -H 'content-type: application/json' -d '{
  "method": "GET", "path": "/clientes/:dni", "name": "consultar_cliente",
  "responses": [
    { "when": { "params.dni": "30123456" }, "body": { "dni": "30123456", "nombre": "Martina López", "saldo": 152300.5, "segmento": "premium" } },
    { "when": { "params.dni": { "matches": "^\\d{7,8}$" } }, "seed": "{{ params.dni }}",
      "body": { "dni": "{{ params.dni }}", "nombre": "{{ name() }}", "saldo": "{{ int(1000, 90000) }}", "segmento": "{{ pick(\"clasico\", \"gold\") }}", "alta": "{{ date(\"-400d\", \"date\") }}" } },
    { "status": 404, "body": { "error": "cliente_no_encontrado" } }
  ]
}'

# 3. A CRUD resource with generated data (day offsets, so the deck never expires)
curl -s -X POST $M/projects/banco/resources -H "$A" -H 'content-type: application/json' -d '{
  "name": "reclamos", "count": 8,
  "schema": { "id": "RCL-{{ pad(seq(\"rcl\", 1001), 5) }}", "dni": "{{ dni() }}", "motivo": "{{ pick(\"cargo duplicado\", \"tarjeta no llegó\") }}",
              "estado": "{{ pick(\"ABIERTO\", \"EN_ANALISIS\", \"RESUELTO\") }}", "creado": "{{ date(\"-\" + int(1, 30) + \"d\", \"date\") }}" }
}'

# 4. Call the mock like any API
K="X-API-Key: mk_…"
curl -s https://mapis.stdcht.io/banco/clientes/30123456 -H "$K"
curl -s "https://mapis.stdcht.io/banco/reclamos?estado=ABIERTO&limit=5" -H "$K"
curl -s -X POST https://mapis.stdcht.io/banco/reclamos -H "$K" -H 'content-type: application/json' -d '{"dni": "30123456", "motivo": "cargo duplicado"}'

# 5. Before each demo
curl -s -X POST $M/projects/banco/reset -H "$A"

Management API (https://mapis.stdcht.io/api, header X-Admin-Key)

CallWhat it does
GET /projects · POST /projectsList / create. Body: { name, slug?, description?, auth?: {mode: "api_key"|"none", header?, key?}, timezone?, log_limit? }
GET /projects/:slugSummary + endpoints + resources (+ the project's key)
PUT /projects/:slugDeclarative upsert of the whole definition (the shape export returns). Config-as-code.
PATCH /projects/:slug · DELETE /projects/:slug · POST …/rotate-keyChange meta / auth, delete everything, mint a new key
GET|POST|PUT /projects/:slug/endpointsList / add one or an array / replace all
GET|PUT|PATCH|DELETE /projects/:slug/endpoints/:idOne endpoint
GET|POST /projects/:slug/resources · GET|PATCH|DELETE …/resources/:nameCRUD resources; PATCHing seed/schema/count re-seeds
GET|PUT|DELETE …/resources/:name/recordsRead, replace or wipe the data behind a resource
POST /projects/:slug/resetBack to the seed: records, counters, logs (?keep_logs=1). Also POST https://mapis.stdcht.io/<slug>/__reset with the project key.
GET /projects/:slug/logs?limit=50 · DELETE …/logsWhat the assistant actually sent: method, path, body, matched rule, status, response
GET /projects/:slug/export · POST /projects/import?replace=1Whole definition out / in (?include_key=1 to include the key)
GET /projects/:slug/openapi.jsonOpenAPI 3 description of the mock
GET /projects/:slug/studiochat-toolsReady-made Studio Chat api-tool payloads (manage_api_tools), keyed with the project key
GET /helpers · POST /renderTemplate helpers reference; dry-run a template: { template, ctx?, seed? }

Endpoint definition

{
  "method": "POST", "path": "/transferencias", "name": "transferir", "description": "…",
  "public": false,                                   // true = no key needed (public pages, webhooks)
  "request": { "body": { "required": ["cbu", "monto"], "properties": { "monto": { "type": "number", "minimum": 1 } } } },  // 400 when it doesn't match
  "responses": [                                     // first rule whose "when" holds wins; last one without "when" = default
    { "when": { "body.monto": { "gt": 500000 } }, "status": 422, "body": { "error": "supera_limite", "limite": 500000 } },
    { "when": { "body.cbu": { "starts_with": "999" } }, "status": 404, "body": { "error": "cbu_inexistente" } },
    { "status": 201, "delay_ms": 800,
      "body": { "id": "TRF-{{ pad(seq(\"trf\", 900001), 6) }}", "estado": "PENDIENTE", "monto": "{{ num(body.monto) }}", "fecha": "{{ now() }}", "acreditacion": "{{ date(\"+1d\", \"dmy\") }}" } }
  ]
}

Conditions (when): { "path": value } is a loose equality (40000 == "40.000"); operators eq ne gt gte lt lte in nin exists empty contains starts_with ends_with matches type; combine with any, all, not or a bare array (OR). Paths: params.*, query.*, body.*, headers.*, method, path.

Templates ({{ … }}) work in bodies, headers, seeds and schemas. A string that is exactly one expression keeps the value's type ("{{ int(1, 9) }}" → a number). Helpers: ids (uuid seq id derive derive_pick), random (int float amount bool pick sample chance), fake es-AR data (name first_name last_name email phone dni cuit cbu alias bank company city province address word words sentence paragraph), dates (now today date format_date shift days_between with offsets like +2d -3h +1M and formats iso date datetime time dmy human unix weekday month), strings, numbers (num round money format_number percent), collections and logic (len sum pluck find where default if eq gt …). seed on a rule makes every random helper deterministic for that input — the same DNI always gets the same customer.

Resource definition

{
  "name": "turnos", "id_field": "id", "wrap": false, "readonly": false, "public": false,
  "seed":  [ { "id": "T-1", "paciente": "Martina López", "fecha": "{{ date(\"+2d\", \"date\") }}", "estado": "CONFIRMADO" } ],   // curated deck, templated at seed time
  "count": 12,                                                                                                             // plus generated records
  "schema": { "id": "T-{{ seq(\"t\", 100) }}", "paciente": "{{ name() }}", "fecha": "{{ date(int(1, 20) + \"d\", \"date\") }}", "estado": "{{ pick(\"PENDIENTE\", \"CONFIRMADO\") }}" }
}

Generated routes: GET /turnos (?page&limit&sort&order&search&fields, plus campo=valor, campo_gte, campo_lte, campo_ne, campo_like; X-Total-Count header), GET /turnos/:id, POST /turnos (schema fills what the body leaves out), PUT|PATCH /turnos/:id, DELETE /turnos/:id. An explicit endpoint on the same path wins over the resource route, so one verb can be customised while the rest stays generic.

Auth

Each project has auth.mode: api_key (default: a generated mk_… key in X-API-Key or Authorization: Bearer; the header name is configurable) or none. Endpoints and resources can be public. The admin key opens every mock too, for curl tests. Keys are demo credentials: readable through the management API on purpose.

Studio Chat · studiochat-workers/mapis