Docs

Data API

Read a project's schema, read and write rows, change tables, run SQL, import a dump and export the database, from outside the project.

Updated October 12, 2026
On this page
  1. List tables
  2. List views
  3. List indexes
  4. Read rows
  5. Insert rows
  6. Update rows
  7. Delete rows
  8. Change the schema
  9. Run a query
  10. Run a batch
  11. Import a SQL script
  12. Export the database
  13. Read backup status

Every project has its own PostgreSQL database. These routes manage it from outside the project with your account token — for scripts, CI and admin tools. Code running inside the project uses the server data API on loopback instead, and a browser uses the frontend data API with the publishable key.

GET/projects/{name}/tablesTables, their columns and foreign keys
GET/projects/{name}/viewsViews and their definitions
GET/projects/{name}/indexesIndexes you created
GET/projects/{name}/tables/{table}/rowsRead a page of rows
POST/projects/{name}/tables/{table}/rowsInsert a row, or several
PATCH/projects/{name}/tables/{table}/rowsUpdate rows matching a filter
DELETE/projects/{name}/tables/{table}/rowsDelete rows matching a filter
POST/projects/{name}/sqlRun schema changes: create, alter, drop
POST/projects/{name}/queryRun one SQL statement and get its result
POST/projects/{name}/batchRun several statements in one transaction
POST/projects/{name}/importImport a SQL script
GET/projects/{name}/exportDownload the whole database
GET/projects/{name}/backupDatabase size and export formats

A table name that is not in the schema is refused with 400 and no table named {table}; table and column names are checked against the schema before they reach SQL, and values are always bound.

List tables

GET/projects/{name}/tablesTables, their columns and foreign keys

The tables in the public schema, read from the database itself.

namestringRequired
Path parameter. The project's name.
Terminal
curl https://api.fortly.com/projects/my-app-ada/tables \
  -H "Authorization: Bearer $FORTLY_TOKEN"
JSON
{
  "tables": [
    {
      "name": "orders",
      "columns": [
        { "name": "id", "type": "integer", "not_null": true, "primary_key": true, "default_value": null },
        { "name": "customer_id", "type": "integer", "not_null": true, "primary_key": false, "default_value": null },
        { "name": "total", "type": "numeric", "not_null": false, "primary_key": false, "default_value": null },
        { "name": "created", "type": "timestamp with time zone", "not_null": true, "primary_key": false, "default_value": "now()" }
      ],
      "links": [
        { "column": "customer_id", "table": "customers", "to": "id", "on_delete": "CASCADE", "on_update": "" }
      ]
    }
  ]
}

links are the table's declared foreign keys: column in this table points at column to of table. on_delete and on_update are the SQL action — CASCADE, SET NULL or SET DEFAULT — or empty for NO ACTION and RESTRICT, which behave as an omitted clause does.

List views

GET/projects/{name}/viewsViews and their definitions

Views and materialized views in the public schema, with their definitions as PostgreSQL prints them.

Terminal
curl https://api.fortly.com/projects/my-app-ada/views \
  -H "Authorization: Bearer $FORTLY_TOKEN"
JSON
{
  "views": [
    { "name": "paid_orders", "sql": " SELECT id,\n    total\n   FROM orders\n  WHERE status = 'paid'::text;" }
  ]
}

List indexes

GET/projects/{name}/indexesIndexes you created

Indexes in the public schema that you created. The indexes PostgreSQL builds for primary keys and unique constraints are left out.

Terminal
curl https://api.fortly.com/projects/my-app-ada/indexes \
  -H "Authorization: Bearer $FORTLY_TOKEN"
JSON
{
  "indexes": [
    { "name": "orders_created_idx", "table": "orders", "unique": false, "columns": ["created"] }
  ]
}

columns holds each indexed column or expression, so a functional index reads like ["lower(email)"].

Read rows

GET/projects/{name}/tables/{table}/rowsRead a page of rows

A page of rows, with the total that match. Filtering, sorting, column selection and embedding related tables use the same query grammar as the server data API.

