Subscribers API
POST /api/v1/subscribers/upsert
Creates or updates a subscriber record, keyed by external_id. Authenticated
with the publishable SDK key (X-SDK-Key header). Idempotent: re-sending the
same body converges on the same record, which is what makes retrying
pipelines (Zapier, reverse-ETL) safe.
Request Body
{
"external_id": "user-123",
"email": "user@example.com",
"attributes": {
"plan": "pro",
"country": "US"
}
}
external_id is the only required field. An optional consent assertion
records an express email opt-in alongside the upsert. An optional
claim_token is the one-time hober_claim token a subscription form's
redirect carries: it takes over the form signup for that address, as the
server identify does by email. The browser SDK
sends it for you. A token issued for another address than the request's
email is ignored, and the identity rules below still apply.
An update merges into the stored subscriber; it never replaces it:
| You send | Result |
|---|---|
a field you omit (email, attributes, or an attribute key) | the stored value is kept |
| an attribute key with a value | that key is set; other stored keys are kept |
an attribute key set to null | that key is removed |
"email": "" | the stored email is cleared |
So an app can call identify with only external_id on every launch without
erasing attributes set from your backend or an import.
Response
200 confirming the record. The SDK key is publishable, so this response
returns only what the caller already knows: email and attributes echo the
values sent in this request (empty when omitted), never the stored profile.
Read stored profiles through the dashboard API. id is the Hober subscriber id
the SDKs use for device registration.
{
"id": "8a2f6c1e-…",
"external_id": "user-123",
"email": "user@example.com",
"attributes": "{\"plan\":\"pro\",\"country\":\"US\"}",
"created_at": "2026-07-26T12:00:00Z",
"updated_at": "2026-07-26T12:00:00Z"
}
attributes travels as a JSONB-encoded string (schemaless by design).
401 — missing or invalid SDK key. 403 IDENTITY_VERIFICATION_REQUIRED — the
change to an existing subscriber needs an X-Hober-Identity token (see
identity verification): replacing
its email always does, and every write does under enforce. 422 — validation
failure naming the field.
POST /v1/subscribers/identify
Identifies one of your signed-in users from your backend. Authenticated with a
server key that carries subscribers:write
(Authorization: Bearer hober_srv_…). The body is the upsert body above
(external_id required; email, attributes and consent optional), and it
merges the same way. Your backend has verified its own user, so no
X-Hober-Identity token is needed.
Claiming a form signup. Someone who signs up through a subscription form
gets a provisional subscriber with a Hober-assigned id (hf_…). When you
identify a user with the same email, that user takes it over:
Your external_id | Result | claim |
|---|---|---|
| is new | the form subscriber is renamed to your id. Consent, answers, lists, devices and event history stay on it | promoted |
| already exists | the form subscriber is merged into yours and deleted. Lists are combined, form answers fill only the attributes yours lacks, and the most recent consent record wins per channel | merged |
| any, and no form subscriber has that email | a plain upsert | none |
Only form subscribers are ever claimed: two of your own ids are never merged
by email. Ids starting with hf_ are reserved and refused with 422.
Response
200. The response names the subscriber and what the claim did. A write key
is not a read key, so it never returns the stored email or attributes.
{
"id": "8a2f6c1e-…",
"external_id": "user-123",
"claim": "promoted",
"created_at": "2026-10-12T09:30:00Z",
"updated_at": "2026-10-13T12:00:00Z"
}
401 — missing or unknown server key. 403 FORBIDDEN — the key does not
carry subscribers:write. 422 — validation failure. 429 SUBSCRIBER_LIMIT_REACHED — creating a new subscriber is over the plan limit.
DELETE /api/v1/subscribers/{id}
Authenticated with a dashboard bearer token (see Authentication). Permanently deletes a single subscriber and all associated devices and list memberships. Delivery log rows are anonymised (foreign keys set to NULL) rather than deleted.
For GDPR compliance details, cascade behaviour, and the operator runbook, see the GDPR Data Retention Policy guide.
Path parameters
| Parameter | Type | Description |
|---|---|---|
id | string (UUID) | Hober subscriber UUID |
Response
204 No Content on success. No response body.
Error responses
| Status | Meaning |
|---|---|
401 | Missing or invalid bearer token |
404 | Subscriber not found |
POST /api/v1/subscribers/bulk-delete
Permanently deletes up to 1,000 subscribers in one GDPR-compliant transaction. Authenticated with a dashboard bearer token. IDs not found in your workspace are returned in not_found and are not treated as errors.
Request body
{
"subscriber_ids": ["uuid-1", "uuid-2", "uuid-3"]
}
| Field | Type | Description |
|---|---|---|
subscriber_ids | string[] | Hober subscriber UUIDs. 1–1,000 per request. |
Response
200 OK:
{
"deleted": 2,
"not_found": ["uuid-3"]
}
Error responses
| Status | Meaning |
|---|---|
401 | Missing or invalid bearer token |
422 | Empty list, or more than 1,000 IDs supplied |
For cascade behaviour see the GDPR Data Retention Policy guide.