Skip to content
TDACorpIDENTITYSign in

Authorization decision API

Ask identity whether a person may perform an action, one question or up to 200 at a time, and how a service authenticates to ask.

On this page

Tokens say who a person is. They do not say what the person may do: there is no roles claim. A product that needs that answer asks identity's decision API, which evaluates the roles, grants and relations on record and replies allow or deny.

Status: Available

A product is onboarded to these endpoints by TDACorp: the service client is linked to the product and the product is installed in each organization. The SDK's decision client signs the per-request proof the API requires. See Decision client.

Who may call it#

The API is not open to the public. The caller is a service, authenticating with its own access token.

RequirementDetail
TokenA client_credentials access token issued to the service's own client, for its own audience. A person's token, an exchanged token (one with act) and a token with a session are refused.
BindingThe token must be bound to a DPoP key. An unbound token is refused.
HeadersAuthorization: DPoP <token> and a DPoP proof for this request, with ath. The proof's htu is the issuer address plus the path being called.
NonceA proof without a current server nonce gets 401 with WWW-Authenticate: DPoP error="use_dpop_nonce", algs="EdDSA ES256" and a DPoP-Nonce header. Sign again with that nonce.
RegistrationThe client must be linked to your product, and your product must have an active installation in the organization you ask about (an active organization and an active service identity too). Otherwise the answer is 403, or a deny.

A caller can ask only about permissions in its own product's namespace. A question about another product's permission is answered as denied.

See Service to service for obtaining the token.

Single check#

POST /authz/v1/check, JSON body.

FieldRule
principal_sub, principal_id or subject_tokenExactly one. The person's sub from their token, their principal id, or (only while the pdp_subject_token flag is on, default OFF; otherwise 400) a compact access token, up to 8192 characters, that identity verifies itself. The token must be addressed to your client (aud), carry this request's organization as tid, and, if it has an act chain, carry a tdaid:agent_action and no more than three hops. Identity then evaluates the token's av and narrows the answer to the delegated actions. A token that fails any check is answered like an unknown person. With the flag on, a token with act and no tdaid:agent_action is refused.
min_rvOptional. A decimal string, the organization resource version (rv) you last saw after writing. If identity's version is behind it, the answer is INDETERMINATE with reason indeterminate_consistency; retry shortly.
tenantRequired. The organization identifier.
actionRequired. A concrete permission code of the form <namespace>.<action>, such as eidos.document.read. No wildcards.
resourceOptional. A resource reference of the form <namespace>:<type>:<id>, where the namespace is yours.

Unknown fields are refused with 400.

Batch check#

POST /authz/v1/check/batch, JSON body with principal_sub, principal_id or subject_token (exactly one), tenant, an optional min_rv, and items: between 1 and 200 objects, each with action and an optional resource. More than 200 is 400 (batch-too-large).

The response is { "results": [ ... ] }, one entry per item in the same order. An item that could not be evaluated is { "problem": { ... } } in its slot; it is never turned into a deny.

Decision members#

Both calls return the same decision object.

MemberMeaning
decisionALLOW, DENY or INDETERMINATE. Only ALLOW permits the action. Treat INDETERMINATE as a refusal.
reason_codeOn INDETERMINATE for a min_rv that is ahead, indeterminate_consistency. On ALLOW: allowed_by_binding, allowed_by_grant, allowed_by_relation or allowed_by_policy. On every DENY the value is access_denied, whatever the cause, so a denial does not reveal why.
av, pv, pvt, rvVersion counters, as strings: the person's authorization version, the product's policy version, the organization's policy version, the organization's resource version. They change when the answer might.
evaluated_atThe time of evaluation, ISO 8601.
ttl_hintSeconds the answer may be reused. 0 means ask again. Higher-risk permissions get shorter or no reuse.
hints.refresh_tokentrue only when the question carries a token authorization version older than identity's, which tells you to get a fresh token. Only a subject_token carries one, so without it expect false.
hints.last_known_goodtrue when you may keep using a previous ALLOW for the same question that is at most 300 seconds old.

GET /authz/v1/effective lists the permissions a person holds in your product for an organization, with permission, scope_ref, source, expires_at and delegable. It takes principal_sub or principal, tenant and app, and has the same caller rules.

Errors#

Errors are application/problem+json (RFC 9457) with type, title, status, detail and instance.

StatusSlugWhen
400invalid-requestThe body is not valid JSON, fails validation, or sets both or neither subject field. errors names the field.
400batch-too-largeMore than 200 items.
401unauthenticatedNo token, or the token is not a valid caller credential. A DPoP challenge accompanies a DPoP request.
403forbiddenThe client is not linked to a product, or its product is not installed in the organization.
413, 415Body too large, or not JSON.
429rate-limitedRefused for too many requests. Honour Retry-After.
500, 503internal-error, service-unavailableA decision could not be reached. This is never a disguised deny: fail closed and retry later.

Worked example#

curl -s https://identity.tdacorp.in/authz/v1/check \  -H "Authorization: DPoP $SERVICE_TOKEN" \  -H "DPoP: $DPOP_PROOF" \  -H 'content-type: application/json' \  -d '{    "principal_sub": "0194f2b6-7c1e-7a30-9d4e-2f6f0f3b8c11",    "tenant": "0194f2b6-1111-7000-8000-00000000aaaa",    "action": "eidos.document.read",    "resource": "eidos:document:q3-plan"  }'
{  "decision": "ALLOW",  "reason_code": "allowed_by_binding",  "av": "7",  "pv": "3",  "pvt": "1",  "evaluated_at": "2026-10-07T09:30:00.000Z",  "ttl_hint": 300,  "hints": { "refresh_token": false, "last_known_good": false }}

The values are illustrative.

Security notes#

  • Deny by default. An unknown action, an unknown person, or someone who is not an active member of the organization is denied.
  • The caller decides nothing. Identity decides. Your product enforces the answer. Never produce an ALLOW of your own because a call failed.
  • Fail closed. INDETERMINATE, a transport failure and a malformed reply all mean refuse.
  • Cache within the hint. Reuse an answer only for ttl_hint seconds, and use last_known_good only when identity says you may.
  • Keep the service key private. Whoever holds it can ask about every person in the organizations your product is installed in.

Verified against commit 02770c4