namestringRequired
Path parameter. The project's name.
tablestringRequired
Path parameter. The table.
limitinteger
Rows per page. Default 50; values are clamped to 1–200.
offsetinteger
Rows to skip. Default 0.
orderstring
column, column.asc or column.desc, optionally followed by .nullsfirst or .nullslast. Comma-separate several.
selectstring
Columns to return, comma-separated, such as id,total. A related table in parentheses embeds it through a declared foreign key: id,customers(name).
{column}string
A filter, written column=op.value: status=eq.paid, total=gte.100, id=in.(1,2,3), archived=is.null. Prefix not. to negate. Every filter must match.
Terminal
curl "https://api.fortly.com/projects/my-app-ada/tables/orders/rows?status=eq.paid&order=created.desc&limit=20" \
  -H "Authorization: Bearer $FORTLY_TOKEN"
JSON
{
  "rows": [
    { "id": 42, "customer_id": 7, "total": 129.5, "status": "paid", "created": "2026-10-11T14:02:11.482+00:00" }
  ],
  "limit": 20,
  "offset": 0,
  "total": 1
}

total is the number of rows matching the filters, ignoring limit and offset. A filter on a column that does not exist, an unknown operator or a malformed value answers 400 with the reason.

Insert rows

POST/projects/{name}/tables/{table}/rowsInsert a row, or several

The body is one JSON object for one row, or an array of objects for several. Several rows are inserted in one transaction: all of them or none. Columns you leave out get their default, so an identity primary key fills itself in.

namestringRequired
Path parameter. The project's name.
tablestringRequired
Path parameter. The table.
Terminal
curl -X POST https://api.fortly.com/projects/my-app-ada/tables/orders/rows \
  -H "Authorization: Bearer $FORTLY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"customer_id":7,"total":129.5,"status":"paid"}'

Answers 201 with the row as the database stored it, every column included — an object for an object, an array for an array:

JSON
{ "id": 43, "customer_id": 7, "total": 129.5, "status": "paid", "created": "2026-10-12T09:30:00.117+00:00" }

A constraint the database enforces — a duplicate key, a missing not null value, a foreign key with nothing at the other end — answers 400 with the database's message. An empty array answers 400 with nothing to insert.

Update rows

PATCH/projects/{name}/tables/{table}/rowsUpdate rows matching a filter

Sets the columns in the body on every row the filters match. Columns not in the body keep their values.

namestringRequired
Path parameter. The project's name.
tablestringRequired
Path parameter. The table.
{column}stringRequired
At least one filter, as in Read rows. Without one the request is refused with an update needs a filter, because it would rewrite the whole table.

The body is a JSON object of column names and new values. A name that is not a column of the table answers 400.

Terminal
curl -X PATCH "https://api.fortly.com/projects/my-app-ada/tables/orders/rows?id=eq.43" \
  -H "Authorization: Bearer $FORTLY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status":"refunded"}'
JSON
{ "updated": 1 }

Delete rows

DELETE/projects/{name}/tables/{table}/rowsDelete rows matching a filter

Deletes every row the filters match.

namestringRequired
Path parameter. The project's name.
tablestringRequired
Path parameter. The table.
{column}stringRequired
At least one filter, as in Read rows. Without one the request is refused with a delete needs a filter.
Terminal
curl -X DELETE "https://api.fortly.com/projects/my-app-ada/tables/orders/rows?status=eq.cancelled" \
  -H "Authorization: Bearer $FORTLY_TOKEN"
JSON
{ "deleted": 3 }

Change the schema

POST/projects/{name}/sqlRun schema changes: create, alter, drop

Runs migrations. The SQL may hold several statements, and every one of them must start with create, alter or drop; anything else refuses the whole request before any of it runs. Read and write rows through the routes above, or use Run a query.

namestringRequired
Path parameter. The project's name.
sqlstringRequired
The statements, separated by semicolons.

The SQL reaches PostgreSQL as written. id integer primary key means a column you must fill in yourself; use integer generated by default as identity primary key for one that fills itself in.

Terminal
curl -X POST https://api.fortly.com/projects/my-app-ada/sql \
  -H "Authorization: Bearer $FORTLY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"sql":"create table customers (id integer generated by default as identity primary key, name text not null); create index customers_name_idx on customers (name);"}'

Answers with every table now in the database:

JSON
{ "ok": true, "tables": ["customers", "orders"] }

A statement of another kind answers 400 with only create, alter and drop are accepted here; one PostgreSQL refuses answers 400 with its message.

Run a query

POST/projects/{name}/queryRun one SQL statement and get its result

Runs one statement of any kind and returns what it produced. This is the console's query editor; use it for ad-hoc reads, reports and one-off fixes.

