Skip to content
POST /registry/register

Opens 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.

FieldTypeRequiredDescription
clientIdstringYesStable id of the worker process, kept across re-registrations. Drains target it.
secretstringYesRandom secret shared with Pinboard. Signs Pinboard-Attestation to this worker and authenticates instance calls from it.
namespacesstring[]YesNon-empty. Each / plus one segment, for example /threads.
advertiseUrlstringYeshttp: or https: URL Pinboard forwards to, no userinfo. Must reach exactly this process. Trailing / stripped.
headroomnumberYesFree capacity, any finite number. Higher wins placement ties.
drainingbooleanYestrue registers without taking new placements.
challengestringYesRandom token the worker serves at /_pinned/challenge.
challengeValuestringYesValue the worker answers for challenge.
instancesobjectNo{namespace: id[]}: ids still held in memory, for reclaim. Keys must be in namespaces.
prevSessionIdstringNoThe session this process held before. Rows still owned by it are reclaimed.
hostnamestringHostname-aware: yesHostname this worker serves. Lowercased. No *, whitespace, / or :.
envstringNoHostname-aware. Default env for every namespace. Defaults to hostname. Same character rule.
envsobjectNoHostname-aware. {namespace: env} overrides. Keys must be in namespaces.
takeover"wind_down" | "deny"NoHostname-aware. What happens when this env's id is held on another hostname. Default wind_down.
epochintegerNoHostname-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 prevSessionId re-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

FieldTypeDescription
sessionIdstring32 hex characters. Send on every later worker call.
expiresInMsintegerLease length (PINBOARD_LEASE_TTL_MS, default 15000). The session dies if no heartbeat lands within it.
drainingbooleanThe worker's own draining, or an operator drain on clientId.
drainDeadlinenumberPresent 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.

StatuserrorWhen
401unauthorizedNo accepted credential
403forbiddenNo pinboard:worker scope, or malformed pinboard claim
503not the serving fleetNode not serving. Retry-After: 1.
413body too largeOver 8 MiB
400invalid jsonUnparseable body
403namespace not allowed: …A namespace outside the claim
403advertiseUrl is not a valid URL / advertiseUrl scheme not allowed / advertiseUrl must not carry credentialsBad advertiseUrl
403backend host not allowed: …Host outside allowedBackendHosts
403advertiseUrl port not allowed: …Port outside allowedPorts
403hostname not allowed: … / env not allowed: …Hostname-aware, outside the claim
400expected { clientId, secret, namespaces, advertiseUrl, headroom, draining, challenge, challengeValue, instances?, prevSessionId? }Schema failure. Hostname-aware mode appends hostname, env?, envs?, takeover?, epoch?.
400namespace must be '/' plus a single path segment: "…"Malformed namespace
400hostname is required in hostname-aware mode
400invalid hostname: "…" / invalid env: "…"Bad characters or *
400envs key is not a registered namespace: … / instances key is not a registered namespace: …
400hostname, env, envs, takeover, and nonzero epoch require hostname-aware modeSingle-hostname mode
400registration probe failedChallenge probe failed. The cause is only in Pinboard's logs.
409client belongs to another accountclientId registered by another principal
409hostname … is bound to env … at epoch …, not … — register with a higher epoch to displaceHostname-aware binding conflict
409env … agreed takeover … at epoch …, not … — register with a higher epoch to displaceHostname-aware takeover conflict

Hostname-aware bindings

Registration writes two bindings, first writer wins:

  • (hostname, namespace) → env
  • env → 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.