Subchapter 10.3
references/get-api-key.mdMarkdown10 KBView on GitHub
Backs Step 4 in SKILL.md. Runs after Step 3 authenticated the user (OAuth Bearer
token in ${TMPDIR:-/tmp}/dd-oauth-$(id -u).token, or a DD_API_KEY+ pair).
Produces a validated in . Display/wording rules are in .
DD_APP_KEYDD_API_KEY.envconventions.mdThe OAuth token authenticates the user, but downstream instrumentation (e.g. LLM Observability) needs a DD_API_KEY. Use the Bearer token to confirm identity, then obtain a key — retrieve the org’s most-recent key on an OAuth session (OAuth tokens cannot create keys), or create one when authenticating with an app key.
1. Confirm identity + region. First load the Bearer token Path B wrote to its 0600 file — shells don’t persist between commands, so re-read it at the top of each block that needs it. A 403 here means the token is for a different region — back to Step 2.
TOKEN=$(cat "${TMPDIR:-/tmp}/dd-oauth-$(id -u).token") # written by Path B Step 2 (inline exchange)
curl -s -H "Authorization: Bearer $TOKEN" \
"https://api.${DD_SITE}/api/v2/current_user" \
| grep -oE '"email"[[:space:]]*:[[:space:]]*"[^"]*"' | head -12. Get a DD_API_KEY and write it straight to .env. How depends on the auth in hand — and the two cases are genuinely different:
GET /api/v2/api_keys?page[size]=1&sort=-created_at → GET /api/v2/api_keys/{id} → data.attributes.key. Do NOT POST to create — minting an API key is not supported with an OAuth access token (the server rejects it), so it always fails. If the org has zero keys, or listing is role-denied, there is no OAuth create path → send the user to the org key page (they’re already signed in). Reading keys is not gated by a separate scope — it follows the OAuth user’s role, so no api_keys_read scope is needed.DD_API_KEY+DD_APP_KEY present): this path can POST /api/v2/api_keys to mint a fresh named key (an app key with api_keys_write is allowed to, unlike an OAuth token).The block below branches on that and prints the HTTP code of each step so a failure is diagnosable (list-denied vs empty-org vs secret-denied), not a blanket “403”. The secret is written straight to .env, never echoed. Send exactly one auth mechanism (never Bearer + API/APP together — that resolves to the API key’s org).
TOKEN=$(cat "${TMPDIR:-/tmp}/dd-oauth-$(id -u).token" 2>/dev/null)
# Reload DD_* (env > .env.local > .env) so the app-key branch below sees a file-only DD_API_KEY/DD_APP_KEY.
for f in .env.local .env; do [ -f "$f" ] || continue; for k in DD_SITE DD_API_KEY DD_APP_KEY; do eval
Read the outcome, don’t guess:
retrieved existing key / created key → the secret is in .env; go to step 3. You never see it — intentional.EMPTY_ORG (list 200, zero keys) → the org has no keys and OAuth can’t create one. Not an error — send the user to make one (they’re already signed in):
Your org has no API key yet, and I can’t mint one from an OAuth session. Create one here:
https://app.<DD_SITE>/organization-settings/api-keys→ + New Key → paste it back.
LIST_DENIED (list 403/404) → the OAuth user’s role can’t manage API keys in this org (common on managed/enterprise orgs like @datadoghq.com), or wrong region (identity check would also 403). Same manual step — or an org admin grants key-management, or use an app key (the DD_API_KEY+DD_APP_KEY path above).SECRET_DENIED (list OK, get denied) → role can list but not reveal secrets. Manual step.LIST_ERROR (list returned 5xx or another non-401/403/404 status) → a server-side or transient error, not permissions. Retry; if it persists, check the Datadog status page and connectivity to api.<DD_SITE>. Do not tell the user their access was denied.CREATE_DENIED (app-key POST returned non-2xx) → the app key lacks api_keys_write or is for another region. Create one manually — same page as above: https://app.<DD_SITE>/organization-settings/api-keys → + New Key → paste it back.TRANSPORT_ERROR (curl exited non-zero, no HTTP status) → not a permission problem: network, proxy, or a malformed URL (e.g. unescaped [ ] without -g). Re-run; if it persists, check connectivity/proxy to api.<DD_SITE>. Do not tell the user their key was denied.In every manual case, when the user pastes a key, don’t echo it — write it straight to .env: printf 'DD_API_KEY=%s\n' '<pasted>' >> .env && chmod 600 .env (after the same git-tracked/.gitignore guard as above).
3. Load it from .env (the actual /api/v1/validate check runs in Step 5): the secret lives in .env now — read it back for this shell; never paste or export the literal value.
# Load DD_* (env > .env.local > .env) — parse, don't source, so a crafted .env can't execute.
for f in .env.local .env; do [ -f "$f" ] || continue; for k in DD_SITE DD_API_KEY DD_APP_KEY; do eval "[ -n \"\${$k:-}\" ]" && continue; v=$(grep -E "^$k=" "$f" | head -1 | cut -d= -f2- | sed 's/^["'\'']//;s/["'\'']$//'); [ -n "$v" ] && export "$k=$v"; done; done
# App key, only if a downstream flow needs one: https://app.<DD_SITE>/organization-settings/application-keysNever fabricate or poll for a key — create it via the API (written to .env), or wait for the user to paste one (also written to .env). The secret never transits the model or stdout.
↳ Checklist: once a validated key is in hand, tick 4. Get & validate an API key, mark 5 ◔.