Endpoint and keys
Every table and view in your database is an endpoint at https://api.hornethive.io/v1/rest/{database id}/{table}. The database id is on its page in the dashboard. Each request carries an API key from Settings → API keys as Authorization: Bearer <key>.
A key has the read scope, or read and write, and can be limited to one database or to a list of IP addresses. A read key can be used from the browser, from the origins you register; keep write keys on your server. The syntax follows PostgREST, so tools and habits from it carry over; the differences are listed at the end.
The API used to be at https://hornethive.io/api/…. Until 7 November 2026 it still answers, with Deprecation and Sunset headers and a Link to the same call here; from that day (midnight, Malaysia time) it answers 410 with the new address. Drop the /api and change the host: hornethive.io/api/rest/… is api.hornethive.io/v1/rest/….
DB=<database id>
KEY=<api key>
curl "https://api.hornethive.io/v1/rest/$DB/books" -H "Authorization: Bearer $KEY"Reading rows
GET /{table} returns an array of rows. select picks the columns, limit takes 1 to 1,000 rows (10 when left out), and offset skips rows for paging. GET /{table}/{key} returns one row by its primary key; a view, or a table without one, matches a column named id.
Lists are cached for 5 minutes and cleared by any write through the API. A write made straight to Postgres (psql, an ORM) is not seen by the cache, so a list can be up to 5 minutes old after one. The X-Cache header says HIT or MISS.
curl "https://api.hornethive.io/v1/rest/$DB/books?select=id,title,year&limit=20&offset=40" -H "Authorization: Bearer $KEY"
curl "https://api.hornethive.io/v1/rest/$DB/books/42" -H "Authorization: Bearer $KEY"Filters
A filter is column=operator.value. Filters on different columns, and repeats of one column, must all match. A value with no operator is equality, so status=active still works as it always has.
| Operator | Means | Example |
|---|---|---|
| eq | equals | status=eq.active |
| neq | not equal | status=neq.draft |
| gt, gte | greater than, or equal | price=gt.100 |
| lt, lte | less than, or equal | age=lte.65 |
| like, ilike | pattern, ilike ignores case; * is the wildcard | name=ilike.*ali* |
| match, imatch | regular expression, imatch ignores case | sku=match.^AB[0-9]+$ |
| in | one of a list | id=in.(1,2,3) |
| is | null, true, false or unknown | deleted_at=is.null |
| cs, cd | contains, contained by (arrays, ranges, jsonb) | tags=cs.{red,blue} |
| ov | overlaps (arrays, ranges) | tags=ov.{red,blue} |
| fts, plfts, phfts, wfts | full-text search: to_tsquery, plain, phrase, websearch | body=wfts(english).hive hornet |
| not. | negates any of the above | deleted_at=not.is.null |
In an in list or an or group, put double quotes around a value that holds a comma or a parenthesis: in.("Kuala Lumpur, MY",Ipoh). Values are sent to Postgres as parameters, never as SQL.
# between 10 and 50, not deleted, name containing "tea"
curl "https://api.hornethive.io/v1/rest/$DB/products?price=gte.10&price=lte.50&deleted_at=is.null&name=ilike.*tea*" \
-H "Authorization: Bearer $KEY"OR and nested conditions
or=(…) matches rows that pass any of its conditions, written column.operator.value and separated by commas. Groups nest with and(…) and or(…), and not. negates one: not.and=(…), or=(a.eq.1,not.and(b.gt.2,c.is.null)).
# under 18 or over 65, or a member whose plan is pro
curl "https://api.hornethive.io/v1/rest/$DB/people?or=(age.lt.18,age.gt.65,and(member.is.true,plan.eq.pro))" \
-H "Authorization: Bearer $KEY"Ordering, paging and counts
order=col.desc,other.asc sorts by one column or more; each takes asc or desc, and nullsfirst or nullslast. The older orderBy=col&order=desc still works.
Send Prefer: count=exact for the number of matching rows in the Content-Range header, as first-last/total: 0-19/240. Without it the total is *. Browsers can read the header from your registered origins.
curl -i "https://api.hornethive.io/v1/rest/$DB/orders?order=created_at.desc,id.desc&limit=20&offset=20" \
-H "Authorization: Bearer $KEY" -H "Prefer: count=exact"
# Content-Range: 20-39/240Inserting rows
POST /{table} with a JSON object inserts one row and returns it; with an array it inserts them all in one statement and returns an array. Each value takes its column's type: a JSON object fills a jsonb column, a JSON array a Postgres array, a string a timestamp or a bytea written as \x0102. Needs the write scope.
The columns are every key that appears in any row. A column that no row names takes its default; a column that some rows leave out is NULL in those rows, as in PostgREST, so send the same keys in every row when a column has a default. select picks the columns returned, and Prefer: return=minimal returns nothing.
curl -X POST "https://api.hornethive.io/v1/rest/$DB/books" -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '[{"title":"Dune","year":1965},{"title":"Ubik","year":1969}]'Upserts
Add Prefer: resolution=merge-duplicates to update the rows whose key already exists and insert the rest, or resolution=ignore-duplicates to leave existing rows alone. The key is the primary key; on_conflict=email names another unique column or columns. Only the columns you send are changed.
curl -X POST "https://api.hornethive.io/v1/rest/$DB/members?on_conflict=email" -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" -H "Prefer: resolution=merge-duplicates" \
-d '[{"email":"[email protected]","plan":"pro"}]'Updating and deleting
PATCH /{table} sets the columns in the JSON body on every row that matches the filters, and returns those rows. DELETE /{table} removes them and returns {"deleted": n}, or the rows themselves with Prefer: return=representation. Both take any filter above, and both refuse a request with no filter, so one call cannot change a whole table by accident.
curl -X PATCH "https://api.hornethive.io/v1/rest/$DB/orders?status=eq.pending&created_at=lt.2026-10-01" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"status":"expired"}'
curl -X DELETE "https://api.hornethive.io/v1/rest/$DB/sessions?expires_at=lt.now" -H "Authorization: Bearer $KEY" \
-H "Prefer: return=representation"Calling functions
POST /rpc/{function} calls a Postgres function with named arguments from the JSON body; GET /rpc/{function}?arg=value does the same read-only. A function that returns a set gives an array of rows, a composite type an object, a scalar its value, and void an empty 204.
A key without the write scope runs the function in a read-only transaction, as does every GET, so it cannot change data. Use functions for joins, aggregates and anything else a single table cannot express.
# CREATE FUNCTION top_books(n int) RETURNS SETOF books LANGUAGE sql STABLE
# AS 'SELECT * FROM books ORDER BY rating DESC LIMIT n';
curl -X POST "https://api.hornethive.io/v1/rest/$DB/rpc/top_books" -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" -d '{"n":5}'Other schemas
Tables are read from the public schema. Accept-Profile: shop reads from another schema, and Content-Profile: shop writes to it, for tables and functions alike. Postgres's own schemas (pg_catalog, information_schema) are not served.
curl "https://api.hornethive.io/v1/rest/$DB/orders" -H "Authorization: Bearer $KEY" -H "Accept-Profile: shop"Raw SQL
POST /query runs one SQL statement with $1, $2… parameters and returns {"columns": [...], "rows": [...]}. A key without the write scope runs it in a read-only transaction.
curl -X POST "https://api.hornethive.io/v1/rest/$DB/query" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"query":"SELECT author, count(*) FROM books WHERE year > $1 GROUP BY author","params":[2000]}'What values come back as
Integers, floats and booleans are JSON numbers and booleans. Timestamps and dates are ISO 8601 strings. numeric is a string, so no digit is lost. json and jsonb come back as a JSON string to parse; arrays in their Postgres text form, such as {red,blue}; bytea as hex. The OpenAPI document below states each column's type.
Errors
A refused request answers {"error": "…"}. When Postgres refused the statement the body also has code, its SQLSTATE, and details and hint when it gave them.
400 is a bad request or statement, 401 a missing or wrong key, 403 a key without the scope or for another database (or a privilege Postgres refused), 404 no such database, table, function or row, 409 a unique or foreign key conflict, and 429 the rate limit.
{"error":"write failed: pq: duplicate key value violates unique constraint \"members_pkey\"",
"code":"23505","details":"Key (email)=([email protected]) already exists."}OpenAPI
GET /openapi.json returns an OpenAPI 3.1 document for your database, built from its schema when you ask: every table with its columns and types, which ones are required on insert, the primary keys, and every function with its arguments. With Accept-Profile it describes another schema. Load it in Swagger UI, Postman or Insomnia, or generate a typed client with openapi-generator or openapi-typescript.
The same operations for any database, with no key needed, are at https://api.hornethive.io/v1/rest/openapi.json.
curl "https://api.hornethive.io/v1/rest/$DB/openapi.json" -H "Authorization: Bearer $KEY" -o openapi.json
npx openapi-typescript openapi.json -o hive.d.tsFrom JavaScript
@hornetcloud/hive-js wraps all of this: every filter above, or(), getWithCount(), bulk insert(), upsert(), rpc(), schema() and openapi().
Differences from PostgREST
- No embedded relations (
select=*,orders(*)): join in a view or a function. The GraphQL API does not follow foreign keys either. - No JSON paths in
selector filters (data->>name), and no aliases or casts inselect. limitis 1 to 1,000 and 10 by default; PostgREST has no default. Page withoffsetand the count.- No
Rangerequest header, no CSV or single-object responses, and onlycount=exact. - A value with no operator is equality, where PostgREST refuses it.
- Lists are cached for up to 5 minutes, as above; PostgREST does not cache.
- Every key acts as the database's owner. Row-level security has no per-user role to act on, so keep write keys on your server.
POST /queryruns SQL, which PostgREST does not offer.