opencloudportDocs

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}/export streams a copy of the database as a .sql file (content-disposition: attachment): tables, their rows as INSERTs, then indexes, triggers and views. Built with paged queries, so the database keeps answering while it runs. Read-only keys may export.
  • GET …/export-link answers { "data": { "url", "expires_at", "expires_in" } }: a link to a fresh copy that works without a key for 15 minutes.
  • POST …/import runs a SQL file (the body, as application/sql or text/plain, or JSON { "sql": "…" }), up to 10 MB, in batches. Answers { "data": { "statements", "rows_written" } }. A failing statement stops it with 400 query_failed, saying how far it got; what ran before stays. BEGIN, COMMIT and PRAGMA lines 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.

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-domain with { "domain": "bakery.com", "default_from"?: "hello" } sets it up (or changes default_from). It answers the domain with status: "pending" and the DNS records to add (type, name, value, priority for MX, purpose: spf, dkim, dmarc or bounces). Replacing it with another domain starts again.
  • GET …/sender-domain answers it (or null), checking again while it's pending: pending, verified, or failed with problem saying why.
  • DELETE …/sender-domain goes 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.

On this page