Authentication
Every API call carries an OAuth 2.0 access token: Authorization: Bearer <token>. A platform obtains tokens for its service account with the client credentials grant at the token endpoint of the realm (https://id.eimhub.example/realms/eimhub/protocol/openid-connect/token in these examples; your environment's is in the console's API access panel and in the oauth2 security scheme of the API reference).
A service account authenticates in one of two ways, chosen when it is created and fixed afterwards.
Roles
A token says who you are; your roles decide what you may call. Each operation in the API reference starts with a Roles line naming the predefined roles that may call it — for example Roles: TenantAdmin, SubTenantAdmin, Operator, Integrator. A call by a principal holding none of them is answered 403 FORBIDDEN.
- Scope. A role is bound at a scope: the whole tenant, a sub-tenant or a group. It lets you call an operation on the objects within that scope only; an object outside it is answered as if it did not exist (
404) or403. - Several roles. A principal holding several roles holds all of their permissions together.
- Your own permissions.
GET /api/v1/me/permissions?resource=euicc&owner_path=<path>returns what you hold for a resource at a path;GET /api/v1/melists your role bindings. - High-risk actions. Some requests need a high-risk permission in addition to the operation's own roles: deleting an eUICC, correcting an association's counter,
DELETE_EIM, anUPDATE_EIMthat changes a key. They also need areasonin the request. Where only some requests of an operation are high-risk, its Roles line adds For …: with the roles that may make them. The Integrator role holds no high-risk permission.
Some rules are not role lists, so the Roles line cannot show them:
- Role assignment. An administrator may assign a role at a scope only when they hold every permission of that role there themselves.
- Quotas and caps. Your tenant's quotas are set by eimHub; limits on a sub-tenant are set from above it (by a principal at its parent or tenant-wide), never by a principal scoped to that sub-tenant itself.
- Jobs. A job (an import, an export, a bulk operation) is visible to the principal that created it and to principals holding, at the job's path, the permission its creating request needed.
Client secret
The token request carries the client id and the secret:
curl -s https://id.eimhub.example/realms/eimhub/protocol/openid-connect/token \
-d grant_type=client_credentials \
-d client_id=sa-acme-erp \
-d client_secret="$CLIENT_SECRET"
The secret is shown once, when the account is created or its secret rotated. Keep it in your secret store; eimHub cannot show it again.
Secret rotation
An administrator rotates the secret in the console (Rotate secret) or with POST /api/v1/service-accounts/{user_id}:rotate-secret. The answer carries the new secret once and previous_secret_expires_at:
- Normal rotation. The previous secret keeps obtaining tokens for 24 hours. Deploy the new secret to every client within that window; tokens already issued stay valid until they expire.
- Suspected leak. With Revoke the previous secret now (
{"revoke_previous": true}) the previous secret stops obtaining tokens at once.
Key pair (private_key_jwt)
Your platform holds a private key; eimHub holds only the public key. The token request carries a short-lived JWT — the client assertion (RFC 7523) — that you sign with the private key:
| Claim | Value |
|---|---|
header kid |
the id of the key that signed it |
iss, sub |
your client id |
aud |
the token endpoint |
jti |
a unique value per assertion |
exp |
at most 60 seconds ahead (iat = now) |
Keys are EC P-256 (ES256) or RSA of 2048 bits or more (RS256 / PS256), each with a distinct kid.
import time, uuid
import jwt, requests # pip install 'pyjwt[crypto]' requests
TOKEN_URL = "https://id.eimhub.example/realms/eimhub/protocol/openid-connect/token"
CLIENT_ID = "sa-acme-erp"
now = int(time.time())
assertion = jwt.encode(
{
"iss": CLIENT_ID,
"sub": CLIENT_ID,
"aud": TOKEN_URL,
"jti": uuid.uuid4().hex,
"iat": now,
"exp": now + 60,
},
open("private-key.pem").read(),
algorithm="ES256",
headers={"kid": "acme-2026-10"},
)
token = requests.post(
TOKEN_URL,
data={
"grant_type": "client_credentials",
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"client_assertion": assertion,
},
).json()["access_token"]
eimHub learns your public keys in one of two ways:
- Registered keys. One or two public JWKs, given at creation or later with
PUT /api/v1/service-accounts/{user_id}/keys({"keys": [...]}). - A JWKS URL. An
httpsURL on port 443 where you publish your JWK Set ({"jwks_url": "https://keys.example.com/jwks.json"}). eimHub fetches it every hour and when asked to (POST /api/v1/service-accounts/{user_id}/keys:refresh); a failed fetch keeps the last good set and is shown asjwks_error.
Never send a private key: eimHub refuses a key with private members, and a private key that was sent anywhere must be replaced.
Key rotation
- Registered keys. Register the new key beside the old one (two keys are the rotation overlap), move your clients to it, then register the new key alone. A key registered with
PUT …/keysis usable, and a key left out of it is refused, from the next token request. - A JWKS URL. Publish the new key in your set and call
…/keys:refresh. The new key is usable once eimHub has fetched it; allow up to about 10 more seconds before the first token request signed with it succeeds.
Ending a compromised key
Removing a key from your published set does not end it: the identity provider may keep accepting a key it has already read. To end a compromised key:
- replace the keys with
PUT /api/v1/service-accounts/{user_id}/keysand{"keys": [...]}— registered keys without the compromised one — which takes effect from the next token request; or - have an administrator delete the service account.
If the PUT answers 503 DEPENDENCY_UNAVAILABLE, your keys are stored but not yet in force: repeat the same request.
Token caching
Access tokens are valid for 15 minutes (expires_in in the token response). Cache the token and reuse it for every call until shortly before it expires; request a new one then, or when a call answers 401 UNAUTHENTICATED. Requesting a token per call adds latency and counts against the identity provider's limits.