Developer docs
Hosting API
Provision and manage hosted static sites for users of an approved external project, then hand each user into an isolated, co-branded management portal. Checkout links and payment data are not exposed in v1.
Authentication and scopes
Send a hosting key in Authorization: Bearer pt_host_…. Keys are application-bound and may have sites:read, sites:write, and portal_sessions:create. Suspended applications reject API calls and portal access immediately. Never put keys in browser or mobile code.
1. Provision a portal user
curl -X PUT 'https://api.purpleturret.com/v1/portal-users/user_42' \
-H 'Authorization: Bearer pt_host_...' \
-H 'Content-Type: application/json' \
-d '{"email":"ada@example.com","name":"Ada Lovelace"}'External IDs and identities are namespaced to your application. Purpleturret creates a dedicated portal-only Clerk identity; an existing Purpleturret customer account is never reused.
2. Create and upload a batch
curl -X POST 'https://api.purpleturret.com/v1/upload-batches' \
-H 'Authorization: Bearer pt_host_...' \
-H 'Content-Type: application/json' \
-d '{"manifest":[
{"path":"index.html","size":421,"content_type":"text/html"},
{"path":"assets/app.js","size":18420,"sha256":"<64 lowercase hex>"}
]}'Upload each file body directly to its returned upload_url. Upload URLs last one hour. Purpleturret does not proxy file bytes through Next.js. Limits are 1,000 files, 100 MB per site, and 100 MB per file; paths are normalized and root index.html is required.
curl -X PUT '<upload_url>' \
-H 'Content-Type: text/html' \
--upload-file './dist/index.html'curl -X POST 'https://api.purpleturret.com/v1/upload-batches/<batch_id>/complete' \
-H 'Authorization: Bearer pt_host_...'Upload URLs bind the declared byte length. Completion verifies every object’s actual size and SHA-256 before it can be deployed.
3. Create, read, update, and deploy
curl -X POST 'https://api.purpleturret.com/v1/sites' \
-H 'Authorization: Bearer pt_host_...' \
-H 'Idempotency-Key: site-user-42-v1' \
-H 'Content-Type: application/json' \
-d '{
"external_id":"squibb_site_9",
"owner_external_id":"user_42",
"upload_batch_id":"<batch id>",
"title":"Ada’s site",
"slug":"ada-notes",
"published":true
}'
curl -H 'Authorization: Bearer pt_host_...' 'https://api.purpleturret.com/v1/sites?limit=25'
curl -X PATCH 'https://api.purpleturret.com/v1/sites/<site_id>' \
-H 'Authorization: Bearer pt_host_...' \
-H 'Content-Type: application/json' \
-d '{"title":"New title","published":true,"spa_fallback":true}'
curl -X POST 'https://api.purpleturret.com/v1/sites/<site_id>/deployments' \
-H 'Authorization: Bearer pt_host_...' \
-H 'Idempotency-Key: deploy-site-9-v2' \
-H 'Content-Type: application/json' \
-d '{"upload_batch_id":"<completed batch id>"}'Site creation and deployments require Idempotency-Key. Reusing a key with the same request returns the original result; reusing it with a different request returns 409 idempotency_conflict.
4. Add a custom domain
curl -X PUT 'https://api.purpleturret.com/v1/sites/<site_id>/custom-domain' \
-H 'Authorization: Bearer pt_host_...' \
-H 'Content-Type: application/json' \
-d '{"domain":"www.example.com"}'
curl -X POST 'https://api.purpleturret.com/v1/sites/<site_id>/custom-domain/verify' \
-H 'Authorization: Bearer pt_host_...'Configure every returned dns_records entry at the domain’s DNS provider, then call the verify endpoint. The domain becomes the site’s public_url only when its status is active. Each site supports one domain; remove the current one before attaching a replacement.
Subdomains use CNAME routing by default. If the exact hostname must also carry records such as Mission Inbox MX or TXT, request A routing so those records can coexist:
curl -X PUT 'https://api.purpleturret.com/v1/sites/<site_id>/custom-domain' \
-H 'Authorization: Bearer pt_host_...' \
-H 'Content-Type: application/json' \
-d '{"domain":"acme.pool-domain.com","routing_type":"A"}'Use the provider-recommended IPv4 returned in the routing entry of dns_records. The response also reports the selected routing_type. Repeating the request for the same hostname changes its routing mode in place.
curl -X DELETE 'https://api.purpleturret.com/v1/sites/<site_id>/custom-domain' \
-H 'Authorization: Bearer pt_host_...'Removal is immediate in Purpleturret; provider cleanup continues safely in the background.
5. Hand off to the portal
curl -X POST 'https://api.purpleturret.com/v1/portal-sessions' \
-H 'Authorization: Bearer pt_host_...' \
-H 'Content-Type: application/json' \
-d '{"external_user_id":"user_42","site_id":"<optional site id>"}'Redirect the browser to the returned URL. It expires after five minutes and is single-use. The ticket is stripped from browser history immediately. Portal users can list and manage only their API-created sites; creation and every seller/payment/developer route are server-rejected.
Permanent deletion and errors
curl -X DELETE 'https://api.purpleturret.com/v1/sites/<site_id>' \
-H 'Authorization: Bearer pt_host_...'Deletion is permanent. The site record and public availability disappear transactionally before 204 returns; files, storage blobs, visits, submissions, and tracking rows are then purged in bounded idempotent jobs.
{
"error": {
"code": "validation_error",
"message": "The manifest must include index.html at its root",
"request_id": "req_..."
}
}Expected statuses are 401, 403, 404, 409, 413, 422, 429, and 503. On 429, honor Retry-After. Hosting endpoints do not enable wildcard CORS and are intended only for server-to-server use.