Authentication for integrations¶
Use this page to choose the authentication method for your integration, ask ESZ for the right credentials and make an authenticated request. Most partners calling an ESZ API use OAuth 2.0 client credentials and a short-lived Bearer token. Browser login is separate and requires no integration work from you.
Choose the method¶
| What needs to connect? | Method | What you need to do |
|---|---|---|
| Your system calls an ESZ API | OAuth 2.0 client credentials | Request an ESZ client, exchange its credentials for a token, then send the token as Bearer. |
| Your system sends an event to an ESZ webhook | Signed payload | Issue a webhook signing secret and sign the exact request body. There is no Bearer token. |
| ESZ calls your API | Your OAuth 2.0 client credentials | Provide ESZ with a client, token URL and the scopes required by your API. |
| An employee signs in through a browser | Microsoft sign-in managed by ESZ | Nothing. These sessions are not integration credentials. |
flowchart LR
partner[Your system]
auth[ESZ auth service]
api[ESZ resource API]
partner -- "1. client ID + secret" --> auth
auth -- "2. short-lived access token" --> partner
partner -- "3. Bearer token + API request" --> api
api -- "4. response" --> partner
Get an access token¶
1. Request client credentials¶
Ask your ESZ integration contact for an OAuth client. Include:
- the environment you need: staging or production;
- the ESZ API and operations your system will call;
- the least-privileged scopes those operations require;
- a technical owner and a rotation contact on your side.
ESZ will provide a client_id, a client_secret, the granted scopes and the
token lifetime through an agreed secure channel. Store the secret in a secret
manager, never in source control, an image, logs or support messages. Treat
staging and production as separate credential requests.
2. Exchange the credentials¶
Send a form-encoded request to the auth service. HTTP Basic authentication is recommended; the username is your client ID and the password is your client secret. The example asks for one granted scope:
POST /auth/oauth/token HTTP/1.1
Host: auth.esz.ag
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&scope=employees%3Apersonio%3Async
{
"access_token": "<signed JWT>",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "employees:personio:sync"
}
The scope parameter is optional. Omit it to receive every scope granted to
the client. When you provide it, use a space-separated subset of the granted
scopes. The response's scope field is the definitive list of what that token
can do.
For staging, use auth.esz.info; for production, use auth.esz.ag. The
environment reference lists the matching API host patterns.
3. Call the resource API¶
Send the access token—not the client secret—to the service that owns the endpoint:
POST /v1/personio/sync/ HTTP/1.1
Host: employees.esz.ag
Authorization: Bearer <signed JWT>
The service validates the signed token and its scopes locally. It never needs your client secret.
4. Renew before expiry¶
Cache the token for its expires_in lifetime and exchange the client
credentials again before it expires. Do not request a new token for every API
call. If an API responds with 401, obtain a new token once and retry once; do
not loop indefinitely. A 403 usually means the token is valid but lacks the
scope required by that operation.
| Token response | Meaning | What to check |
|---|---|---|
invalid_client |
The client could not authenticate. | Client ID, client secret, environment and whether the client is active. |
invalid_scope |
A requested scope is not granted. | Exact spelling and the scopes assigned to your client. |
unauthorized_client |
The client has no usable platform scopes. | Ask your ESZ contact to review its grants. |
unsupported_grant_type |
The request used another OAuth grant. | Send grant_type=client_credentials. |
The discovery document at
https://auth.esz.ag/.well-known/openid-configuration publishes the token and
JWKS endpoints.
The four hops¶
There are four distinct auth boundaries. They are not interchangeable, and confusing them is the most common source of wasted meetings.
| # | Hop | Direction | Mechanism | Who issues the secret |
|---|---|---|---|---|
| 1 | Employee's browser → any service | inbound, human | Microsoft OAuth at auth, then a signed JWT cookie validated locally by each service |
ESZ (auth service) |
| 2 | Your system → our sync/read endpoints | inbound, machine | OAuth2 client credentials: exchange your client id and secret for a short-lived RS256 token, then send it as Bearer; services verify it locally through JWKS. |
ESZ issues, you store |
| 3 | Your system → our webhook endpoint | inbound, machine | Signed payload (Svix-style headers: signature + timestamp + id). No bearer token. | You issue, ESZ stores |
| 4 | Our client → your API | outbound, machine | OAuth2 client credentials grant — we exchange client_id + client_secret at your token URL, cache the access token, send it as Bearer |
You issue, ESZ stores |
Hop 4 is what we use against every REST vendor today (Personio, Büro+, Business Central, Salesforce and Microsoft Graph). Inbound resource endpoints accept Bearer access tokens; they do not accept OAuth client credentials via HTTP Basic.
Practical consequences:
- Hop 2 client credentials and hop 4 credentials are different secrets in opposite directions. Both need a rotation owner named on each side; naming those owners is a discovery-phase deliverable, not engineering work.
- Hop 3 has no bearer token on purpose. The signature proves authenticity of the body; a leaked URL is not a leaked credential. Do not ask us to add a bearer token instead — ask us to add one as well, if your platform requires it.
- The employee-browser hop (1) never touches your system. Your integration is machine-to-machine throughout.
Hop 2 — you calling us (client credentials)¶
This is the default for a partner calling a protected ESZ endpoint. Follow
Get an access token, then use only scopes granted to
your client. Each resource service verifies the JWT locally; there is no
per-request call back to auth.
Current machine scopes are:
certificates:records:read,certificates:records:writecustomers:records:read,customers:buero-plus:sync,customers:business-central:sync,customers:salesforce:syncemployees:personio:syncintegrations:email:sendlogging:events:writeprocurements:buero-plus:sync,procurements:hivebuy:syncproducts:records:read,products:buero-plus:sync,products:salesforce:sync
Scopes match exactly. A certificate client that reads and writes requests both
certificates:records:read and certificates:records:write.
Fixed-header exception
If your platform cannot perform an OAuth exchange, raise that constraint
during discovery. ESZ may issue a long-lived Bearer token that your system
sends directly as Authorization: Bearer <token>. This is not the default
partner contract: the token cannot be individually revoked before expiry,
so it requires a named owner, explicit expiry and rotation plan.
Hop 3 — webhook signature verification¶
Inbound webhooks (you → us) carry no bearer token. Authenticity comes from a signature over the raw request body, using Svix-style headers:
webhook-id: msg_2c1f…
webhook-timestamp: 1731582000
webhook-signature: v1,g0hM9SsE…
We verify three things on every webhook:
- Signature — HMAC over
id.timestamp.bodywith the shared secret you issued, compared in constant time. A mismatch is a400. - Timestamp freshness — a request whose
webhook-timestampis outside a small tolerance window is rejected, so a captured payload cannot be replayed later. - Body integrity — the signature is over the exact bytes received; re-serialising the JSON before signing will not match.
You issue and own the shared secret; ESZ stores it. It needs a named rotation owner on your side.
Hop 4 — us calling you (client credentials)¶
Against your API we run the OAuth2 client-credentials grant: exchange the
client_id + client_secret you issued at your token URL, cache the access
token for its lifetime, and send it as Bearer on each call. On a 401 we
re-exchange once and retry. You issue and own these credentials; ESZ stores
them in the deployment secret store, never in a repository or image.
Rotation¶
Every secret in the table above needs a named rotation owner on both sides. Rotate ESZ-issued credentials (hop 2) with your ESZ integration contact. Rotation of credentials you issue (hops 3 and 4) is coordinated with the owner you name during discovery. Support an overlap window wherever your platform allows it so neither side has to switch a credential at an exact instant.