Public Endpoints Reference
All public endpoints are served at https://<tenant-custom-domain>/public/v1/. No authentication is required. Tenant identity is derived exclusively from the Host header.
GET /public/v1/privacy/dsar
Renders the DSAR submission form as an HTML page.
Authentication: None
Content-Type returned: text/html; charset=utf-8
Request
curl -X GET "https://privacy.example.com/public/v1/privacy/dsar"
Response
Returns an HTML page containing the DSAR form pre-populated with the tenant's display name and default jurisdiction (if configured). The response includes no cookies and no external script tags.
Response headers:
Content-Type: text/html; charset=utf-8
Cache-Control: no-store
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Status codes:
| Code | Meaning |
|---|---|
| 200 | Form rendered successfully |
| 404 | Hostname not found / no tenant matches this Host |
| 500 | Internal error |
POST /public/v1/privacy/dsar/submit
Submits a DSAR. Validates the form, persists the request, and sends a confirmation email.
Authentication: None
Content-Type accepted: application/x-www-form-urlencoded
Content-Type returned: text/html; charset=utf-8 (renders confirmation or error page)
Request
curl -X POST "https://privacy.example.com/public/v1/privacy/dsar/submit" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "full_name=Jane Doe" \
--data-urlencode "email=jane.doe@example.com" \
--data-urlencode "request_type=access" \
--data-urlencode "jurisdiction=eu" \
--data-urlencode "details=I would like a copy of all data you hold about me."
Form fields
| Field | Type | Required | Validation |
|---|---|---|---|
full_name | string | Yes | 1–200 characters |
email | string | Yes | Valid RFC 5321 email address |
request_type | enum | Yes | One of: access, erasure, amend, portability, restrict |
jurisdiction | enum | Yes | One of: us, eu, uk, ca, au, in, br |
details | string | No | Up to 2000 characters |
Response
On success, returns an HTML confirmation page (HTTP 200) showing a short ticket reference (first 8 characters of the UUID). The full ticket UUID is sent to the consumer's email.
On validation failure, returns HTTP 400 with the form re-rendered and an error message.
Response headers (success and error):
Content-Type: text/html; charset=utf-8
Cache-Control: no-store
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Status codes:
| Code | Meaning |
|---|---|
| 200 | Request submitted and confirmation rendered |
| 400 | Validation failure — form re-rendered with error |
| 404 | Hostname not found |
| 500 | Internal error |
Security notes
- The consumer's email address is stored as a one-way SHA-256 hash in Helix's database. The plaintext email is used only to send the confirmation email in a fire-and-forget goroutine; failure to send does not fail the submission.
- No rate limit headers are returned, but server-side rate limiting is applied per source IP and per tenant to prevent form spam.
- The form embed sets no cookies.
GET /public/v1/privacy/notice
Returns the current published privacy notice for the tenant as HTML.
Authentication: None
Content-Type returned: text/html; charset=utf-8
Request
curl -X GET "https://privacy.example.com/public/v1/privacy/notice" \
-H "If-None-Match: \"<etag-from-previous-response>\""
Response
Returns the rendered HTML of the tenant's current published privacy notice.
Response headers:
Content-Type: text/html; charset=utf-8
Cache-Control: public, max-age=300
ETag: "<sha256-hex-of-content>"
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
ETag is a double-quoted hex string of the SHA-256 hash of the notice content. Use If-None-Match on subsequent requests to benefit from 304 Not Modified responses and avoid transferring the full body.
Status codes:
| Code | Meaning |
|---|---|
| 200 | Notice returned |
| 304 | Not Modified (ETag matched If-None-Match) |
| 404 | No published notice for this tenant |
| 500 | Internal error |
GET /public/v1/privacy/notice/v:version
Returns a specific historical version of the privacy notice. Only published or archived versions are accessible.
Authentication: None
Content-Type returned: text/html; charset=utf-8
Request
# Fetch version 3 of the privacy notice
curl -X GET "https://privacy.example.com/public/v1/privacy/notice/v3"
Path parameter
| Parameter | Type | Description |
|---|---|---|
version | positive integer | The notice version number |
Response
Returns the HTML of the specified version. Archived versions are immutable.
Response headers:
Content-Type: text/html; charset=utf-8
Cache-Control: public, max-age=31536000, immutable
ETag: "<sha256-hex-of-content>"
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Status codes:
| Code | Meaning |
|---|---|
| 200 | Version returned |
| 304 | Not Modified (ETag matched) |
| 400 | Invalid version number (non-integer or ≤ 0) |
| 404 | Version not found, or version exists but is not published/archived (draft/in-review versions are not exposed) |
| 500 | Internal error |
Common error behavior
All public endpoints return plain-text or HTML error messages (never JSON) because they are consumer-facing surfaces. Machine-readable error codes are not needed on the public tier.
If the request Host header does not match any configured tenant domain, Helix returns 404 with a plain-text body. There is no fallback tenant.
Idempotency
GET endpoints are idempotent. POST /public/v1/privacy/dsar/submit is not idempotent — re-submitting the same form creates a new DSAR ticket. There is no client-supplied idempotency key in Phase 1.