REST quickstart

From credential to first takeoff in ten minutes.

Everything below talks to https://api.aginera.ai/partner/v1 with an Authorization: Bearer … token. Pick one of the two token flows in step 1 — both call the same services, permissions and billing.

1. Get a token

Option A — Delegated OAuth (self-serve, no partner setup)

OAuth 2.1 authorization code + PKCE against https://auth.aginera.ai. A user approves once, you get a refreshable token scoped to their organization. No client secret is issued — register a public client and use PKCE.

AS=https://auth.aginera.ai

# Register a public client once
curl -s -X POST $AS/oauth/register -H "Content-Type: application/json" -d '{
  "client_name":"My integration",
  "redirect_uris":["http://localhost:8765/callback"],
  "grant_types":["authorization_code","refresh_token"],
  "response_types":["code"],
  "token_endpoint_auth_method":"none"
}'
# → 201 { "client_id": "oc_…", … }

# PKCE pair, then send the user to authorize
VERIFIER=$(openssl rand -base64 96 | tr -d '\n=+/' | cut -c1-64)
CHALLENGE=$(printf %s "$VERIFIER" | openssl dgst -binary -sha256 | openssl base64 | tr '+/' '-_' | tr -d '=\n')
open "$AS/oauth/authorize?response_type=code&client_id=$CLIENT_ID\
&redirect_uri=http://localhost:8765/callback&code_challenge=$CHALLENGE&code_challenge_method=S256\
&scope=projects:write+documents:write+takeoffs:read+takeoffs:run+exports:create+billing:read&state=$(uuidgen)"

# Exchange the returned ?code for a token
curl -s -X POST $AS/oauth/token \
  --data-urlencode grant_type=authorization_code --data-urlencode "code=$CODE" \
  --data-urlencode "redirect_uri=http://localhost:8765/callback" \
  --data-urlencode "client_id=$CLIENT_ID" --data-urlencode "code_verifier=$VERIFIER"
# → { "access_token":"…", "refresh_token":"…", "expires_in":3600, "token_type":"Bearer" }

Refresh with grant_type=refresh_token before expires_in. Scopes and discovery endpoints are in Authentication.

Option B — Service credentials (partner backends)

OAuth 2.0 client credentials, issued by Microsoft Entra ID. Use this for unattended backends, CI and batch jobs.

curl -s -X POST "https://login.microsoftonline.com/$AGINERA_TENANT_ID/oauth2/v2.0/token" \
  -d grant_type=client_credentials -d client_id="$CLIENT_ID" -d client_secret="$CLIENT_SECRET" \
  -d scope="api://$AGINERA_API_APP_ID/.default"
# → { "access_token":"…", "expires_in":3600, "token_type":"Bearer" }

$AGINERA_TENANT_ID and $AGINERA_API_APP_ID are fixed for Aginera; $CLIENT_ID / $CLIENT_SECRET are provisioned for your organization during partner onboarding — email developers@aginera.ai. Cache the token until five minutes before expires_in.

Confirm the token and see exactly what it can do:

API=https://api.aginera.ai/partner/v1
curl -s $API/clients/me -H "Authorization: Bearer $TOKEN"
# → { "mode":"…", "scopes":[…], "billing_mode":"artifact", "rate_limits":{…}, "quotas":{…} }

2. Create a project and add a drawing

curl -s -X POST $API/projects -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" -d '{"name":"Bavarian Lofts","external_ref":"BL-2026"}'
# → { "id": "prj_…", … }

Add a drawing one of two ways — give Aginera a URL to pull, or upload the bytes directly.

# By URL
curl -s -X POST $API/documents -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"project_id":"prj_…","filename":"E-series.pdf","source":{"type":"url","url":"https://files.example.com/E-series.pdf"}}'
# → 202 { "id":"doc_…","status":"ingesting" }

# By upload — request a slot, PUT the bytes with the returned headers, then complete
curl -s -X POST $API/documents -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"project_id":"prj_…","filename":"E-series.pdf","source":{"type":"upload","size_bytes":8123400,"content_type":"application/pdf"}}'
# → 201 { "document": { "id":"doc_…","status":"awaiting_upload", … },
#          "upload": { "url":"https://…","method":"PUT","headers":{…},"expires_at":"…" } }

# PUT the bytes to upload.url using upload.method, sending every header from upload.headers
curl -s -X PUT "$UPLOAD_URL" -H "x-ms-blob-type: BlockBlob" --data-binary @E-series.pdf
curl -s -X POST $API/documents/doc_…/complete -H "Authorization: Bearer $TOKEN"

Poll GET /documents/{id} until status is ready (you get page_count, sheet numbers and whether each page has a text layer).

3. Run a takeoff

curl -s -X POST $API/takeoffs -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"document_id":"doc_…","discipline":"electrical"}'
# → 202 { "id":"tko_…","status":"queued","estimated_credits":80, … }

estimated_credits is the maximum that will be charged; the final charge never exceeds it. Poll GET /takeoffs/{id} until status is completed, or register a webhook. Restrict to specific sheets with "pages":[3,4,5].

New accounts start with trial credits, so your first takeoffs run without buying anything. Check GET /credits for the balance and rate card; billable operations (takeoffs:run, estimates:generate) draw down credits exactly as they do in the web app, and you top up when the balance runs low.

4. Read the results

curl -s "$API/takeoffs/tko_…/items?consolidated=true&units=imperial" -H "Authorization: Bearer $TOKEN"
curl -s "$API/takeoffs/tko_…/routes?system=conduit" -H "Authorization: Bearer $TOKEN"
curl -s "$API/takeoffs/tko_…/schedules" -H "Authorization: Bearer $TOKEN"
curl -s -X POST $API/exports -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"takeoff_id":"tko_…","format":"xlsx"}'
# → 202 { "id":"exp_…","status":"…","download":{…},"credits_charged":… }

Every item carries extracted_quantity (immutable), quantity_override (yours) and effective_quantity, plus geometry and evidence in page-normalized coordinates (origin top-left, page as displayed).