Provider Access Token Acquisition Guide
Audience: External provider teams integrating with Kontrolle+ Provider API.
Scope: How to obtain an access token. Does not cover payload schemas or submission rules.
:::info Machine-to-machine only Token flow is OAuth 2.0 client credentials — no user login is involved. :::
1. What You Need
Request the following values from the Kontrolle+ team before starting:
| Value | Description |
|---|---|
| Authority base URL | Microsoft Entra tenant endpoint |
| OAuth token endpoint | Full token URL |
| Client ID | Your provider application ID |
| Client secret | Credential for your application |
| Scope | <provider-api-app-id-uri>/.default |
Expected aud claim | Audience expected by the API runtime |
2. Request Format
POST <token-endpoint>
Content-Type: application/x-www-form-urlencoded
client_id=<provider-client-id>
client_secret=<provider-client-secret>
grant_type=client_credentials
scope=<provider-api-app-id-uri>/.default
3. curl — Request Token
curl -sS -X POST "https://<tenant-domain>/<tenant-id>/oauth2/v2.0/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "client_id=<provider-client-id>" \
--data-urlencode "client_secret=<provider-client-secret>" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "scope=<provider-api-app-id-uri>/.default"
Successful response includes token_type, expires_in, and access_token.
4. curl — Extract Token to Variable
ACCESS_TOKEN=$(curl -sS -X POST "https://<tenant-domain>/<tenant-id>/oauth2/v2.0/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "client_id=<provider-client-id>" \
--data-urlencode "client_secret=<provider-client-secret>" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "scope=<provider-api-app-id-uri>/.default" | jq -r '.access_token')
echo "$ACCESS_TOKEN"
5. Validate Token Claims
Before the first API call, check the token contains:
| Claim | Expected value |
|---|---|
aud | Must match the API runtime audience |
roles | Must include ProviderApi.Access |
exp | Must be in the future |
Quick decode (no signature verification):
echo "$ACCESS_TOKEN" | awk -F '.' '{print $2}' | tr '_-' '/+' | base64 -d 2>/dev/null
6. Common Errors
| Error | Cause | Fix |
|---|---|---|
400 invalid_client | Wrong client_id/client_secret, disabled credential, or expired secret | Verify credential pair and secret validity window |
400 invalid_scope | Incorrect scope value | Use exact value <provider-api-app-id-uri>/.default |
401 from API | Issuer/audience mismatch or expired token | Verify authority, aud claim, and token lifetime |
403 from API | Missing ProviderApi.Access role or client not allow-listed | Ensure app role assignment is complete |
7. Production Recommendations
- Cache the token until shortly before expiry — do not request one per API call.
- Rotate client secrets/certificates before expiration.
- Store credentials only in a secure secret management system.
- Never log full access tokens.
8. Integration Template (per environment)
Use this block as a runbook input when onboarding a new environment:
Environment: <ENV_NAME>
Authority: <AUTHORITY>
Token endpoint: <TOKEN_ENDPOINT>
Client ID: <CLIENT_ID>
Client secret delivery: <SECURE_CHANNEL_REFERENCE>
Scope: <PROVIDER_API_APP_ID_URI>/.default
Expected aud: <API_AUDIENCE>
Required role: ProviderApi.Access
See Also
- INT Test Credentials — copy-paste ready token commands for the INT environment
- API Usage Quickstart — how to call the API once you have a token