POST /registry/registerOpens a new session for a worker process. Pinboard first calls the worker back at advertiseUrl to prove the URL reaches this process, then returns a sessionId. The session is the worker's identity for leases, placements and attestation. Keep it alive with heartbeat at an interval well inside expiresInMs (pinned uses 3 s against the 15 s default).
Every call mints a new sessionId, even for a known clientId.
Authentication
pinboard:worker scope. The body must fall inside the principal's pinboard claim. The principal's sub becomes the session's account.
Request body
application/json, at most 8 MiB.
| Field | Type | Required | Description |
|---|---|---|---|
clientId | string | Yes | Stable id of the worker process, kept across re-registrations. Drains target it. |
secret | string | Yes | Random secret shared with Pinboard. Signs Pinboard-Attestation to this worker and authenticates instance calls from it. |
namespaces | string[] | Yes | Non-empty. Each / plus one segment, for example /threads. |
advertiseUrl | string | Yes | http: or https: URL Pinboard forwards to, no userinfo. Must reach exactly this process. Trailing / stripped. |
headroom | number | Yes | Free capacity, any finite number. Higher wins placement ties. |
draining | boolean | Yes | true registers without taking new placements. |
challenge | string | Yes | Random token the worker serves at /_pinned/challenge. |
challengeValue | string | Yes | Value the worker answers for challenge. |
instances | object | No | {namespace: id[]}: ids still held in memory, for reclaim. Keys must be in namespaces. |
prevSessionId | string | No | The session this process held before. Rows still owned by it are reclaimed. |
hostname | string | Hostname-aware: yes | Hostname this worker serves. Lowercased. No *, whitespace, / or :. |
env | string | No | Hostname-aware. Default env for every namespace. Defaults to hostname. Same character rule. |
envs | object | No | Hostname-aware. {namespace: env} overrides. Keys must be in namespaces. |
takeover | "wind_down" | "deny" | No | Hostname-aware. What happens when this env's id is held on another hostname. Default wind_down. |
epoch | integer | No | Hostname-aware. Binding epoch, ≥ 0, default 0. A higher epoch displaces conflicting bindings. |
In single-hostname mode hostname, env, envs, takeover and a nonzero epoch are rejected.
Challenge probe
Before answering, Pinboard calls:
GET {advertiseUrl}/_pinned/challenge?challenge={challenge}
Pinboard-Proxy-Secret: {secret}It expects a 2xx with JSON {"value": "{challengeValue}"} within PINBOARD_VERIFY_TIMEOUT_MS (2500 ms). Redirects are not followed. Any other outcome fails the registration. See Worker callbacks.
Reclaim
With instances, a restarted process keeps ids it still holds in memory:
- An id whose row is still owned by
prevSessionIdre-binds to the new session and keeps its epoch. A pending wind-down is re-sent to the new session. - Every other listed id is returned in
duplicates. The worker must evict those locally; the next request re-places them.
Example
curl -X POST https://pinboard.example.com/registry/register \
-H "Authorization: Bearer $PINBOARD_WORKER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"clientId": "c1e0b7a94f6d4a1e9b2c3d4e5f60718a",
"secret": "9d1c...e4",
"namespaces": ["/threads"],
"advertiseUrl": "http://10.0.3.17:8000",
"headroom": 100,
"draining": false,
"challenge": "5b0f7e2a1c9d4e3f",
"challengeValue": "a7c3e1f09b2d4c6e"
}'Response
200
| Field | Type | Description |
|---|---|---|
sessionId | string | 32 hex characters. Send on every later worker call. |
expiresInMs | integer | Lease length (PINBOARD_LEASE_TTL_MS, default 15000). The session dies if no heartbeat lands within it. |
draining | boolean | The worker's own draining, or an operator drain on clientId. |
drainDeadline | number | Present only for a timed operator drain: seconds until forced release, 0 once passed. |
duplicates | {ns, id}[] | Present only when non-empty: instances ids to evict. |
{
"sessionId": "3f9c0d6e2b1a4c7d8e9f0a1b2c3d4e5f",
"expiresInMs": 15000,
"draining": false
}Errors
Checked in this order.
| Status | error | When |
|---|---|---|
401 | unauthorized | No accepted credential |
403 | forbidden | No pinboard:worker scope, or malformed pinboard claim |
503 | not the serving fleet | Node not serving. Retry-After: 1. |
413 | body too large | Over 8 MiB |
400 | invalid json | Unparseable body |
403 | namespace not allowed: … | A namespace outside the claim |
403 | advertiseUrl is not a valid URL / advertiseUrl scheme not allowed / advertiseUrl must not carry credentials | Bad advertiseUrl |
403 | backend host not allowed: … | Host outside allowedBackendHosts |
403 | advertiseUrl port not allowed: … | Port outside allowedPorts |
403 | hostname not allowed: … / env not allowed: … | Hostname-aware, outside the claim |
400 | expected { clientId, secret, namespaces, advertiseUrl, headroom, draining, challenge, challengeValue, instances?, prevSessionId? } | Schema failure. Hostname-aware mode appends hostname, env?, envs?, takeover?, epoch?. |
400 | namespace must be '/' plus a single path segment: "…" | Malformed namespace |
400 | hostname is required in hostname-aware mode | |
400 | invalid hostname: "…" / invalid env: "…" | Bad characters or * |
400 | envs key is not a registered namespace: … / instances key is not a registered namespace: … | |
400 | hostname, env, envs, takeover, and nonzero epoch require hostname-aware mode | Single-hostname mode |
400 | registration probe failed | Challenge probe failed. The cause is only in Pinboard's logs. |
409 | client belongs to another account | clientId registered by another principal |
409 | hostname … is bound to env … at epoch …, not … — register with a higher epoch to displace | Hostname-aware binding conflict |
409 | env … agreed takeover … at epoch …, not … — register with a higher epoch to displace | Hostname-aware takeover conflict |
Hostname-aware bindings
Registration writes two bindings, first writer wins:
(hostname, namespace) → envenv → takeover
A registration that disagrees with an existing binding at an equal or higher epoch answers 409. A higher epoch rewrites it; the displaced (hostname, namespace) then refuses new placements for one lease, and workers on the old binding are told superseded on their next heartbeat.