Data
Run SQL on a database, manage the files in a bucket, and send email.
These calls work on one service: {serviceId} is its svc_… id. A key made for just that service can use them.
Run SQL
POST /api/v1/apps/{id}/services/{serviceId}/query
Prop
Type
{
"data": [
{ "columns": ["item", "qty"], "rows": [["Sourdough", 2]], "truncated": false, "rows_read": 1, "rows_written": 0 }
],
"duration_ms": 3
}One result per statement, each with up to 1,000 rows (truncated says if there were more). A SQL error is 400 query_failed, with the database's own message.
Reading counts as reading
SQL that only reads counts as a read, though it's a POST: every statement a SELECT, WITH … SELECT, VALUES, EXPLAIN, or a reading PRAGMA (table_info, index_list, foreign_key_list…), and no word that changes data (INSERT, UPDATE, DELETE, CREATE, DROP…) outside strings and comments. So:
- Read-only keys can run it; anything else from them is
403 key_out_of_scope. - Spending limits don't stop it: a key at its limit can still read.
- The activity log leaves it out, as it does every read.
The check errs on the side of "changes": if a read trips it, run it as a change. Agents get two tools for this: query_database reads, and execute_sql changes data (it asks the person first).
Migrations
Apply schema migrations in order, each once. Applied names are kept in a d1_migrations table, the same one wrangler d1 migrations apply uses, so you can switch between the two:
curl https://opencloudport.com/api/v1/apps/$APP_ID/services/$DB_ID/migrations \
-H "Authorization: Bearer $OCP_KEY" \
-H "Content-Type: application/json" \
-d '{ "migrations": [
{ "name": "0001_init.sql", "sql": "create table orders (id integer primary key, item text)" },
{ "name": "0002_email.sql", "sql": "alter table orders add column email text" }
] }'Send every migration each time: the ones already applied are skipped. It answers { "data": { "applied": [...], "skipped": [...], "migrations": [...] } }. The first one that fails stops the run with 400 migration_failed and the database's message; the ones before it stay applied. A database can't undo part of a migration, so keep each one small. GET …/migrations lists what's applied. Over MCP: apply_migrations (asks first) and list_migrations.
Copies and loading SQL files
GET /api/v1/apps/{id}/services/{serviceId}/exportstreams a copy of the database as a.sqlfile (content-disposition: attachment): tables, their rows asINSERTs, then indexes, triggers and views. Built with paged queries, so the database keeps answering while it runs. Read-only keys may export.GET …/export-linkanswers{ "data": { "url", "expires_at", "expires_in" } }: a link to a fresh copy that works without a key for 15 minutes.POST …/importruns a SQL file (the body, asapplication/sqlortext/plain, or JSON{ "sql": "…" }), up to 10 MB, in batches. Answers{ "data": { "statements", "rows_written" } }. A failing statement stops it with400 query_failed, saying how far it got; what ran before stays.BEGIN,COMMITandPRAGMAlines are skipped.
Files
List
GET /api/v1/apps/{id}/services/{serviceId}/files
Prop
Type
{ "object": "list", "data": [{ "object": "file", "key": "uploads/cake.jpg", "size": 120000, "uploaded_at": "…" }], "next_cursor": null, "has_more": false }Delete
DELETE /api/v1/apps/{id}/services/{serviceId}/files?key=uploads/cake.jpg. Deleting a file that isn't there succeeds.
Signed links
POST /api/v1/apps/{id}/services/{serviceId}/signed-url returns a link that downloads, uploads or deletes one file until it expires. Hand it to a browser for direct uploads.
Prop
Type
{ "data": { "url": "https://…", "method": "PUT", "key": "uploads/photo.jpg", "expires_at": "…" } }Temporary credentials
POST /api/v1/apps/{id}/services/{serviceId}/credentials returns S3 credentials that reach only this bucket, for any S3 tool or library.
Prop
Type
{ "data": { "access_key_id": "…", "secret_access_key": "…", "session_token": "…", "endpoint": "https://…", "bucket": "…", "region": "auto", "expires_at": "…" } }Where these can't be issued (local development), you get 501 not_supported; use signed links instead.
Changing files (delete, signed links, credentials) needs a key that isn't read-only.
Send email
POST /api/v1/apps/{id}/services/{serviceId}/send sends one email from the app's own address, or from the service's own domain once it's verified (see below).
Prop
Type
Answers 202 with { "data": { "id": "msg_…", "object": "email" } }. Each email is billed as one email.sent, to the key that sent it. Not sent while the app is paused (402 app_paused); if the provider refuses it (502 email_failed), nothing is billed.
Sender domain
An email service can send from its own domain once it's verified.
PUT /api/v1/apps/{id}/services/{serviceId}/sender-domainwith{ "domain": "bakery.com", "default_from"?: "hello" }sets it up (or changesdefault_from). It answers the domain withstatus: "pending"and the DNSrecordsto add (type,name,value,priorityfor MX,purpose:spf,dkim,dmarcorbounces). Replacing it with another domain starts again.GET …/sender-domainanswers it (ornull), checking again while it's pending:pending,verified, orfailedwithproblemsaying why.DELETE …/sender-domaingoes back to the app's own address.
Each domain can be used by one email service across all workspaces (409 domain_taken). Addresses like URLs, IPs and our own domains are refused with 422 invalid_domain.
opencloudport