Service Accounts
Use the OAuth 2.0 client credentials flow when your integration runs server-side with no human in the loop — MSP automation, reseller provisioning, scheduled sync jobs, or backend-to-backend integrations.
Managing users or orgs? Service accounts are the right choice for Partner API calls. Acting on behalf of a user for Drive or Sites? Use OAuth auth code + PKCE or a PAT bound to a user.
Prerequisites
A platform superadmin must register your service account in Keycloak and create a service principal in EUnifyer:
- In Back-office → Service Accounts: create a Keycloak confidential client with the service account role enabled.
- Note your
client_idandclient_secret. - Create a service principal in Admin → API Access linked to that Keycloak client ID.
- Assign scopes to the service principal.
For org-level (non-reseller) service accounts, an org admin can create them in Admin → API Access.
Get a token
curl -X POST https://auth.eunifyer.com/realms/eunify/protocol/openid-connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "scope=partner:orgs:read partner:orgs:write partner:users:write"const tokenRes = await fetch(
"https://auth.eunifyer.com/realms/eunify/protocol/openid-connect/token",
{
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
scope: "partner:orgs:read partner:orgs:write partner:users:write",
}),
},
);
const { access_token } = await tokenRes.json();Response:
{
"access_token": "eyJ...",
"expires_in": 900,
"token_type": "Bearer",
"scope": "partner:orgs:read partner:orgs:write partner:users:write"
}Access tokens are short-lived (15 minutes). Re-grant when they expire — there is no refresh token for client credentials.
Call the API
TOKEN=$(curl -s -X POST https://auth.eunifyer.com/realms/eunify/protocol/openid-connect/token \
-d "grant_type=client_credentials&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET" \
| jq -r .access_token)
curl https://api.eunifyer.com/api/v1/partner/orgs \
-H "Authorization: Bearer $TOKEN"import httpx
TOKEN_URL = "https://auth.eunifyer.com/realms/eunify/protocol/openid-connect/token"
API_BASE = "https://api.eunifyer.com/api/v1"
def get_token(client_id: str, client_secret: str) -> str:
r = httpx.post(TOKEN_URL, data={
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret,
})
r.raise_for_status()
return r.json()["access_token"]
token = get_token(CLIENT_ID, CLIENT_SECRET)
orgs = httpx.get(f"{API_BASE}/partner/orgs", headers={"Authorization": f"Bearer {token}"})
print(orgs.json())Token caching
Re-issuing a token on every API call is wasteful. Cache the token and re-grant when it is about to expire:
import time
_cached_token: str | None = None
_token_expires_at: float = 0.0
def get_cached_token(client_id: str, client_secret: str) -> str:
global _cached_token, _token_expires_at
if _cached_token and time.time() < _token_expires_at - 30:
return _cached_token
r = httpx.post(TOKEN_URL, data={
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret,
})
r.raise_for_status()
data = r.json()
_cached_token = data["access_token"]
_token_expires_at = time.time() + data["expires_in"]
return _cached_tokenManaging service principals via the API
List your service principals and their tokens from the Admin → API Access page in the UI, or via the API:
# List service principals
GET /api/v1/developer/service-principals
# Create a new service principal
POST /api/v1/developer/service-principals
{ "name": "MSP Provisioning Bot", "description": "…", "scopes": ["partner:orgs:write"] }
# Add a PAT to a service principal
POST /api/v1/developer/service-principals/{sp_id}/tokens
{ "name": "prod-key-1", "scopes": ["partner:orgs:write"] }These management endpoints require
admin:access. Use a first-party admin session or a superadmin PAT to set them up.
Security checklist
- Store
client_secretin a secrets manager — never in source code or.envfiles committed to git. - Use the narrowest scope set that covers what your service account does.
- Rotate
client_secretregularly; invalidate old tokens after rotation. - Monitor
X-RateLimit-Remaining— service accounts share the org’s rate-limit bucket.