Build a dashboard on the platform alone

An agent fills a store, a hosted API serves it, a published front end reads and writes it, and you run no server

Stores, files and hosted APIs are in beta and enabled per workspace. Without the grant every endpoint here answers 402.

This is the case the platform is built around, done with nothing but the API. A coding agent given this page and the reference can assemble it; every call below is an operation in the SDK, named after the operationId you see in the reference.

The shape, in one sentence: the run is your backend function, and each route is a fast path past it for one common operation. Reading a collection, reading a record, summarising a collection, serving a file and writing a record have fast paths. Anything else goes through a run, which is an agent with tools and a person.

What you end up with

  • A store, leads, that an agent fills and your users read and write.
  • A hosted API at acme-leads.apis.speechify.ai with four routes: a list, a summary, a create and an update.
  • A front end published as files and served by the same API under /app/*.
  • An audience: your own workspace for an internal tool, or your users through a token your backend signs.

1. Create the store

POST
/v1/stores
1curl -X POST https://api.speechify.ai/v1/stores \
2 -H "Authorization: Bearer <token>" \
3 -H "Content-Type: application/json" \
4 -d '{
5 "name": "news"
6}'

Name it for what is in it. An agent attached to it reads the name and the description to decide whether to look here, and a coding agent reads them the same way.

A store is Firestore-shaped: store > collection > document, schemaless JSON, filters on top-level fields, one order_by, a limit, cursor paging, and one aggregate operation. There are no joins, nested-field filters, full-text search or SQL, on purpose. The ids query, batch and aggregate are reserved: they are operations on a collection, not documents in it.

2. Let an agent fill it

Attach the store to the agent that produces the data, and give it a run that writes.

POST
/v1/agents/:agent_id/stores/:store_id
1curl -X POST https://api.speechify.ai/v1/agents/agent_01jqr8x9zg5k2m3n4p5q6r7s8t/stores/store_01jqr8x9zg5k2m3n4p5q6r7s8t \
2 -H "Authorization: Bearer <token>"

A run reads and writes the store with store_query, store_get, store_put, store_delete and store_aggregate. Ask for one document per lead with a stage and a value, and the agent’s writes land in the leads collection like any other document.

3. Assemble the API

POST
/v1/apis
1curl -X POST https://api.speechify.ai/v1/apis \
2 -H "Authorization: Bearer <token>" \
3 -H "Content-Type: application/json" \
4 -d '{
5 "slug": "acme-news",
6 "name": "Acme News"
7}'

Pick the audience while you create it, narrowest first:

auth_modeWho can call itWhat they present
ownerOnly youYour own Speechify API key or console session
workspaceMembers of your workspaceTheir own API key or console session
user_tokenYour usersA short-lived JWT your backend signs for each of them
consumer_keyCallers you issue a key toA ck_ bearer minted for this API
publicAnyone on the internetNothing; reads only, rate limited per address

An internal dashboard is a workspace API and needs no key at all: every member’s own credential works. A customer-facing one is a user_token API, and every route can then scope records to the person calling. A workspace can refuse public as policy (hosted_apis_public_allowed), and the console asks twice before opening an API to the internet.

4. Add the routes

Each route names what answers it. The four below are the whole dashboard.

POST
/v1/apis/:api_id/routes
1curl -X POST https://api.speechify.ai/v1/apis/api_01jqr8x9zg5k2m3n4p5q6r7s8t/routes \
2 -H "Authorization: Bearer <token>" \
3 -H "Content-Type: application/json" \
4 -d '{
5 "method": {
6 "0": "G",
7 "1": "E",
8 "2": "T",
9 "value": "GET"
10 },
11 "path": {
12 "0": "/",
13 "1": "n",
14 "2": "e",
15 "3": "w",
16 "4": "s",
17 "5": "/",
18 "6": "{",
19 "7": "i",
20 "8": "d",
21 "9": "}",
22 "value": "/news/{id}"
23 },
24 "resolver": {
25 "type": "store_query",
26 "value": {
27 "type": "store_query"
28 }
29 }
30}'

The list. GET /leads with a store_query resolver over leads, filtered by a request value the caller may or may not send:

1{
2 "method": "GET",
3 "path": "/leads",
4 "resolver": {
5 "type": "store_query",
6 "store_id": "store_…",
7 "collection": "leads",
8 "where": [{ "field": "stage", "op": "eq", "value": "{{query.stage}}" }],
9 "order_by": { "field": "value", "direction": "desc" }
10 },
11 "cache_ttl_seconds": 30
12}

The summary. GET /summary with a store_aggregate resolver: a count and a sum per stage, in one request, from the index rather than the bodies:

1{
2 "method": "GET",
3 "path": "/summary",
4 "resolver": {
5 "type": "store_aggregate",
6 "store_id": "store_…",
7 "collection": "leads",
8 "group_by": "stage",
9 "metrics": [{ "op": "count" }, { "op": "sum", "field": "value", "as": "pipeline" }]
10 }
11}

The create. POST /leads with a store_write resolver: the POST body lands as a document and the platform mints the id:

1{
2 "method": "POST",
3 "path": "/leads",
4 "resolver": { "type": "store_write", "store_id": "store_…", "collection": "leads", "write_mode": "create" }
5}

The update. POST /leads/{id} with write_mode: merge: the fields sent replace their namesakes on the document the path names and the rest stay:

1{
2 "method": "POST",
3 "path": "/leads/{id}",
4 "resolver": { "type": "store_write", "store_id": "store_…", "collection": "leads", "document_id": "{{path.id}}", "write_mode": "merge" }
5}

On a workspace, owner or user_token API the written document carries user_identity set to the caller, and only they can change it afterwards. Add { "field": "user_identity", "op": "eq", "value": "{{user.sub}}" } to the list’s where and every user sees their own leads.

A write route is never served by a public API, and it counts against the API’s daily_write_cap the way reads and runs count against theirs. Send an Idempotency-Key from the browser and a retried request replays the first answer instead of writing twice.

5. Publish the front end

Upload the built application as kept files under one prefix, and serve the tree from one route:

POST
/v1/files
1curl -X POST https://api.speechify.ai/v1/files \
2 -H "Authorization: Bearer <token>" \
3 -H "Content-Type: multipart/form-data" \
4 -F file=@"[object Object]" \
5 -F kind='{
6 "type": "json"
7}' \
8 -F path='{
9 "type": "json"
10}' \
11 -F project_id='{
12 "type": "json"
13}' \
14 -F user_identity='{
15 "type": "json"
16}'
1{
2 "method": "GET",
3 "path": "/app/*",
4 "resolver": { "type": "file", "file_root": "dash", "file_index": "index.html" }
5}

/app/ answers dash/index.html, /app/assets/main.js answers dash/assets/main.js, and a deep link with no file behind it answers the entry document, so a client-routed app survives a refresh. A rebuild that renames its assets uploads new files and touches no route. Add the API’s own origin to cors_origins if the front end is served from somewhere else; served from /app/* it is same-origin and needs nothing.

6. Call it

From the published front end, on a workspace API:

$curl "https://acme-leads.apis.speechify.ai/summary" \
> -H "Authorization: Bearer $SPEECHIFY_API_KEY"
$curl -X POST "https://acme-leads.apis.speechify.ai/leads" \
> -H "Authorization: Bearer $SPEECHIFY_API_KEY" \
> -H "Content-Type: application/json" \
> -H "Idempotency-Key: lead-7f3a" \
> -d '{"name": "Ada", "stage": "new", "value": 1200}'

On a user_token API, replace the key with the JWT your backend signed for the user, and the same calls answer only that person’s leads.

The API’s own OpenAPI document is at /openapi.json, so a coding agent can generate a client for the routes you assembled.

What to watch

GET
/v1/apis/:api_id/usage
1curl https://api.speechify.ai/v1/apis/api_01jqr8x9zg5k2m3n4p5q6r7s8t/usage \
2 -H "Authorization: Bearer <token>"

Reads, runs and writes today against each cap, per route. A response served from the cache is not a read; a replayed write is not a write.

Where the rest lives