What a function can call once it runs (db, kv, SQL, fetch, limits, logs) is in the
function runtime guide; the typed names are GET /runtime.d.ts.
Each endpoint is an operation, the same one MCP and your functions call.
Discover the operation IDs from GET /v1/_operation_registry or
the tool manifest at GET /v1/_tools; the routes below are the HTTP form of
those operations.
Every endpoint needs an app key, which reaches only its own app.
Changing a function, step by step
Use this sequence when changing a function:
- Call
GET /v1/_functions/_indexfor the function name, currentcontent_hash, entry point, enabled state, and deployment counts.GET /v1/_functions/_cataloggives the same bounded summaries grouped by database, so you can discover the whole project without opening any source body. - Call
GET /v1/functions/{name}when you need the source body or the complete metadata. - Call
POST /v1/_functions/_syntaxto validate a proposed source without running it. A response withvalid:falseholds the line, column, and compiler diagnostic. - Use
PATCH /v1/functions/{name}/sourcefor a bounded edit, orPUT /v1/functions/{name}/sourcefor a complete replacement. Always send thecontent_hashthe index returned, asexpected_hash. - Treat a
409as a concurrent update: refresh the index or source and retry against the new hash. Never overwrite a changed function blindly.
For targeted inspection, call GET /v1/functions/{name}/source with
start_line and exclusive end_line query fields. It copies only the
bounded slice you asked for into the response. max_bytes defaults to a
bounded value; lower it yourself if you need to. Send expected_hash when
the read must be tied to one specific source.
Bounded source search
GET /v1/functions/{name}/search
Pass q to find literal matches without requesting the complete source:
q=checkout&max_matches=32&max_bytes=12000&expected_hash=...
The result holds absolute byte offsets, one-based line numbers, and a
bounded line snippet for each match. max_matches defaults to 128 and is
capped at 1024; max_bytes defaults to 32 KiB and is capped at 1 MiB.
truncated:true means a bound stopped the scan or a snippet copy. The search
is literal and line-local: it never runs or parses the function.
Bounded source symbols
GET /v1/functions/{name}/symbols
Call this to locate declaration anchors before reading or editing a range:
max_symbols=128&max_bytes=32768&expected_hash=...
It lists identifiers that follow function, class, const, let, and
var, skipping comments and quoted strings, and gives each one's
keyword-to-identifier byte span. max_symbols defaults to 128 and is capped
at 1024; max_bytes defaults to 32 KiB and is capped at 1 MiB. Treat the
result as a navigation hint, not a full JavaScript AST: read the surrounding
range before you edit.
For durable history, use the app's branch history: GET /v1/_commits?path=functions/NAME.js
(version.log) lists the commits that changed that one function, newest first,
and GET /v1/_commits/{version}/unit?path=... (version.unit) reads its source at
any of them. Deploying an older source again is a commit of its own, so the
history is never rewritten.
Set environment keys independently of source edits: GET /v1/functions/{name}/env
reads the current typed key/value map, PUT /v1/functions/{name}/env sets one
key, and DELETE /v1/functions/{name}/env/{key} removes one key. An
environment write keeps routes, bindings and entry point as
they are; it never moves the function's deploy counter, since its content hash
does not change. Send the current content_hash as expected_hash when you
coordinate with other concurrent changes. Deleting an already absent key still
succeeds.
A source edit takes either byte offsets or one-based line ranges. For byte
edits, offsets count through the UTF-8 source as Substrio stores it; start
is inclusive and end is exclusive. For line edits, start_line is one-based
and end_line is exclusive, matching bounded source reads. Send one range
form per request: a line range is resolved into the byte range the approval
binds. An empty replacement is valid and deletes that range. An inverted,
ambiguous, or out-of-bounds range is refused before any write.
Source writes use a preview-then-apply handshake. Call
POST /v1/functions/{name}/source/preview with either start/end or
start_line/end_line, plus replacement and the current expected_hash. It
answers an approval token bound to the base hash, proposed hash, resolved byte
range, replacement hash, and expiry. A line-based preview also echoes
start_line, end_line, and range_kind:"lines" so you can keep working in
your own coordinates. Send that token to PATCH /v1/functions/{name}/source
with the same range form; it is resolved again and the approval is spent only
once the deployment succeeds. Repeating a preview with the same
idempotency_key returns the same approval, and retrying an already applied
token returns the same successful deployment result while the proposed hash is
still current.
For larger changes, commit on a branch instead of staging: POST /v1/_commits
(version.commit) writes the candidate into a branch of the app's history
without touching the live function, and POST /v1/_live (version.live.set)
makes a version live when the work is ready. A branch holds as many commits as
the change needs, and nothing of it is executable until it goes live.
Syntax diagnostics
POST /v1/_functions/_syntax
Request:
{
"source": "module.exports = (event) => event.value + 1;",
"source_name": "index.js"
}
source is required. source_name is optional and defaults to
agent-source.js. The response is always HTTP 200 for a syntactically
checked source:
{"valid":true,"line":0,"column":0,"message":""}
or, for invalid source:
{"valid":false,"line":1,"column":25,"message":"Unexpected token"}
The check only parses the source: it never runs your code or writes anything. Malformed JSON, missing source, a missing app, an authentication failure, and an unavailable runtime all answer the usual Substrio error envelope instead.
Function index
GET /v1/_functions/_index
A deterministic name-sorted index, with no source bodies or environment secret values:
{
"functions": [
{
"name": "welcome",
"id": "demo/welcome",
"entry_point": "index.js",
"content_hash": "bcac97e07a163d9a",
"size_bytes": 184,
"enabled": true,
"route_count": 1,
"binding_count": 0,
"environment_key_count": 2
}
],
"count": 1
}
content_hash is the optimistic-concurrency token for source edits. Call
GET /v1/functions/{name} when you need the source text, routes, bindings,
timestamps, or the full metadata object.
The token is an opaque Substrio deployment identity: compare it for
concurrency, never treat it as a cryptographic digest and never recompute it.
GET /v1/_functions/_catalog
A source-free summary of the app's functions, grouped by database: database
names, function counts, entry points, hashes, sizes, route and binding counts,
and enabled state. max_databases defaults to 64 (cap 256) and
max_functions defaults to 4096 (cap 65536); truncated:true means request a
larger page. Secret environment values and source bodies are never included.
Bounded source line reads
GET /v1/functions/{name}/source
Query fields:
start_line=120&end_line=146&max_bytes=12000&expected_hash=...
start_line is one-based. end_line is exclusive, so the example returns
lines 120 through 145. The server returns line and byte boundaries, the total
line count, the current content hash, and the selected UTF-8 source slice:
{
"function": "welcome",
"start_line": 120,
"end_line": 146,
"line_count": 318,
"start_offset": 4210,
"end_offset": 4972,
"content_hash": "bcac97e07a163d9a",
"source": "...bounded source slice..."
}
A stale expected_hash, an invalid range, or a response over the byte budget
is rejected before source is returned.
Atomic source replacement
PUT /v1/functions/{name}/source
Request fields:
{
"script": "module.exports = (event) => ({ok: true});",
"expected_hash": "bcac97e07a163d9a",
"main": "index.js",
"routes": ["POST /welcome"],
"vars": {"MODE": "production"}
}
Only script is required. Leaving out deployment metadata keeps it as the
current function has it; sending an array or object replaces that metadata.
The same presence rule holds for PATCH /v1/functions/{name} and the
POST /v1/deploy compatibility alias: omitting routes, vars or bindings
is not destructive, while sending one of them empty clears that metadata.
The new source is syntax checked, then deployed as one step. Success returns
the normal deployment result with success, function_id, and the complete
metadata.
Bounded source editing
POST /v1/functions/{name}/source/preview
Request:
{
"start_line": 43,
"end_line": 46,
"replacement": "2",
"expected_hash": "bcac97e07a163d9a",
"idempotency_key": "agent-change-001"
}
The preview checks the complete proposed source without running it, and
answers approval_token, base_hash, proposed_hash, original_size,
proposed_size, and expires_at. Nothing is deployed yet.
GET /v1/functions/{name}/approvals/{token}
A bounded view of one approval: its app and function, its
base/proposed/replacement hashes, byte range, expiry, and state (pending,
applied, or expired). The replacement text itself is never returned.
DELETE /v1/functions/{name}/approvals/{token}
Revokes a pending or expired approval. An applied approval is an immutable
receipt and returns 409; a revoked token cannot authorize a later edit.
Direct function invocation
POST /v1/functions/{name}/_invoke
Runs the deployed function in the current app. The request body is the
JSON event delivered to the function; an omitted body is treated as {}. The
optional timeout_ms and memory_mb query parameters override the app's
configured CPU and memory limits for this call, and must be positive integers.
This endpoint works with no AI integration, and returns the function's JSON
result directly. A successful response carries an X-Substrio-Function-Hash
header so you can match the run to the source snapshot you inspected.
PATCH /v1/functions/{name}/source
Request:
{
"start": 43,
"end": 46,
"replacement": "2",
"expected_hash": "bcac97e07a163d9a",
"approval_token": "..."
}
replacement and approval_token are required, together with either
start/end or start_line/end_line; always send expected_hash. The
call checks the durable approval and current hash, applies the edit, and
persists it with the existing deployment metadata. A token is bound to one
exact edit and cannot authorize another range or replacement.
Feedback and recovery
The shared error envelope uses code as the stable discriminator and detail
for field-level guidance. Common outcomes are:
| Status | Meaning | Next action |
|---|---|---|
400 |
Invalid JSON, missing field, invalid range, or syntax error | Read detail; correct the request or source. Syntax errors also include valid:false, line, column, and message. |
401 |
Missing or invalid authentication | Refresh the app or session credentials. |
404 |
Function name does not exist | Refresh the index or deploy the function first. |
409 |
expected_hash does not match |
Re-read the index/source and retry against the new hash. |
503 |
JavaScript runtime unavailable | Retry once the runtime is available. |
These operations are intentionally narrow: they do not expose secret values, execute source during validation, or silently merge concurrent edits. The operation registry and generated OpenAPI document remain authoritative for the current schemas and grant requirements.
Function environment
GET /v1/functions/{name}/env
Returns a deterministic environment map and the current source hash:
{
"function": "welcome",
"content_hash": "bcac97e07a163d9a",
"environment": {"MODE": "production", "REGION": "local"},
"count": 2
}
PUT /v1/functions/{name}/env
Set one key without replacing the rest of the deployment metadata:
{"key":"MODE","value":"production","expected_hash":"bcac97e07a163d9a"}
key and value are required strings. A successful response is the normal
deployment result with the updated environment. A stale expected_hash
returns 409; an unknown function returns 404.
DELETE /v1/functions/{name}/env/{key}
Remove one key. The optional body may contain expected_hash; deleting a key
that is already absent succeeds without writing a new deployment.