namestringRequired
Path parameter. The project's name.
sqlstringRequired
One SQL statement.
Terminal
curl -X POST https://api.fortly.com/projects/my-app-ada/query \
  -H "Authorization: Bearer $FORTLY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"sql":"select status, count(*) from orders group by status"}'

A statement that returns rows answers with them, at most 500. truncated is true when there were more:

JSON
{
  "columns": ["status", "count"],
  "rows": [
    { "status": "paid", "count": 118 },
    { "status": "refunded", "count": 4 }
  ],
  "truncated": false,
  "took_ms": 3
}

A statement that returns no rows answers with how many it changed:

JSON
{ "columns": [], "rows": [], "changed": 12, "took_ms": 5 }

A statement that runs longer than 15 seconds answers 400 with that query took too long and was stopped. A result with two columns of the same name is refused; alias one of them. An empty sql answers 400 with there is nothing to run.

Run a batch

POST/projects/{name}/batchRun several statements in one transaction

Runs up to 200 statements in order, in one transaction. If any fails, none of them is applied.

namestringRequired
Path parameter. The project's name.
statementsstring[]Required
The statements, one per item. Empty items are ignored.
Terminal
curl -X POST https://api.fortly.com/projects/my-app-ada/batch \
  -H "Authorization: Bearer $FORTLY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"statements":["alter table orders add column note text","update orders set note = '\''imported'\'' where note is null"]}'

results holds one query result per statement, in order:

JSON
{
  "results": [
    { "columns": [], "rows": [], "changed": 0, "took_ms": 4 },
    { "columns": [], "rows": [], "changed": 122, "took_ms": 9 }
  ]
}

A failing statement answers 400 naming its position, such as statement 2: …. No statements answers 400 with there is nothing to run; more than 200 with at most 200 statements in one batch.

Import a SQL script

POST/projects/{name}/importImport a SQL script

Runs a SQL script — a dump from any PostgreSQL, including a Fortly export — against the database as it is. Nothing is emptied first. The whole script runs in one transaction, so a script that stops partway leaves the database exactly as it was. See Importing a database for preparing a dump.

Only the account owner or a team admin can import; a member gets 403.

namestringRequired
Path parameter. The project's name.

The body is the script itself, as UTF-8 text, up to 256 MB. Lines that only make sense to psql or to a session are skipped: backslash commands, SET (but not SET CONSTRAINTS), select pg_catalog.set_config(…), and the dump's own BEGIN, COMMIT, END and ROLLBACK. Everything else runs, including GRANT, COMMENT ON and ALTER TABLE.

Terminal
curl -X POST https://api.fortly.com/projects/my-app-ada/import \
  -H "Authorization: Bearer $FORTLY_TOKEN" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @dump.sql
JSON
{ "imported": "statements", "ran": 1284, "skipped": 9, "tables": 14 }

ran is statements sent to the database, skipped the lines left out as above, and tables the number of tables afterwards.

StatusWhen
400The file is empty, binary (such as a SQLite database file) or not UTF-8 text, or a statement was refused. The error names the statement and how many ran before it; none of them is kept.
403You are a member, not the owner or an admin.
413The file is over 256 MB.

Export the database

GET/projects/{name}/exportDownload the whole database

The whole database as a file: schema and data.

namestringRequired
Path parameter. The project's name.
formatstring
postgres (default; also postgresql, pg), sql (the same dialect), mysql (also mariadb), or json.
gzipstring
1, true or yes to compress it.
Terminal
curl -OJ "https://api.fortly.com/projects/my-app-ada/export?format=postgres&gzip=1" \
  -H "Authorization: Bearer $FORTLY_TOKEN"

The response body is the file, not JSON:

HTTP
HTTP/1.1 200 OK
Content-Type: application/sql
Content-Disposition: attachment; filename="my-app-ada-2026-10-12.sql"

Content-Type is application/sql for the SQL formats, application/json for json, and application/gzip when compressed, in which case the filename ends in .gz. An unknown format answers 400.

Read backup status

GET/projects/{name}/backupDatabase size and export formats

What can be taken from this database, and how big it is.

Terminal
curl https://api.fortly.com/projects/my-app-ada/backup \
  -H "Authorization: Bearer $FORTLY_TOKEN"
JSON
{
  "automatic": false,
  "size_bytes": 8028160,
  "formats": ["sql", "postgres", "mysql", "json"]
}

automatic is false: no continuous copy of the database is taken, so export is how you keep one. See Backups.

Something missing or unclear? Tell us and we will fix the page.© 2026 Fortly