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.

OpenAPI 3.1 document

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.