Machine-to-Machine Authentication¶
Applies to: All subscriptions
Purpose¶
Explain how an ERP proves its identity to Complifly, how its authority is bounded, and how to move from static keys to identity-provider tokens without interrupting a live integration.
Audience¶
Integration developers, identity administrators, security reviewers.
Prerequisites¶
- Authentication and Security
- For OAuth: an identity tenant and the ability to register an application
- For static keys: administrator access to issue them
Reference¶
Two mechanisms, accepted side by side¶
The ingest and integration endpoints accept either mechanism. Both are supported simultaneously so that a live integration can migrate without an outage.
| OAuth token | Static key | |
|---|---|---|
| Header | Authorization: Bearer <token> |
X-API-Key: cfly_..., or the same value as a bearer token |
| Lifetime | Short-lived, refreshed by the client | Long-lived until rotated |
| Revocation | At the identity provider | In the Complifly admin console |
| Suits | New integrations; enterprises with an identity platform | Simple integrations; migration from an earlier deployment |
| Security posture | Preferred | Acceptable, with disciplined rotation |
Both produce the same authorisation context downstream, so no endpoint behaves differently depending on which was used.
OAuth, in practice¶
The application is registered in your own Microsoft Entra directory, not in Complifly's. Complifly never holds your client secret; it stays in your directory and you rotate it there on your own schedule. What Complifly stores is two public identifiers — your directory (tenant) ID and your application (client) ID — neither of which can authenticate anything on its own.
Your ERP obtains a token from your directory using the client-credentials flow. Complifly validates it in this order:
| # | Validation | Consequence if wrong |
|---|---|---|
| 1 | The directory and application are a known pair, and the connection is active | Rejected as an unknown client, before anything else happens |
| 2 | Signature, against your directory's published keys | Token rejected |
| 3 | Issuer is exactly your directory. Both token-version issuer forms are accepted | Rejected |
| 4 | Audience. Both the application identifier URI and the bare identifier may be needed, depending on token version | Rejected |
| 5 | The required application role | Rejected as forbidden, not unauthorised — the token is valid, it simply lacks the granted role |
Step 1 comes first deliberately. A token from a directory Complifly does not serve is refused without any lookup against that directory at all.
The role check at step 5 is the one integration developers most often stumble on. A token that authenticates but is refused usually means the application role was never granted through admin consent — not that the credential is wrong.
Your directory and your application ID are both required¶
An application ID does not identify you by itself. The same application can be installed in many directories and keeps the same ID in each, so Complifly recognises a connection only as the pair: this application, in this directory.
Two consequences follow, and both are normal:
- A connection cannot be made live before its directory is known. There is nothing to pair the application with.
- The same application ID may legitimately appear under two different customers. That is not a conflict.
Onboarding a connection, step by step¶
Onboarding spans two organisations, so it is not instant. Complifly tracks which stage a connection has reached, and both sides can see who is holding it up.
| Stage | Waiting on | What has to happen |
|---|---|---|
PENDING_CONSENT |
You | Your Global Administrator opens the consent link |
AWAITING_CLIENT |
You | You send Complifly your application (client) ID |
ACTIVE |
— | Tokens are accepted |
SUSPENDED |
Complifly | Deliberately switched off |
Work through the steps in order. Steps 1 and 6 are Complifly's; steps 2 to 5 happen in your own Entra directory, and Complifly cannot see or perform them.
1. Complifly creates the connection¶
Not customer configurable
This step requires Platform Super Admin in Complifly. Your Complifly account manager performs it; a customer administrator cannot. Ask for it, giving your company and the list of GSTINs the ERP will submit for.
Complifly records the connection against your company and its permitted GSTINs, and sends you an admin-consent link. The connection is now at PENDING_CONSENT.
The link is valid for 15 minutes and can be used once. Have your Global Administrator ready before asking for it. A lapsed link is expected, not a fault — ask for a fresh one.
2. Your Global Administrator grants admin consent¶
Intended for your identity administrator
This step needs Global Administrator or Privileged Role Administrator in your Entra directory. A Complifly administrator cannot do it for you.
- Open the consent link in a browser signed in as that administrator.
- Read the permission being requested and accept it.
- You are returned to a Complifly confirmation page.
This provisions the Complifly API's service principal in your directory — which is what makes the API selectable in your own app registrations at step 4. Your Directory (tenant) ID is captured automatically, and the connection moves to AWAITING_CLIENT.
If the browser never returns to Complifly, your network blocked the redirect. Your administrator's consent still counts and nothing needs repeating — send Complifly your Directory (tenant) ID (Entra admin centre → Overview) and it is recorded directly.
3. Register your ERP's application¶
In the Entra admin centre, as an administrator of your directory:
- Go to Identity → Applications → App registrations.
- Select New registration.
- Name it for the ERP and the environment —
SAP S/4 Production → Complifly, notApp1. - Leave Supported account types at single tenant. A redirect URI is not needed: the client-credentials flow does not use one.
- Select Register.
- On the Overview page, copy the Application (client) ID and the Directory (tenant) ID. You need the first at step 5.
Give each ERP its own registration. Sharing one across ERPs makes it impossible to revoke a single integration without breaking the others, and makes the two indistinguishable to Complifly — which matters when they share a GSTIN, because a shared registration is one connection and receives one write-back stream.
4. Grant your application the Invoice.Post role¶
Still in your new app registration:
- Open API permissions → Add a permission.
- Choose APIs my organisation uses and search for the Complifly API. It appears here only because step 2 provisioned it; if it is missing, step 2 did not complete.
- Select Application permissions — not delegated. There is no signed-in user in this flow.
- Tick the
Invoice.Postrole. - Select Add permissions, then Grant admin consent for <your directory>, and confirm the status column reads Granted.
The role is named Invoice.Post unless your deployment overrides it through the AZURE_REQUIRED_ROLE setting. If a token is refused as forbidden while everything else checks out, confirm the expected role name with Complifly.
Step 5 of token validation checks this role. Consent at step 2 does not grant it — that consent installed the API in your directory; this step grants your application permission to call it. Missing this is the single most common OAuth stumble, and it presents as a valid token refused with 403.
5. Send Complifly the application (client) ID¶
Send the Application (client) ID from step 3. Complifly pairs it with the directory captured at step 2, and the connection goes ACTIVE.
Never send your client secret. Complifly does not need it, does not store it, and cannot use it. It stays in your directory, and you rotate it there on your own schedule.
6. Prove it end to end¶
Have the ERP acquire a token from your directory using the client-credentials flow, and call a low-risk endpoint:
GET /api/v1/integration/health
Authorization: Bearer <token>
Any response other than 401 or 403 means authentication and authorisation both work — even 404 NO_SUBSCRIPTION.
What your administrator needs, in one list¶
| Item | Where it comes from |
|---|---|
| Global Administrator (or Privileged Role Administrator) | Your directory |
| The consent link | Complifly — valid 15 minutes, single use |
Application role Invoice.Post on the Complifly API |
Selected when you add the API permission at step 4 |
| Your application's Application (client) ID | App registration overview — send this to Complifly |
| Your Directory (tenant) ID | Directory overview — needed only if the redirect is blocked |
Your client secret is not on that list and must never be sent to Complifly.
Static keys, in practice¶
A key is issued in the admin console and shown once. Store it in a secret manager at that moment; it cannot be retrieved later.
| Property | Detail |
|---|---|
| Scope | Company-wide. A key may submit for any GSTIN registered under its company. Per-GSTIN scoping is an OAuth capability |
| Revocation | Immediate, from the console. Permanent |
| Rotation | Issue the new key, migrate the caller, then revoke the old one |
| OAuth-only mode | A key's record can be marked so that the key is refused and the client must use OAuth. Reversible, and the safer first step |
Keys issued before this change may still carry a GSTIN restriction. Some older keys have a single GSTIN recorded against them, and that restriction is still enforced for those keys. Newly issued keys are company-wide. If an existing integration is refused for a registration you expected it to serve, this is the likely cause — check the key's recorded scope rather than assuming the credential is broken.
Two ways to close a key¶
Require OAuth refuses that client's static key while leaving everything else untouched. A caller already on OAuth notices nothing; one still using the key is told clearly to switch. It is reversible, which makes it the right first move when you are not certain who still holds the key.
Revoke is permanent. Reach for it once you are sure.
Neither undoes work already done — documents posted with a key remain valid.
GSTIN scope is the real authorisation boundary¶
A credential's authority is bounded by the registrations it may act for. A document whose supplier registration is outside that boundary is refused, regardless of anything else the credential may do.
This is the property to explain to a security reviewer: a compromised ERP credential can submit documents only for the registrations that ERP was ever entitled to.
Where the boundary comes from differs by mechanism, and the difference matters:
| Mechanism | Boundary |
|---|---|
| OAuth connection | The explicit GSTIN list attached to that connection. One connection can cover a portfolio of registrations, or exactly one |
| Static key with a recorded GSTIN | That one registration |
| Static key without one | Every GSTIN registered under the key's company |
| Situation | Outcome |
|---|---|
| Document's supplier GSTIN is in scope | Accepted |
| Out of scope | Refused as forbidden |
| Credential scoped to several registrations | All are accepted |
An OAuth connection is therefore the tighter instrument: its list is set deliberately at onboarding, whereas a company-wide key follows whatever registrations are added to the company later.
A forbidden response on ingest is almost always a scope mismatch, not a broken credential — a distinction worth building into the ERP's own error handling.
Migrating from keys to OAuth, step by step¶
The dual-path design exists for exactly this, and it can be done without an outage. Both mechanisms stay open throughout, so there is no cut-over moment at which traffic can fall on the floor.
- Complete the consent handshake above, so the connection reaches
ACTIVE. Check: ask Complifly for the connection's stage.AWAITING_CLIENTmeans step 5 above was never done. - Have the ERP acquire a token and call a low-risk endpoint.
Check: accepted. A
403here is theInvoice.Postrole, not the credential. - Switch the ERP's production calls to the token. Check: documents are still being accepted, at the usual rate.
- Watch for a full business cycle, including month end. Check: no code path fell back to the key. Do not skip this — month-end processing frequently uses paths that never run during a normal week.
- Mark the key OAuth-only, so it is refused. Check: a deliberate call with the old key is refused. This is reversible, which is what makes it the right move before revocation.
- Once a further cycle passes cleanly, revoke the key. Check: the credential list shows no key that is both superseded and still accepted.
A superseded key still works until you close it. Moving an ERP to OAuth does not disable its old key. Both paths stay open, and only one of them is being watched. The admin console lists which keys would still be accepted today — treat a superseded-but-accepted key as an open door, not as history.
Validation¶
| Check | Method | Pass condition |
|---|---|---|
| Connection reached ACTIVE | Ask Complifly for the connection's stage | ACTIVE, not AWAITING_CLIENT |
| Credential authenticates | Call a low-risk endpoint | Accepted |
| Token comes from your own directory | Decode the token and read tid |
Your directory ID, not Complifly's |
| An unknown application is refused | Present a token from an app registration Complifly was never told about | Refused as an unknown client |
| Scope enforced | Submit a document for an out-of-scope registration | Refused as forbidden |
| Revocation is immediate | Revoke, then call again | Refused |
| Role requirement enforced | Present a token without the required role | Refused as forbidden, not unauthorised |
| Audience validated | Present a token for a different audience | Rejected |
| Expiry enforced | Present an expired token | Rejected with an expiry indication |
| Key not recoverable | Attempt to view an issued key in the console | Not retrievable |
| One credential per ERP | Review issued credentials | Each ERP has its own |
| Rotation rehearsed | Perform a full rotation in a non-production environment | Completes without interrupting the integration |
| Secrets stored properly | Review where the ERP holds the credential | In a secret manager, not in code or a configuration file in source control |
| Client secret never shared | Search your correspondence with Complifly | The secret appears nowhere. Only the application and directory IDs were sent |
| Superseded keys closed | Review the credential list after migrating to OAuth | No key is both superseded and still accepted |
Troubleshooting¶
| Symptom | Cause | Action |
|---|---|---|
| HTTP 403 "not authorised for Complifly" on the first ever call | The connection was never made live — Complifly has your directory but not your application ID, or neither | Send Complifly your application (client) ID. Check the connection's stage before debugging anything else |
| HTTP 403 immediately after moving the ERP to a new app registration | The new application ID was never sent to Complifly. The pair no longer matches | Send the new application ID. A registration change is a connection change |
| HTTP 401 with a token that looks correct | Wrong audience, wrong issuer, expired, or clock drift | Decode the token and compare audience and issuer against the configuration. Check host time synchronisation |
| HTTP 403 with a valid token | The required application role was not granted | Grant it through admin consent. This is the most common OAuth stumble |
| HTTP 403 on ingest, credential otherwise fine | The document's supplier GSTIN is outside the credential's scope | Widen the scope deliberately, or send from the correct credential |
| Static key rejected although not revoked | The key has been marked OAuth-only | Complete the OAuth migration for that client |
| Key lost | Keys are shown once and are not retrievable | Issue a new key, migrate, revoke the old one |
| Token validation fails after working for months | Identity provider signing keys rotated and cannot be fetched | Confirm outbound access to the provider's key endpoint |
| All integrations fail at once after a change | One credential shared across several ERPs | Issue separate credentials per ERP |
| Cannot revoke one integration without breaking others | Same cause | Same fix |
| Intermittent authentication failures | Clock drift on the ERP or application host | Correct time synchronisation. Token validation is time-sensitive |
| Integration works in testing, fails at month end | A code path exercised only at month end uses a different credential or endpoint | Test through a full cycle before decommissioning the old credential |
The consent link gives an AADSTS error before any Complifly page appears |
The link expired, was already used, or the redirect address does not match what is registered | Ask for a fresh link. If it fails again, report the full AADSTS code — it is raised inside your directory and Complifly cannot see it |
| Consent completed but the browser never returned to a Complifly page | Your network blocked the redirect | Nothing needs repeating. Send your Directory (tenant) ID so it can be recorded directly |
| Consent succeeded, tokens still refused | Consent provisions the API in your directory; it does not by itself grant your ERP's application the role | Add the API permission to your own application, select Invoice.Post, and grant admin consent for that application too |
| The old static key still works after the OAuth cut-over | Moving to OAuth does not disable the key | Mark the key OAuth-only, then revoke it once a full cycle has passed |
| Two ERPs on one GSTIN, and only one receives write-back events | They share an application registration, so they are one connection | Give each ERP its own registration and its own connection |
Related Articles¶
- User Authentication — the other plane
- Ingest API — what these credentials call
- Write-back API — same authentication
- Integration Guides — per-ERP setup
- Security Hardening — storing and rotating secrets
- Integration Error Codes — every code these endpoints return
- Write-back Failures — when authentication is fine but events do not arrive