Conventions
These rules apply across the whole API. Endpoint pages only call out exceptions.
Versioning
Section titled “Versioning”All routes live under /api/v1, and every response from your library carries the header X-PH-API: 1.
You may declare the major version you speak by sending X-PH-API: 1. The /api/v1 path already counts as that declaration, so the header is optional. If a request declares a different major (in the header or the path), or sends a malformed X-PH-API value, the library refuses it without processing it:
{ "error": { "code": "UPGRADE_REQUIRED", "message": "This library speaks API major 1. Update the client ... the request was not processed.", "details": { "server_major": 1, "declared": "2", "source": "header" } }}That is a 426 response. Within a major version, new fields can appear in responses and new values can appear in open enums (entry types, activity event types, notification reasons, change-feed entities). Ignore fields and values you do not recognize.
You can also send X-PH-Client: <name>/<version> (for example asset-sync/1.2.0) to identify your integration. It is informational.
Requests and responses
Section titled “Requests and responses”| Rule | Detail |
|---|---|
| JSON bodies | Send Content-Type: application/json. Unknown fields in request bodies are ignored unless an endpoint says otherwise. |
| Partial updates | PATCH bodies change only the fields you send. Sending null clears a nullable field; omitting it leaves it alone. At least one field is required. |
| Binary uploads | Thumbnails, previews, attachments, search-by-image queries and blob uploads take the raw bytes as the request body (not multipart). |
| Empty success | Deletes and idempotent membership changes return 204 No Content. |
| Hashes | Lowercase hex SHA-256 of the exact file bytes (64 characters). |
| Timestamps | UTC ISO 8601 with Z, for example 2026-07-25T12:00:00Z. |
| User stubs | People appear as {"id", "name", "display_name"}. id is the library-local user id. |
| Validation | A body or query parameter that fails validation returns 422 VALIDATION with the failures listed in details.errors. |
Pagination
Section titled “Pagination”List endpoints take limit and return a cursor for the next page. There are three cursor styles; each endpoint page says which one it uses.
| Style | Used by | How it works |
|---|---|---|
| Opaque cursor | GET /entries, /packs, /tags, /collections, /pull-jobs |
Pass the returned next_cursor string back as cursor. null means the last page. Do not parse or construct it. |
| Id cursor (newest first) | GET /activity, /notifications, /review-requests |
next_cursor is an integer id; pass it back as cursor to get older items. null means the feed is exhausted. |
| Sequence cursor | GET /catalog/changes |
cursor is the seq of the last change you applied. See Activity and changes. |
Unless an endpoint says otherwise, limit defaults to 50 and accepts 1 to 200.
A search cursor is tied to the sort it was created with. Reusing a cursor with a different sort or order returns 422 VALIDATION (Pagination cursor does not match the requested sort.).
# First pagecurl -H "Authorization: Bearer $PH_KEY" \ "https://api.prophouse.dev/api/v1/entries?sort=date_updated&order=desc&limit=100"
# Next page: same query plus the cursorcurl -H "Authorization: Bearer $PH_KEY" \ "https://api.prophouse.dev/api/v1/entries?sort=date_updated&order=desc&limit=100&cursor=<next_cursor>"Downloads and presigned URLs
Section titled “Downloads and presigned URLs”File bytes (blobs, thumbnails, previews, preview artifacts, comment attachments) are not streamed through the API. The endpoint answers 302 Found with a Location pointing at a short-lived presigned URL, and your client fetches the bytes from there.
Redirect responses carry Cache-Control: private, no-store. Do not cache the redirect or the URL; request a fresh one each time.
Paths the API returns inside JSON (for example blob_url in a check-out) are same-origin API paths such as /api/v1/blobs/<hash>. Request those with your key as usual.
Uploads
Section titled “Uploads”Blob uploads for pushes are usually direct to storage as well: POST /push/negotiate can return presigned PUT targets with headers you must send verbatim. The full flow is on Push (uploads).
Write availability
Section titled “Write availability”Reads keep working in every account state. Writes can be refused while the workspace is read-only for billing reasons (403, details.reason: "read_only"), while storage is at its limit (403, details.reason: "ingest_blocked", which blocks new uploads only), or for a few seconds while your library is starting (503, details.reason: "policy_unavailable", with Retry-After: 5). See Errors and rate limits.
