---
summary: End-to-end agent self-serve path from cold PoW workspace bootstrap through device-flow human delegation to a working delegated token, with dual curl and sigid-cli tracks.
tags:
  - developers
  - agents
  - quickstart
  - bootstrap
  - delegation
  - cli
categories:
  - AI Agents
---

# Agent Self-Serve Quickstart

<!-- agent:page
You are a coding agent that can fully self-serve SigID without a human pre-creating the workspace.
Execute this page end to end against the configured IDP (default production https://auth.sigid.org; local stack http://auth.sigid.localhost:3000).
Preferred path: workspace bootstrap (sigid-cli setup or POST /api/v1/agents/workspace/bootstrap/pow*), which creates agent + sandbox org + environment + OAuth application and caches a direct tenant agent token.
Before delivery, test the real hosted authorize path with fixture human accounts, record application-owned sample-data links in a secret-free test manifest, and issue sigid-cli handoff with the agent, organization, and application IDs. An operator invitation is access-only and is not a substitute for handoff.
Then initiate agent device delegation (POST /api/v1/agents/delegations/device or sigid-cli delegation create), hand verification_uri_complete to a human for AAL2+fresh approval, and poll until a delegated token is issued.
Never log or commit access tokens, refresh tokens, client secrets, device_code, private keys, or user_codes. Prefer environment variables and redacted CLI output.
Definition of done: agent identity exists, customer app client_id is known, hosted login and application state are reproducible by a fixture human, the intended developer accepted the operator handoff, and any optional device request produced a delegated access token with act for AAL2-gated work.
-->

Cold agents can provision a sandbox workspace, create an application, and obtain
human-delegated authority without a human first creating the account in a
dashboard. This page is the single mechanical path.

**Hosted end-user authorize:** sandbox bootstrap returns
`fixture_end_user` – one ordinary human at `@test.sigid.dev` + **once-only**
password for the new environment. Use those credentials on the real hosted login
for your app’s authorize leg (mail to that domain is never delivered). Mint more
(up to 5 active): `POST /api/v1/sandbox/fixture-users`. Rotate:
`POST /api/v1/sandbox/fixture-users/{user_id}/rotate-password` (`users:manage`).
Agent identities are not stand-ins for human OIDC. Beyond the fixture cap use
signup, invite, or AAL2 admin create. Fixtures are retired on sandbox →
production activation.

For the required provision → fixture-test → ownership-transfer → human preview
and activation → branded retest sequence, including how to preserve
application-owned sample data for the developer, follow
[Provision, Test, And Hand Off](provision-test-handoff.md).

Human-managed agent CRUD (portal or `POST /api/v1/agents` with an existing
bearer) is documented separately in [Agent Registration](registration.md).

Time: 10–20 minutes after the IDP is reachable. Step 4 always requires a human
browser session (AAL2 + fresh authentication).

## What You Will Build

1. An agent identity on the control-plane tenant (PoW-gated).
2. A sandbox organization, environment, and customer OAuth application.
3. A device-flow delegation request with `verification_uri_complete`.
4. Human approval on the hosted consent page.
5. A delegated access token whose `act` chain carries the human’s AAL2 evidence.

## Before You Start

| Value | Production default | Local Docker Compose |
|---|---|---|
| IDP base / issuer host | `https://auth.sigid.org` | `http://auth.sigid.localhost:3000` |
| Control-plane tenant | `sigid` | `sigid` |
| App redirect URI | your app callback | `http://localhost:3000/` |
| Delegation audience | tenant issuer or API audience | often `sigid` or the tenant issuer |
| Delegation scopes | scopes the **human** holds | e.g. `applications:manage` |

Set:

```bash
export SIGID_IDP="${SIGID_IDP:-https://auth.sigid.org}"
# Local stack example:
# export SIGID_IDP="http://auth.sigid.localhost:3000"
```

Security rules for this entire page:

- Never log or paste into tickets: access tokens, refresh tokens, `client_secret`,
  `device_code`, private keys, keystore passphrases, or raw `user_code` values
  beyond what the human must type once.
- Device-delegation routes require a **direct** (non-delegated) agent bearer.
  Do not overwrite the agent token cache with the delegated token.
- Prefer `sigid-cli` for key generation, PoW, and key-ownership proofs. Raw HTTP
  is shown for wire transparency; crypto fields are non-trivial by hand.

Optional one-shot check (CLI track) after install:

```bash
# From a docs checkout, or copy the script next to your agent:
bash docs/developers/agent-quickstart.sh --help
```

## Step 1 – Self-Register And Bootstrap Workspace

The preferred path is one PoW-gated composite that creates the agent, sandbox
organization, environment, and application.

### CLI track (recommended)

<!-- agent:action Bootstrap workspace with CLI
Run: sigid-cli setup --idp "$SIGID_IDP" --name <agent-name> --redirect-uri <uri>
Capture client_id, tenant_issuer, organization_id, and env_block. Do not log client_secret or tokens.
Store the operator invite link out-of-band if present. Verify: sigid-cli whoami and sigid-cli verify-setup --idp "$SIGID_IDP".
-->

```bash
sigid-cli setup \
  --idp "$SIGID_IDP" \
  --name my-agent \
  --redirect-uri http://localhost:3000/

# Optional re-issue of co-owner invite:
# sigid-cli invite --organization-id <org_id> --idp "$SIGID_IDP"

sigid-cli verify-setup --idp "$SIGID_IDP"
```

What you get:

| Field | Use |
|---|---|
| `agent_id` | Agent identity transferred by operator handoff |
| `application_id` | Application resource included in operator handoff |
| `client_id` / optional `client_secret` | Customer app OAuth client (`client_secret` printed once) |
| `tenant_issuer` | Tenant OIDC issuer |
| `organization_id` | Control-plane org for invites / activation |
| Cached tenant agent token | Direct bearer for later steps (`tokens.enc`) |
| `env_block` / `start_snippet` | Paste into your app |

### curl track

PoW mining and `key_ownership_proof` require a local signing key. Prefer the CLI
unless you already implement the ownership-proof format from
[Agent Authentication](agent-auth.md) / core agent identity types.

```bash
# 1) Start PoW (control-plane host; body is the same registration shape as
#    POST /api/v1/agents/auth/register/pow – name, anchor_type, public_key,
#    key_algorithm, key_ownership_proof).
curl -sS "$SIGID_IDP/api/v1/agents/workspace/bootstrap/pow" \
  -X POST \
  -H "content-type: application/json" \
  -d @bootstrap-pow-start.json
# → challenge_id, challenge_token, difficulty_bits, min_duration_seconds, …

# 2) Mine the challenge (CLI does this; custom miners must meet difficulty + duration).

# 3) Complete bootstrap
curl -sS "$SIGID_IDP/api/v1/agents/workspace/bootstrap/pow/complete" \
  -X POST \
  -H "content-type: application/json" \
  -d '{
    "challenge_id": "'"$CHALLENGE_ID"'",
    "organization_name": "My Agent Workspace",
    "redirect_uris": ["http://localhost:3000/"],
    "issue_operator_invite": true,
    "framework": "generic"
  }'
# → agent, organization_*, tenant_*, application_id, client_id, client_secret?,
#   fixture_end_user, control_plane_access_token, tenant_access_token,
#   start_snippet, env_block, …
```

Store `tenant_access_token` as `AGENT_ACCESS_TOKEN` for later steps. Do not log it.

The bootstrap invite makes a human co-owner but leaves the agent identity under
its previous ownership. After integration testing, use `sigid-cli handoff`
with the printed agent, organization, and application IDs to transfer the agent
and grant the developer the complete resource package.

### Alternate: register only (no workspace)

If you only need an agent principal on a tenant that already admits agents:

```bash
# CLI
sigid-cli init --idp "$SIGID_IDP" --tenant-id "$SIGID_TENANT_ID" --name my-agent --anchor-type did_key --algo ed25519

# HTTP (paths – not the stale /auth/agent/* aliases)
# POST $SIGID_IDP/api/v1/agents/auth/register
# POST $SIGID_IDP/api/v1/agents/auth/register/pow
# POST $SIGID_IDP/api/v1/agents/auth/register/pow/complete
```

`init` / bare register does **not** create an org or application. Use Step 2
only when you already hold a tenant token with `applications:manage`.

## Step 2 – Create An Application (If Needed)

Bootstrap already created one application. Create another only when you need a
second client.

Requires a **tenant-scoped** direct agent token with `applications:manage`
(bootstrap’s `tenant_access_token` / CLI cache after `setup`).

### CLI track

<!-- agent:action Create application with CLI
Run: sigid-cli app create --idp "$SIGID_IDP" --name <name> --redirect-uri <uri>
Capture client_id; treat client_secret as one-time. Never store the secret in keys.enc or logs.
-->

```bash
sigid-cli app create \
  --idp "$SIGID_IDP" \
  --name "Web App" \
  --redirect-uri http://localhost:3000/
```

### curl track

```bash
curl -sS "$SIGID_IDP/api/v1/applications" \
  -X POST \
  -H "authorization: Bearer $AGENT_ACCESS_TOKEN" \
  -H "content-type: application/json" \
  -H "idempotency-key: $(uuidgen)" \
  -d '{
    "name": "Web App",
    "redirect_uris": ["http://localhost:3000/"]
  }'
# 201 → id, client_id, client_secret (once), audience, …
```

<details>
<summary>Update an application (<code>app update</code>)</summary>

`app update` is a **read-modify-write**: it fetches the current application,
overlays your flags, and PUTs the merged state. This matters because the
server's `PUT` is full-replacement – omitting fields would clear them.

```bash
# Rename and add a redirect URI; scopes / grant types / web origins are preserved.
sigid-cli app update "$APP_ID" \
  --idp "$SIGID_IDP" \
  --name "Web App (renamed)" \
  --redirect-uri https://app.example.com/cb \
  --auth-profile strict
```

Repeatable replace-only flags: `--redirect-uri`, `--scope`, `--grant-type`,
`--web-origin`. Pass `--description ""` to clear the description.

</details>

## Step 3 – Initiate Device Delegation

Request human-delegated authority through an RFC 8628-shaped ceremony.

Use a **direct** agent bearer for the **managed tenant** (not a delegated token).

### CLI track

<!-- agent:action Initiate device delegation
Run: sigid-cli delegation create --idp "$SIGID_IDP" --audience <aud> --scope <scope>
Print absolute_link / verification_uri_complete for the human. Keep device_code only for polling; do not log it broadly.
-->

```bash
sigid-cli delegation create \
  --idp "$SIGID_IDP" \
  --audience sigid \
  --scope applications:manage
# → absolute_link, user_code_display, device_code, expires_in, interval
```

### curl track

```bash
curl -sS "$SIGID_IDP/api/v1/agents/delegations/device" \
  -X POST \
  -H "authorization: Bearer $AGENT_ACCESS_TOKEN" \
  -H "content-type: application/json" \
  -d '{
    "audience": "sigid",
    "scope": "applications:manage"
  }'
```

**Response shape**

```json
{
  "device_code": "<secret>",
  "user_code": "ABCDEFGH",
  "user_code_display": "ABCD-EFGH",
  "verification_uri": "/device/delegation/ABCDEFGH",
  "verification_uri_complete": "/device/delegation/ABCDEFGH?complete=1",
  "expires_in": 900,
  "interval": 5
}
```

| Field | Semantics |
|---|---|
| `device_code` | Agent-only secret for poll; hashed at rest |
| `user_code` / `user_code_display` | Human-facing code |
| `verification_uri_complete` | Relative path; prefix with `$SIGID_IDP` for the absolute link |
| `interval` | Minimum poll spacing (seconds) |

```bash
export DEVICE_CODE='…'   # from response; keep private
export DELEGATION_LINK="${SIGID_IDP}/device/delegation/${USER_CODE}?complete=1"
```

## Step 4 – Human Approves

<!-- agent:action Hand off to human for AAL2 approval
Present verification_uri_complete (absolute) to the human operator. They must sign in/up, satisfy AAL2 and fresh authentication (about 5 minutes), then approve or deny on the hosted page.
The agent cannot complete this step. Do not automate browser AAL2 with stored passwords in docs or scripts.
-->

1. Open `verification_uri_complete` (absolute URL) in a browser, or type the
   user code on the verification page.
2. Sign in or sign up on the hosted identity surface.
3. Complete MFA / step-up until the session is **AAL2** and **fresh**.
4. Review agent identity, audience, and scopes; **Approve** or **Deny**.

Hosted routes (human browser, not agent JSON):

| Method + path | Role |
|---|---|
| `GET /device/delegation/{user_code}` | Consent page |
| `POST /device/delegation/{user_code}/approve` | Approve (AAL2 + fresh) |
| `POST /device/delegation/{user_code}/deny` | Deny |

Scopes granted are the **intersection** of what you requested and what the human
actually holds at approval time. Empty intersection fails at approve.

## Step 5 – Poll For The Delegated Token

### CLI track

<!-- agent:action Poll for delegated token
Run: sigid-cli delegation poll <device_code> --idp "$SIGID_IDP"
Handle authorization_pending / slow_down until tokens or access_denied / expired_token.
Do not write the delegated token over the direct agent cache entry.
-->

```bash
sigid-cli delegation poll "$DEVICE_CODE" --idp "$SIGID_IDP"
# Optional single check: sigid-cli delegation poll "$DEVICE_CODE" --once --idp "$SIGID_IDP"
```

### curl track

```bash
# Respect interval from initiate (default often 5s). On slow_down, increase wait.
while true; do
  resp="$(curl -sS -w '\n%{http_code}' "$SIGID_IDP/api/v1/agents/delegations/device/token" \
    -X POST \
    -H "authorization: Bearer $AGENT_ACCESS_TOKEN" \
    -H "content-type: application/json" \
    -d "{\"device_code\":\"$DEVICE_CODE\"}")" || true
  body="$(printf '%s' "$resp" | sed '$d')"
  code="$(printf '%s' "$resp" | tail -n1)"
  case "$code" in
    200)
      # Success: JSON token bundle. Capture to a secret store; do not echo.
      printf '%s\n' "$body" > /dev/null
      break
      ;;
    400)
      err="$(printf '%s' "$body" | sed -n 's/.*"error"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)"
      case "$err" in
        authorization_pending) sleep 5 ;;
        slow_down) sleep 10 ;;
        access_denied|expired_token) echo "terminal: $err" >&2; exit 1 ;;
        *) echo "unexpected error body" >&2; exit 1 ;;
      esac
      ;;
    *) echo "unexpected HTTP $code" >&2; exit 1 ;;
  esac
done
```

**Success body (shape)**

```json
{
  "access_token": "<delegated>",
  "token_type": "Bearer",
  "expires_in": 900,
  "scope": "applications:manage",
  "refresh_token": "<optional>"
}
```

Validate before use: signature, `iss`, `aud`, expiry, tenant, scopes,
`subject_type`, and delegated `act` (human AAL evidence on the chain). See
[Verify Access Tokens](verify-tokens.md).

## Step 6 – Revoke A Delegation (Optional)

Revoke an **issued** delegation by its delegation id (not a device code). This
hits `POST /api/v1/delegations/{id}/revoke` – a different router from the
device flow – and the server enforces **fresh + highest-AAL step-up**. If the
cached agent token is rejected (401/403), pass a freshly minted stepped-up
token via `--access-token`.

```bash
sigid-cli delegation revoke "$DELEGATION_ID" --idp "$SIGID_IDP"
```

Revocation is immediate and reflected on the backend; subsequent use of the
delegated token is rejected at token-exchange time.

## Failure Modes

| Symptom | Cause | What to do |
|---|---|---|
| PoW start/complete fails | Bad ownership proof, expired challenge, insufficient difficulty/duration, registration disabled | Regenerate proof; restart PoW; check tenant registration policy |
| Bootstrap / app create `403` | Missing `applications:manage` or wrong tenant token | Use bootstrap tenant token; re-run `setup` or switch tenant |
| Device initiate `403` | Not a direct agent principal, inactive agent, or delegated bearer | Re-auth with `sigid-cli auth` / setup; never poll with delegated token |
| Empty scope / approve fails | Requested scopes not held by human or not delegatable for audience | Narrow `--scope`; human must hold each scope |
| Poll `authorization_pending` | Human has not finished | Keep polling at `interval` |
| Poll `slow_down` | Polling too fast | Increase wait (`Retry-Interval` header or +5s) |
| Poll `access_denied` | Human denied | Stop; start a new device request if still needed |
| Poll `expired_token` | Device request timed out | Start a new `delegation create` |
| Approve blocked / step-up loop | Human not AAL2 or session not fresh (~5 min) | Complete MFA; re-authenticate freshly |
| AAL2 routes still reject after poll | Freshness window elapsed or missing `act` validation | Re-run device flow; validate `act` / `auth_time` on the token |
| `delegation revoke` 401/403 | Cached token not fresh or not highest-AAL (step-up required) | Re-auth freshly; pass stepped-up token via `--access-token` |

## Security Checklist

- [ ] No tokens, secrets, `device_code`, or private keys in logs, tickets, or git
- [ ] `client_secret` captured once or rotated via `sigid-cli app rotate-secret`
- [ ] Unused delegations revoked via `sigid-cli delegation revoke`
- [ ] Direct agent token kept separate from delegated token
- [ ] Least-privilege scopes on device initiate
- [ ] Operator invite / co-owner path used before treating sandbox as long-lived
- [ ] Hosted authorize tested with fixture humans and application-owned sample data recorded
- [ ] Operator handoff accepted by the developer (invite alone does not transfer the agent)

## See Also

- [Agent And MCP Auth](agents-mcp.md) – integration overview
- [Agent Registration](registration.md) – tenant-scoped agent CRUD
- [Agent Authentication](agent-auth.md) – challenge-response runtime auth
- [Delegation And Token Exchange](delegation.md) – RFC 8693 exchange (after a grant exists)
- [Use Vault And Egress](vault-egress.md) – discover vault credentials, call external APIs through egress, sign SSH certificates
- [Agent CLI](cli.md) – full `sigid-cli` reference
- Public shortest path: [www quickstart](https://www.sigid.org/quickstart.md)
- Self-test script: [agent-quickstart.sh](agent-quickstart.sh)
