# ZoomHub API reference ZoomHub’s REST API turns large images and PDFs into zoomable images to share and embed. This is the index of the Markdown edition of its [reference](https://zoomhub.net/docs/api): what every endpoint shares, then one file per resource. Examples use curl. > **Beta:** The v2 API is in beta: endpoints, attributes and error codes may still change before it is final. Questions or feedback: [daniel@zoomhub.net](mailto:daniel@zoomhub.net). ## Instructions for agents - The API is at `https://api.zoomhub.net`; pages, share links and embeds are on `https://zoomhub.net`. - The API is part of the [Business plan](https://zoomhub.net/pricing): a valid key whose account’s plan doesn’t include it is refused with `403` and the error code `plan_required`. Reads need no key, unless what they read is Protected (password-protected). - Send `Authorization: Bearer `, reading the key from an environment variable such as `ZOOMHUB_API_KEY`. Never put a key in source code or in code that runs in a browser: call the API from a server. People create keys on their [API keys page](https://zoomhub.net/account/api-keys). - Send bodies as JSON, with `Content-Type: application/json`; answers are JSON too. A file goes straight to storage, with a `PUT` of each part to its own URL: follow the curl walkthrough in [Uploads](https://zoomhub.net/docs/api/uploads.md). - Processing is asynchronous: poll the asset until its `status` is `succeeded` or `failed`, waiting longer between attempts. - On a refusal, branch on the HTTP status and `code`, never on the prose in `detail`. - Ask the user before deleting an asset, a collection or a viewer: a deletion can’t be undone. - Each resource has its own Markdown file: fetch only the ones the task needs. ## Introduction The ZoomHub API turns large images and PDFs into zoomable images you can embed anywhere, gathers them into collections, and configures the viewers that show them. It speaks JSON over HTTPS and answers with standard HTTP status codes. Create [assets](https://zoomhub.net/docs/api/assets.md#assets) from images on the web, or [upload](https://zoomhub.net/docs/api/uploads.md#uploads) files, and get back zoomable images with a page to share and a script to embed. [Collections](https://zoomhub.net/docs/api/collections.md#collections), [collection items](https://zoomhub.net/docs/api/collection-items.md#collection-items) and [viewers](https://zoomhub.net/docs/api/viewers.md#viewers) arrange your images into sets and configure how they are shown. Every endpoint is at this base URL: ```http https://api.zoomhub.net ``` The examples on this page call it with `curl`, JavaScript’s `fetch` and Python’s `requests`, and read your API key from the `ZOOMHUB_API_KEY` environment variable. ## Authentication Authenticate with an API key, sent as a [Bearer token](https://datatracker.ietf.org/doc/html/rfc6750) in the `Authorization` header. The scheme name `Bearer` is case-insensitive. ```http Authorization: Bearer sk-zh-v1-… ``` Create and revoke keys on your [API keys page](https://zoomhub.net/account/api-keys). An account holds up to 5 keys at a time. The API is part of the [Business plan](https://zoomhub.net/pricing): a valid key whose account’s plan doesn’t include it is refused with `403` and the error code `plan_required`. ZoomHub stores only a hash of each key, so a key is shown once, when you create it. A lost key can’t be recovered: revoke it and create another. A revoked key stops working at once. > **Warning:** Keep keys out of source code and out of web pages. A key in a page’s JavaScript is a key anyone who opens the page can copy: call the endpoints that need one from your server. What each endpoint asks for: - Every write requires a key, and so does everything about uploads. What you create with a key belongs to your account. - Reads need no key, unless what they read is Protected (password-protected). Send your key to read your own Protected assets and collections. The API host never reads cookies: a request is authenticated by its `Authorization` header alone, even in a browser where you are signed in to ZoomHub. On endpoints that require a key, a missing key is refused with `401` and the error code `not_authenticated`, an unknown or revoked one with `401` and `invalid_api_key`. On a read, a key that fails counts as no key. ## Hosts The API is served at `https://api.zoomhub.net`. ZoomHub’s pages, share links and embeds are on `https://zoomhub.net`. Responses link to both: a `links.self` URL is an API URL you can call with your key, while `links.share` and the embed links are pages for people. An endpoint called on the other host may be refused with `404` and the error code `wrong_host`, whose detail names the host to use (see [Errors](https://zoomhub.net/docs/api.md#errors)): ```json { "code": "wrong_host", "detail": "This endpoint is served at https://api.zoomhub.net.", "request_id": "4fKx9mQ2TzR7wB1c", "status": 404, "title": "Not Found" } ``` Tiles and their manifests (`dzi.url` and `dzc.url`) are on a content host. Read them, but don’t store their URLs: where an image’s tiles are served from can change, for instance when its owner protects it. Fetch the object again for a current URL. ## Errors ZoomHub answers with conventional HTTP status codes: `2xx` for success, `4xx` for a request that can’t be served as sent, and `5xx` for a problem on ZoomHub’s side. A refusal is a JSON object in the form of [problem details](https://datatracker.ietf.org/doc/html/rfc9457), sent as `application/problem+json`: ```json { "code": "invalid_api_key", "detail": "Invalid API key", "request_id": "4fKx9mQ2TzR7wB1c", "status": 401, "title": "Unauthorized" } ``` | Field | Meaning | | --- | --- | | `title` | The status’s reason phrase. | | `status` | The HTTP status again. | | `detail` | Prose for people, specific to this request. | | `code` | A stable, machine-readable identifier: one of the codes below. | | `request_id` | The response’s `Request-Id` header again: quote it when you report a problem. | Branch on `status` and `code`, never on `title` or `detail`: they are worded for people and may change. Every refusal has this body. A request no endpoint takes, such as one to a path with an ID that can’t be an ID, answers `404` `not_found` for `GET` and `HEAD`, and `405` `method_not_allowed` for any other method; a failure on ZoomHub’s side answers `500` `internal_error`. A `503` is temporary: retry with exponential backoff. When it carries a `Retry-After` header, wait at least that many seconds. Every code a caller can receive: | Code | Status | When | | --- | --- | --- | | `asset_not_ready` | 400 | The asset added to a collection is still processing. Add it once its processing has succeeded, or name it in a batch, which takes assets still processing. | | `asset_processing_failed` | 400 | The asset’s processing failed, so it can’t join a collection. | | `bad_request` | 400 | The request can’t be carried out as sent; the detail says why. For instance: a body that isn’t valid JSON, misses a required key, has a key the endpoint doesn’t know or breaks a limit; an ID in the query that can’t be an ID; a collection asked to add its own members, a viewer’s options sent under the other viewer type, a viewer asked for something without an owner, or a list of viewers that named both filters or neither. | | `file_too_large` | 400 | The upload’s `size_bytes` is larger than the account’s plan takes; the detail says how large a file may be. | | `invalid_url` | 400 | The `original_url` isn’t a valid URL ZoomHub can fetch, or points into ZoomHub’s own uploads: upload the file instead. | | `subject_owner_mismatch` | 400 | A viewer can show only its owner’s collections and images, and the new `collection_id` or `content_id` belongs to someone else. | | `unsupported_content_type` | 400 | The upload’s `content_type` is neither an image type nor `application/pdf`. | | `invalid_api_key` | 401 | The key is unknown or revoked, or the `Authorization` header doesn’t hold a `Bearer` key. | | `not_authenticated` | 401 | The endpoint requires a key and the request has no `Authorization` header. | | `forbidden` | 403 | You may not do this. It refuses a read of a Protected asset or collection you may not view, an asset added to a collection its owner doesn’t own, a write to an asset, collection or viewer whose owner you aren’t, and an email address that may not submit content. | | `plan_required` | 403 | The key is valid, but its account’s plan doesn’t include the API. See [Authentication](https://zoomhub.net/docs/api.md#authentication). | | `subscription_required` | 403 | The owner’s plan doesn’t include a feature the request asks for: password protection, a reference ID, hiding the ZoomHub logo, or a slideshow. The detail names the feature. | | `content_not_found` | 404 | No asset has this ID, or it was deleted. | | `not_found` | 404 | No collection, viewer or upload has this ID, or it was deleted, or an asset isn’t a member of the collection. An upload is its creator’s alone: anyone else’s answers as if it didn’t exist. Also a `GET` or `HEAD` to a path no endpoint takes, such as one with an ID that can’t be an ID. | | `wrong_host` | 404 | The endpoint is served on the other host; the detail names it. See [Hosts](https://zoomhub.net/docs/api.md#hosts). | | `method_not_allowed` | 405 | A request other than `GET` or `HEAD` that no endpoint takes: the path doesn’t take its method, or matches no endpoint. | | `in_foreign_collection` | 409 | The asset can’t be protected while it is a member of a collection someone else owns. Ask them to remove it first. | | `reference_id_taken` | 409 | Another of the owner’s assets already uses this `reference_id`. | | `upload_cancelled` | 409 | The upload was cancelled, so it can’t be completed. Start a new one. | | `upload_completed` | 409 | The upload is completed and its asset made, so it can’t be cancelled. Delete the asset instead. | | `upload_expired` | 409 | The upload expired before it was completed. Start a new one. | | `upload_incomplete` | 409 | S3 lacks one of the upload’s parts, or holds it at another size than `parts` says; the detail names the first such part. Send it again, then complete the upload. | | `url_already_exists` | 409 | Someone else already submitted the URL; the answer doesn’t say which asset it is. Or, completing an upload, another asset already holds its file: start a new upload. | | `collection_full` | 422 | The change would take a collection past what its owner’s plan holds: 1,000 members on Business. Nothing was added; the detail says how many fit and which plan holds more. | | `idempotency_key_reused` | 422 | The `Idempotency-Key` named an upload of another file. Send a new key for a new upload. | | `upload_limit_reached` | 429 | The account, or the email address, has reached its upload limit. | | `internal_error` | 500 | Something failed on ZoomHub’s side. Retry later; if it keeps happening, reach out. | | `protection_pending` | 503 | The change involves a Protected asset and the CDN wasn’t updated in time. An asset’s protection still took effect, and ZoomHub finishes the update within a minute; retrying is safe. A collection change saved nothing: retry it. | | `protection_unavailable` | 503 | Password protection can’t be changed right now. Nothing was changed; retry later. | | `service_unavailable` | 503 | ZoomHub couldn’t carry out the request right now, and nothing was saved. Retry later; when the answer has a `Retry-After` header, wait that long. | | `uploads_disabled` | 503 | ZoomHub isn’t accepting new content right now. Nothing was created, and a pending upload stays pending: retry later. Other 503 answers carry other codes, such as `service_unavailable`. | ## CORS Browsers can call the API from any origin through [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS). Preflight requests allow the methods `GET`, `HEAD`, `POST`, `PUT`, `PATCH` and `DELETE`, and the request headers `Authorization` and `Content-Type`. Responses don’t allow credentials, so a browser never sends ZoomHub’s cookies along: authenticate with the `Authorization` header. A page can read only the response headers CORS always exposes, such as `Cache-Control` and `Content-Type`; `Retry-After` and `Location` aren’t among them. ## IDs & objects IDs are case-sensitive strings of letters and digits, with no prefix for their type: the field or header an ID comes in says what it names. Treat them as opaque: older IDs are shorter than newer ones, and an ID says nothing about what it names. Every response from ZoomHub’s endpoints and pages carries a `Request-Id` header, such as `Request-Id: 4fKx9mQ2TzR7wB1c`, which names the request in the logs and which a refusal repeats in its body as `request_id` (see [Errors](https://zoomhub.net/docs/api.md#errors)): quote it when you report a problem. Every object has an `object` attribute that names its type: `asset`, `upload`, `collection`, `collection_item`, `added_items` or `viewer`. A list is an object too: `{"object": "list", "data": […], "has_more": false}`, whose `data` holds the objects. Nothing is paged yet, so `has_more` is always `false`. Keys are snake\_case, and so are query parameters. Request bodies refuse keys they don’t know with `400`, so a misspelled key fails instead of being ignored. Send them as JSON, with `Content-Type: application/json`. Updates are partial, as in [JSON Merge Patch](https://datatracker.ietf.org/doc/html/rfc7396): a key you leave out keeps its value, `null` clears a field that can be cleared, and a value replaces what is stored. Each asset, and so each collection of assets, has a visibility. `public` assets may be listed on their owner’s profile. `unlisted` assets are open to anyone with their link but listed nowhere, and ask search engines not to index them. `protected` assets need a password, or their owner’s key, to view. A response about a Protected asset carries `Cache-Control: no-store, private`: don’t cache it. ## Versioning The version is part of the path: `/v2`. A version can gain endpoints, attributes and enum values, so ignore attributes you don’t know and handle values you don’t expect. `/v1`, which kept the shape of the [Zoom.it API](https://web.archive.org/web/20140814051321/http://zoom.it/pages/api/), still answers the clients built on it, but it isn’t documented here: build on `/v2`. ## Resources - [Assets](https://zoomhub.net/docs/api/assets.md): Single zoomable images: create from a URL, read, update, delete. - Create an asset — `POST /v2/assets` - Retrieve an asset — `GET /v2/assets/{id}` - Update an asset — `PATCH /v2/assets/{id}` - Delete an asset — `DELETE /v2/assets/{id}` - [Uploads](https://zoomhub.net/docs/api/uploads.md): Files sent straight to storage, then made assets, with a curl walkthrough. - Create an upload — `POST /v2/uploads` - Retrieve an upload — `GET /v2/uploads/{id}` - Complete an upload — `POST /v2/uploads/{id}/complete` - Cancel an upload — `POST /v2/uploads/{id}/cancel` - [Collections](https://zoomhub.net/docs/api/collections.md): Ordered sets of images that zoom together, such as a PDF’s pages. - Create a collection — `POST /v2/collections` - Retrieve a collection — `GET /v2/collections/{id}` - Update a collection — `PATCH /v2/collections/{id}` - Delete a collection — `DELETE /v2/collections/{id}` - [Collection items](https://zoomhub.net/docs/api/collection-items.md): A collection’s members: list, add, remove, reorder. - List a collection’s items — `GET /v2/collections/{id}/items` - Add an item — `POST /v2/collections/{id}/items` - Add items — `POST /v2/collections/{id}/items/batch` - Remove items — `POST /v2/collections/{id}/items/removal` - Order items — `PUT /v2/collections/{id}/items/order` - Remove an item — `DELETE /v2/collections/{id}/items/{assetId}` - Move an item — `PATCH /v2/collections/{id}/items/{assetId}` - [Viewers](https://zoomhub.net/docs/api/viewers.md): Saved embeds: a gallery for a collection, or a viewer for one image. - Create a viewer — `POST /v2/viewers` - Retrieve a viewer — `GET /v2/viewers/{id}` - List viewers — `GET /v2/viewers` - Update a viewer — `PATCH /v2/viewers/{id}` - Delete a viewer — `DELETE /v2/viewers/{id}` - [Guides](https://zoomhub.net/docs/api/guides.md): Complete programs in curl, JavaScript and Python, step by step. - Upload an image and embed it - Turn image URLs into a gallery - Configure and embed a viewer