POST /v1/hub/authorize
Complete the device flow and register hub keys with Hub.
This endpoint has two calling modes depending on whether keys are provided. The browser calls it after the user approves the device code. The CLI calls it with keys to complete hub connection creation.
Browser-only call (no keys)
When the browser confirms approval without providing Treeship/hub keys, Hub marks the challenge as approved and returns a status. No hub connection or Treeship record is created yet.
Request
POST /v1/hub/authorize
Content-Type: application/json{
"device_code": "dvc_a1b2c3d4"
}| Field | Type | Required | Description |
|---|---|---|---|
device_code | string | Yes | The device code from the challenge |
Response
{
"state": "approved",
"status": "approved"
}| Field | Type | Description |
|---|---|---|
state | string | Device-flow state. "approved" once the browser approves. |
status | string | Legacy alias for state, kept for older clients. |
No hub_id is returned. The browser's job is just to approve -- the CLI completes registration in the next step.
CLI call (with keys)
When the CLI calls this endpoint with Treeship and hub public keys, Hub verifies the challenge is already browser-approved, validates the nonce, atomically claims the challenge for a new dock_id, creates the Treeship record, and returns it. The claim is single-use: a concurrent or replayed finalize is rejected as already_attached.
The challenge must already be approved by the browser before the CLI calls this endpoint. The CLI must also provide the nonce that was returned by GET /v1/hub/challenge.
Request
POST /v1/hub/authorize
Content-Type: application/json{
"device_code": "dvc_a1b2c3d4",
"ship_public_key": "3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29",
"hub_public_key": "fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025",
"nonce": "e4f8a2c1b7d9306f5e1a9c4b8d7f2e0a"
}| Field | Type | Required | Description |
|---|---|---|---|
device_code | string | Yes | The device code from the challenge |
ship_public_key | string | Yes | Hex-encoded Ed25519 public key used for signing artifacts |
hub_public_key | string | Yes | Hex-encoded Ed25519 public key used for DPoP authentication |
nonce | string | Yes | The nonce returned by GET /v1/hub/challenge -- must match exactly |
Response
{
"state": "attached",
"dock_id": "hub_9f8e7d6c",
"next_steps": [
"treeship status",
"treeship attest receipt --system system://<your-system> --kind <kind> --payload-file <file>",
"treeship hub push <artifact-id>",
"treeship verify <artifact-id>"
],
"example": "treeship attest receipt --system system://zmem --kind memory.proof --payload-file proof.json"
}| Field | Type | Description |
|---|---|---|
state | string | "attached" on success. |
dock_id | string | Unique identifier for this hub connection (format: dck_/hub_ + 32 hex chars) |
next_steps | string[] | Provider-neutral command templates to surface on the success page. |
example | string | One illustrative invocation. The placeholders are real; system://zmem and memory.proof are an example customer's memory-proof workflow, not built-in behavior. --system and --kind accept any value. |
The challenge is claimed, not deleted, on success. The row is retained (with its dock_id) until well past expiry so a still-polling activation page reports attached rather than a misleading not found.
Errors
Every state-bearing response carries a stable state field so the activation page can distinguish outcomes precisely.
| Status | Body | State | Cause |
|---|---|---|---|
400 | {"error": "missing device_code"} | -- | Request body is missing the device_code field |
400 | {"error": "missing nonce -- required for dock finalization"} | -- | CLI sent keys but omitted the nonce field |
400 | {"error": "invalid ship_public_key hex"} | -- | ship_public_key is not valid hex |
400 | {"error": "invalid dock_public_key hex"} | -- | dock_public_key is not valid hex |
403 | {"state": "pending", "error": "challenge not yet approved -- complete browser activation first"} | pending | Keys were sent but the browser has not approved the challenge yet |
403 | {"error": "nonce mismatch"} | -- | The nonce does not match the one stored with the challenge |
404 | {"state": "invalid", "error": "device_code not found"} | invalid | No challenge exists for this device code |
409 | {"state": "already_attached", "error": "device code already used"} | already_attached | This challenge was already finalized (prevents double-registration) |
410 | {"state": "expired", "error": "device_code expired"} | expired | The challenge has passed its 5-minute expiry window |
Examples
Browser approval (no keys)
curl -X POST https://api.treeship.dev/v1/hub/authorize \
-H "Content-Type: application/json" \
-d '{
"device_code": "dvc_a1b2c3d4"
}'CLI registration (with keys)
curl -X POST https://api.treeship.dev/v1/hub/authorize \
-H "Content-Type: application/json" \
-d '{
"device_code": "dvc_a1b2c3d4",
"ship_public_key": "3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29",
"hub_public_key": "fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025",
"nonce": "e4f8a2c1b7d9306f5e1a9c4b8d7f2e0a"
}'Notes
- The Treeship public key and hub public key must be different Ed25519 keys
- The Treeship key is stored for identification only. Hub never receives any private key
- The hub key is used to verify DPoP JWT signatures on subsequent API requests
- Each device code can only be authorized once -- the challenge is atomically claimed (by
dock_id) on success, and the row is kept so the activation page can still poll and readattached - If the
ship_public_keyalready exists on Hub, the existing Treeship record is reused and a new hub connection is attached to it - The
noncefield prevents replay attacks and binds the CLI request to the specific challenge session