Authentication
All IDCloud APIs (Web & SDK and API contracts) use OAuth2 with JWT Bearer Grant Type (RFC 7523). You generate a short-lived JWT assertion on your back-end, exchange it for a Bearer token, and use that token in every subsequent call.
The JWT assertion must be generated on your back-end only. Never expose your private key in front-end code, mobile apps, repositories, or logs.
Obtaining credentials
Before you can generate tokens, you need a service account provisioned by Unico. Contact Unico support and provide:
- Service account name (max 12 characters)
- Responsible person's name, email and phone (Brazil, US or Mexico numbers only)
You will receive:
- Unique account name
- Tenant ID
- Base JWT payload
- Private key file (
.pemformat)
Keep separate service accounts for UAT and Production.
Building the JWT assertion
The assertion is a JWT in compact JWS format: {Base64url(Header)}.{Base64url(Payload)}.{Base64url(Signature)}.
{
"alg": "RS256",
"typ": "JWT"
}
| Claim | Value | Notes |
|---|---|---|
iss | <account_name>@<tenant_id>.iam.acesso.io | Provided with your credentials |
aud | https://identityhomolog.acesso.io (UAT) or https://identity.acesso.io (Production) | Must match the token endpoint host |
scope | * | Grants all permissions |
iat | Unix timestamp (seconds) | Time the JWT was issued |
exp | iat + max 3600 | Cannot exceed 1 hour from iat |
{
"aud": "https://identity.acesso.io",
"scope": "*",
"iat": 1738086000,
"exp": 1738089600
}
Sign the header + payload using RS256 (RSA + SHA-256) with the .pem private key provided by Unico.
Any field not listed above (e.g. sub, jti, nbf) will cause a 1.2.22 error. Use only the claims shown.
Requesting the token
The token endpoint is the same for both contracts:
| Environment | Endpoint |
|---|---|
| Production | POST https://identity.acesso.io/oauth2/token |
| UAT | POST https://identityhomolog.acesso.io/oauth2/token |
| Parameter | Value |
|---|---|
Content-Type | application/x-www-form-urlencoded |
grant_type | urn:ietf:params:oauth:grant-type:jwt-bearer |
assertion | Your signed JWT |
- cURL
- Node.js
- Python
curl -X POST https://identity.acesso.io/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
-d "assertion=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
import jwt from 'jsonwebtoken';
import fs from 'fs';
import qs from 'querystring';
const privateKey = fs.readFileSync('./private-key.pem');
const now = Math.floor(Date.now() / 1000);
const assertion = jwt.sign(
{
aud: 'https://identity.acesso.io',
scope: '*',
iat: now,
exp: now + 3600,
},
privateKey,
{ algorithm: 'RS256' }
);
const res = await fetch('https://identity.acesso.io/oauth2/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: qs.stringify({
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion,
}),
});
const { access_token, expires_in } = await res.json();
import time
import jwt # PyJWT
import requests
with open("private-key.pem", "rb") as f:
private_key = f.read()
now = int(time.time())
assertion = jwt.encode(
{
"aud": "https://identity.acesso.io",
"scope": "*",
"iat": now,
"exp": now + 3600,
},
private_key,
algorithm="RS256",
)
response = requests.post(
"https://identity.acesso.io/oauth2/token",
data={
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": assertion,
},
)
token_data = response.json()
access_token = token_data["access_token"]
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600
}
| Field | Type | Description |
|---|---|---|
access_token | string | JWT access token. Use in Authorization: Bearer <token> on all API calls. |
expires_in | integer | Expiration time in seconds. Example: 3600. |
token_type | string | Always Bearer. |
Using the token
Add the token to the Authorization header of every API request. The header is identical for both contracts — what differs is the host and path of the API you're calling:
| Contract | Production host | UAT host |
|---|---|---|
| Web & SDK | https://api.idcloud.unico.app | https://api.idcloud.uat.unico.app |
| API | https://api.id.unico.app | https://api.id.uat.unico.app |
Web & SDK — Authorization is the only auth header required:
curl -X POST https://api.idcloud.unico.app/client/v1/process \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ ... }'
API — Authorization is used alongside the APIKEY header:
curl -X POST https://api.id.unico.app/processes/v1 \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "APIKEY: $API_KEY" \
-H "Content-Type: application/json" \
-d '{ ... }'
Token renewal
Tokens expire after 3600 seconds. Implement proactive renewal in your back-end:
- Track
expires_infrom the token response and store the expiry timestamp. - Request a new token when 10 minutes or less remain before expiration.
- Never wait for a
401in production to trigger a renewal.
API reference
The full OpenAPI specification for the token endpoint is available in authentication.yaml.
| Method | Path | Description |
|---|---|---|
POST | /oauth2/token | Exchange a signed JWT assertion for a Bearer access token |
Error codes
| Code | Description | Action |
|---|---|---|
1.0.1 | The ID provided in the formation of iss is incorrect | Verify the iss field matches the tenant ID provided when the private key was generated |
1.0.14 | Application is not active | Check with the project manager if the application being used is active |
1.1.1 | The scope parameter was not provided | Add "scope": "*" to your JWT payload |
1.2.4 | JWT assertion invalid | The JWT assertion is no longer valid. Two causes: (a) the current time is past exp (JWT truly expired — generate a fresh assertion for each token request); or (b) exp exceeds iat + 3600 (lifetime too long — cap exp at iat + 3600). |
1.2.5 | JWT validation failed | The JWT cannot be validated. Verify the parameters and ensure it was signed with RS256 and the correct private key |
1.2.6 | Private key is no longer valid | The private key used to sign the JWT is no longer acceptable. Request new credentials for the account |
1.2.7 | JWT already used | The JWT is no longer acceptable as it has already been used. Generate a new assertion for each token request |
1.2.11 | Account is not active | The account used is not active |
1.2.14 | Account lacks necessary permissions | The account used does not have the necessary permissions |
1.2.18 | Account temporarily locked | The account has been temporarily locked due to exceeding the number of invalid authentication attempts |
1.2.19 | Unauthorized user impersonation | The JWT contains a sub claim pointing to an account that is not authorized for impersonation. Remove the sub claim from the payload. |
1.2.20 | JWT decoding failed | Failed to decode the JWT. Verify the token format and that it was signed with RS256. |
1.2.21 | Wrong private key / Authentication failed | The JWT signature could not be verified against any known key for this account. Check that you are using the correct .pem private key for this service account and environment. |
1.2.22 | Disallowed fields in payload | The JWT contains extra payload fields that are not allowed. Remove any claims not listed in this guide (e.g. sub, jti, nbf). Note: if you included a sub claim and received 1.2.19 instead, that error takes precedence. |
1.3.1 | IP access restriction | Your IP is not in the allowlist for this account |
1.3.2 | Time-based access restriction | Request is outside the permitted time window for this account |
What's next
- Environments — sandbox vs production hosts
- Web & SDK — Create Process — first call after authentication
- API — Create Process — first call after authentication