# OAuth 2.0

OAuth lets a POS, accounting platform, or other backend application act for
companies that have explicitly approved it.

The model has three separate parts:

- The client application authenticates with `client_id` and `client_secret`.
- A company administrator creates an explicit Grant for that application.
- A short-lived access token is issued for one Grant and its scopes.


Company hierarchy never creates permission automatically.

## Authorization flow

### 1. Start authorization

Redirect a company administrator to:

```text
https://api.cheqi.io/oauth2/authorize
  ?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https%3A%2F%2Fyour-app.example%2Foauth%2Fcallback
  &scope=receipt%3Aissue%20store%3Aread%20company%3Aread
  &code_challenge=BASE64URL_SHA256_CODE_CHALLENGE
  &code_challenge_method=S256
  &state=RANDOM_UNGUESSABLE_VALUE
```

Generate a high-entropy `code_verifier`, derive its Base64URL-encoded SHA-256
`code_challenge`, and retain the verifier server-side until the code exchange.
Cheqi requires `code_challenge_method=S256`.

Validate `state` when the administrator returns. Cheqi shows the requested
permissions and companies, and creates an explicit Grant for each approval.

### 2. Receive the authorization code

Cheqi redirects to your registered callback:

```text
https://your-app.example/oauth/callback?code=AUTHORIZATION_CODE&state=STATE
```

The authorization code is short-lived and single-use. Do not store it as a
long-term credential or attempt to reuse it after a successful exchange.

### 3. Exchange the code

Send the code to `POST /oauth2/authorization-code/exchange`:

```bash
curl -X POST https://api.cheqi.io/oauth2/authorization-code/exchange \
  -H "Content-Type: application/json" \
  -d '{
    "code": "AUTHORIZATION_CODE",
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET",
    "code_verifier": "YOUR_CODE_VERIFIER"
  }'
```

Cheqi verifies the verifier against the original S256 challenge before
consuming the authorization code. A missing or incorrect verifier is rejected.

The response contains the approved Grant metadata:

```json
{
  "grants": [
    {
      "grant_id": "grant-uuid",
      "grant_version": 1,
      "issuer_company_id": "company-uuid",
      "company_legal_name": "Example Retail B.V.",
      "tax_id": "NL123456789B01",
      "scopes": ["receipt:issue", "store:read", "company:read"],
      "store_ids": ["store-uuid-1", "store-uuid-2"],
      "store_access_mode": "SELECTED_STORES"
    }
  ]
}
```

Store the `grant_id`, Company ID, and returned `store_ids` with your mapping to
the customer. The Grant ID is not a secret; the client secret remains
server-side. A Grant with unrestricted store access may return an empty
`store_ids` list, meaning all active stores belonging to that Company are
eligible.

The authorization-code response provides identifiers. Use the Grant-details
endpoint below when the POS needs current Store names, codes, addresses, or
active status.

The Company identity included in this consent response describes what the
administrator approved. It is not a substitute for the `company:read` scope;
that scope controls later Company-information API operations.

## Request a company access token

Request a token for the Grant that represents the customer transaction:

```bash
curl -X POST https://api.cheqi.io/oauth2/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET",
    "grant_id": "grant-uuid",
    "scope": "receipt:issue"
  }'
```

Cheqi verifies the client credentials, Grant ownership and status, and the
requested scope. The Company is resolved from the Grant; the client cannot
choose an arbitrary Company ID.

```json
{
  "access_token": "opaque-access-token",
  "token_type": "bearer",
  "expires_in": 900,
  "scope": "receipt:issue"
}
```

Cache the token by `grant_id` and scope until shortly before it expires. Then
request another token with the same Grant. This flow does not issue a refresh
token.

## Synchronize Company and Store details

After obtaining a Company-bound token, fetch the current mapping with:

```bash
curl https://api.cheqi.io/oauth2/grants/grant-uuid \
  -H "Authorization: Bearer opaque-access-token"
```

The response contains the current Company details, permitted active Stores,
Grant version, and scopes:

```json
{
  "grant_id": "grant-uuid",
  "grant_version": 2,
  "company": {
    "id": "company-uuid",
    "companyLegalName": "Example Retail B.V."
  },
  "stores": [
    {
      "id": "store-uuid-1",
      "storeName": "Amsterdam",
      "storeCode": "AMS-01",
      "isActive": true
    }
  ],
  "scopes": ["receipt:issue", "store:read", "company:read"],
  "store_access_mode": "SELECTED_STORES"
}
```

This endpoint requires `company:read`. Call it again when synchronizing store
changes; the access token and Grant must still be active.

## Why `grant_type` is present

The two exchanges use separate endpoints:

| Endpoint | Purpose |
|  --- | --- |
| `POST /oauth2/authorization-code/exchange` | Redeem the one-time code returned after administrator consent and receive Grant metadata. |
| `POST /oauth2/token` | Authenticate the application and receive a short-lived token for one approved Grant. |


`grant_type` is still required in the `/oauth2/token` request because it
identifies the `client_credentials` OAuth token flow. It is not a permission;
the Grant and requested scope provide authorization.

## Use the access token

Send the token as a bearer token for recipient resolution and receipt
submission:

```http
Authorization: Bearer opaque-access-token
```

Use the same token through the complete receipt flow. Cheqi validates the live
Grant, token scope, Company, Store ownership, Store restrictions, and active
status on each protected operation.

## Use the access token with an SDK

The SDKs request a token for one Grant and accept it per call through
`RequestOptions`. They do not cache or refresh tokens: store each token with
its `expires_in` in your own token store, keyed by `grant_id` and scope, and
request a new one shortly before it expires.

```java Java
OAuthTokenExchangeResponse token = sdk.getOAuthService().requestAccessToken(
    new ClientCredentialsTokenRequest()
        .clientId(clientId)
        .clientSecret(clientSecret)
        .grantId(grantId)
        .scope("receipt:issue"));

ReceiptResult result = sdk.getReceiptService().issueReceipt(
    identificationDetails,
    receiptPayload,
    RequestOptions.withAccessToken(token.getAccessToken()));
```

```csharp .NET
var token = await sdk.OAuthService.RequestAccessTokenAsync(new ClientCredentialsTokenRequest
{
    ClientId = clientId,
    ClientSecret = clientSecret,
    GrantId = grantId,
    Scope = "receipt:issue"
});

var result = await sdk.ReceiptService.IssueReceiptAsync(
    identificationDetails,
    receiptPayload,
    options: RequestOptions.WithAccessToken(token.AccessToken!));
```

```javascript JavaScript
const token = await sdk.oauthService.requestAccessToken({
  client_id: clientId,
  client_secret: clientSecret,
  grant_id: grantId,
  scope: "receipt:issue"
});

const result = await sdk.receiptService.issueReceipt(
  identificationDetails,
  receiptPayload,
  { accessToken: token.access_token }
);
```

```python Python
token = sdk.oauth_service.request_access_token(ClientCredentialsTokenRequest(
    client_id=client_id,
    client_secret=client_secret,
    grant_id=str(grant_id),
    scope="receipt:issue",
))

result = sdk.receipt_service.issue_receipt(
    identification_details,
    receipt_payload,
    options=RequestOptions.with_access_token(token.access_token),
)
```

A per-call access token takes precedence over a configured API key. A call
with neither fails before any request is sent. The SDK token request carries
the client credentials in the request body and sends no bearer token.

## Multiple companies

One client application can have Grants for thousands of companies. Keep one
client credential, but request a separate short-lived access token for each
Grant. A revoked Grant affects only that company; other Grants remain active.

Never infer authority from parent/child or franchise relationships, and never
let an untrusted request select a Company independently of its Grant.

## Automatic and employee-submitted receipts

Accounting and expense integrations can support two distinct receipt-delivery
capabilities:

| Scope | Company policy it enables |
|  --- | --- |
| `receipt:read` | Receive receipts automatically when the company is an authorized match. |
| `receipt:read:user-shared` | Additionally allow company members to submit individual owned receipts to the company integration. |


An integration that supports employee submissions must register and request
both scopes. They remain separate consent decisions: `receipt:read` alone does
not allow employee submissions, and `receipt:read:user-shared` never
participates in automatic matching. Cheqi accepts the user-shared scope on a
Company Grant only together with `receipt:read`.

Start company authorization with both scopes and the company being connected:

```text
https://api.cheqi.io/oauth2/authorize
  ?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https%3A%2F%2Fyour-app.example%2Foauth%2Fcallback
  &scope=receipt%3Aread%20receipt%3Aread%3Auser-shared
  &company_id=COMPANY_UUID
  &code_challenge=BASE64URL_SHA256_CODE_CHALLENGE
  &code_challenge_method=S256
  &state=RANDOM_UNGUESSABLE_VALUE
```

After exchanging the authorization code, request the Company-bound access
token with the same two scopes and register the `RECEIPT_CREATED` webhook with
that token. The subscription is then bound to the client application and that
exact Company.

Automatic and employee-submitted receipts use the same encrypted queue and
`ReceiptEnvelope`. Employee submissions carry
`deliveryPurpose: COMPANY_EXPENSE`, `expenseSubmissionId`, and
`submittedByUserId` in the webhook metadata. See
[Receipt Webhooks](/webhooks/receipt-webhooks) for delivery details.

## Scopes

The current scope names are:

| Scope | Allows |
|  --- | --- |
| `receipt:issue` | Resolve recipients and issue receipts for an authorized company. |
| `receipt:read` | Receive receipts automatically from authorized resources. |
| `receipt:read:user-shared` | Additionally allow users to submit individual receipts to a connected application or company integration. |
| `store:read` | View stores and locations for an authorized company. |
| `store:manage` | Create and manage stores and locations for an authorized company. |
| `company:read` | View company profile and Grant metadata. |
| `company:members:invite` | [Invite ordinary Company members](/companies/inviting-employees) after authorization. |


## Security

- Keep `client_secret` only in a server-side secret manager.
- Never place it in browser code, mobile apps, terminals, URLs, or logs.
- Use TLS for every request.
- Validate OAuth `state` and the exact redirect URI.
- Rotate client credentials with overlapping active credentials.
- Treat `401` as an authentication failure and `403` as a Grant or scope decision.