Skip to content
Last updated

Implement OAuth 2.0 for third-party integrations and multi-merchant platforms.

Overview

OAuth 2.0 enables third-party applications to access Cheqi on behalf of merchants with their explicit authorization. This is ideal for POS systems, accounting software, and marketplace integrations serving multiple merchants.

OAuth 2.0 provides granular permission scopes and merchant-controlled authorization, making it the recommended choice for third-party platforms.

OAuth Flow

Cheqi implements the OAuth 2.0 Authorization Code Grant flow:

Cheqi APIYour ApplicationMerchantCheqi APIYour ApplicationMerchant1. Redirect to /oauth2/authorize2. Show authorization page3. Approve/Deny access4. Redirect with authorization code5. Exchange code for tokens6. Return access & refresh tokens7. Make API calls with access token

Getting Started

Approval Required

You must have an approved client application before merchants can authorize your integration. The approval process ensures security and quality for all Cheqi users.

1. Create a Client Application

Before implementing OAuth, you need to register your application in the Cheqi app:

Step 1: Register Your Application

In the Cheqi app:

  1. Navigate to ProfileCompany ProfileDeveloper ToolsClient Applications
  2. Tap Create New Client Application
  3. Fill in the details:
    • Application Name - How merchants will see your app
    • Description - What your integration does
    • Redirect URIs - Where users return after authorization (one per line)
    • Scopes Required - Permissions your app needs
  4. Submit for review

Step 2: Wait for Approval

Cheqi will review your application:

  • Review typically takes 1-2 business days
  • You'll receive a notification when status changes
  • Check application status in Client Applications section
  • Status will change from Pending to Approved or Rejected

Step 3: Generate Client Secret

Once approved:

  1. Go to Client Applications in the app
  2. Select your approved application
  3. Tap Generate Client Secret
  4. Copy the secret immediately - it will only be shown once

You now have:

  • Client ID - Public identifier (visible in app)
  • Client Secret - Secret key for token exchange (shown once)
  • Authorized Scopes - Permissions granted to your app

Step 4: Configure Your Application

Store credentials securely:

CHEQI_CLIENT_ID=your_client_id
CHEQI_CLIENT_SECRET=your_client_secret  # Store securely!
CHEQI_REDIRECT_URI=https://yourapp.com/oauth/callback
One-Time Secret

The client secret is shown only once during generation. Store it securely immediately.

Why Approval Is Required

Why approval is required: Cheqi reviews client applications to ensure they meet security standards and provide value to merchants. This protects the ecosystem and maintains trust.

2. Authorization Request

Redirect merchants to Cheqi's authorization endpoint:

GET https://api.cheqi.io/oauth2/authorize?
  client_id=your_client_id&
  redirect_uri=https://yourapp.com/oauth/callback&
  scope=read_receipts write_receipts&
  state=random_state_string

Building the URL in your application:

// Construct authorization URL
const url = "https://api.cheqi.io/oauth2/authorize";
const params = {
  client_id: "your_client_id",
  redirect_uri: "https://yourapp.com/oauth/callback",
  scope: "read_receipts write_receipts",
  state: generateRandomState()  // CSRF protection
};

// Redirect user to authorization URL
redirect(url + "?" + buildQueryString(params));

What happens:

  1. Your app redirects the merchant to /oauth2/authorize
  2. Cheqi displays the OAuth authorization web page with a requestId
  3. Merchant reviews and approves the authorization request on the web page
  4. After approval:
    • Merchant sees a success screen
    • Cheqi sends the authorization code to your configured redirect_uri (callback URL)

Parameters:

ParameterRequiredDescription
client_idYesYour application's client ID
redirect_uriYesWhere to redirect after authorization (must match registered URI)
scopeYesSpace-separated list of requested scopes
stateRecommendedRandom string for CSRF protection

3. Merchant Approves on OAuth Web Page

The merchant is redirected to Cheqi's OAuth authorization web page. What they authorize depends on the requested scopes:

Scenario 1: Write Receipts (Company-Level Access)

When requesting write_receipts scope, merchants authorize company-level access:

  1. Review your application details
  2. See requested scopes and permissions
  3. Select which companies to authorize (if they manage multiple companies)
  4. Approve or deny the request

Use case: POS systems, accounting software that sends receipts on behalf of the merchant's company.

Scenario 2: Read Receipts (Identifier Access)

When requesting read_receipts scope, merchants authorize access to receipts created with those identifiers:

  1. Review your application details
  2. See requested scopes and permissions
  3. Select which identifiers to authorize (cards, payment accounts, or emails)
  4. Approve or deny the request

Use case: Bookkeeping software, expense management apps, personal finance apps, expense trackers that read the user's receipts from their payment methods.

Customer Identifiers

Identifiers are the methods used to match receipts to customers:

  • Cards - Credit/debit card payment accounts
  • Payment Accounts - Bank accounts (IBAN)
  • Emails - Email addresses for receipt delivery
Key Difference

Key difference: write_receipts grants company-level permissions to send receipts. read_receipts grants access to individual identifiers and their associated receipts.

Behind the scenes:

  • Cheqi creates an OAuth request with a unique requestId
  • The merchant reviews the request on the web page at /oauth/authorize?requestId={requestId}
  • Upon approval, Cheqi calls /oauth2/approve/{requestId} internally with either company IDs or identifier IDs
  • An authorization code is generated

4. Handle Authorization Callback

After the merchant approves, Cheqi sends a request to your redirect_uri (callback URL):

https://yourapp.com/oauth/callback?code=AUTH_CODE&state=RANDOM_STATE

Callback Parameters:

ParameterDescription
codeAuthorization code (exchange for tokens)
stateThe state value you provided (verify it matches)

Example Handler:

// Handle OAuth callback
function handleOAuthCallback(request) {
  const code = request.query.code;
  const state = request.query.state;
  
  // Verify state to prevent CSRF
  if (state !== session.oauthState) {
    return error("Invalid state parameter");
  }
  
  // Exchange code for tokens (see next step)
  const tokens = await exchangeCodeForTokens(code);
  
  // Store tokens securely in your database
  await saveTokens(tokens);
  
  redirect("/dashboard");
}

5. Exchange Code for Tokens

Exchange the authorization code for access and refresh tokens:

curl -X POST https://api.cheqi.io/oauth2/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "code": "AUTH_CODE",
    "client_id": "your_client_id",
    "client_secret": "your_client_secret",
    "redirect_uri": "https://yourapp.com/oauth/callback"
  }'

Response:

[
  {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refresh_token": "rt_1234567890abcdef1234567890abcdef",
    "token_type": "AUTHORIZATION_CODE",
    "expires_at": "2024-01-13T23:00:00Z",
    "refresh_expires_at": "2024-02-13T22:00:00Z",
    "merchant_id": "550e8400-e29b-41d4-a716-446655440000",
    "tax_id": "NL123456789B01",
    "company_legal_name": "Example Coffee Shop B.V.",
    "customer_id": null
  }
]

Response Fields:

FieldDescription
access_tokenAccess token for API calls (expires in 1 hour)
refresh_tokenToken to get new access tokens (expires in 30 days)
token_typeGrant type ("AUTHORIZATION_CODE")
expires_atWhen the access token expires (ISO 8601)
refresh_expires_atWhen the refresh token expires (ISO 8601)
merchant_idCompany UUID this token is authorized for
tax_idCompany tax identification number
company_legal_nameLegal name of the company
customer_idCustomer UUID (null for company-level tokens)
Array Response

Array response: The response is an array because merchants can authorize access to multiple companies in a single flow. Store tokens separately for each merchant_id.

6. Use Access Tokens

Include the access token in API requests:

curl https://api.cheqi.io/receipt/template \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -H "Content-Type: application/json" \
  -d '{...}'

SDK Usage:

// Initialize SDK without API key
CheqiSdk sdk = new CheqiSdkBuilder()
    .environment(Environment.PRODUCTION)
    .build();

// Use access token for each request
ReceiptResult result = sdk.getReceiptService().sendReceipt(
    identificationDetails,
    receiptRequest,
    accessToken  // Pass token here
);

See SDK-specific guides for implementation: Java SDK | .NET SDK

Token Management

Access Token Expiration

Access tokens expire after 1 hour (3600 seconds). Monitor the expires_at field and refresh before expiration.

Refresh Tokens

Use refresh tokens to obtain new access tokens without re-authorization:

curl -X POST https://api.cheqi.io/oauth2/refresh \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "refresh_token",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "client_id": "your_client_id",
    "client_secret": "your_client_secret"
  }'

Response:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "rt_9876543210fedcba9876543210fedcba",
  "token_type": "AUTHORIZATION_CODE",
  "expires_at": "2024-01-13T23:00:00Z",
  "refresh_expires_at": "2024-02-13T22:00:00Z",
  "merchant_id": "550e8400-e29b-41d4-a716-446655440000",
  "tax_id": "NL123456789B01",
  "company_legal_name": "Example Coffee Shop B.V.",
  "customer_id": null
}
Token Replacement

New tokens: Both the access token AND refresh token are replaced when you refresh. Always store the new refresh token.

Token Refresh Strategy

class TokenManager {
  async getValidToken(companyId) {
    const tokens = await loadTokens(companyId);
    
    // Check if token expires in next 5 minutes
    if (this.isExpiringSoon(tokens.expires_at)) {
      return await this.refreshToken(tokens.refresh_token);
    }
    
    return tokens.access_token;
  }
  
  isExpiringSoon(expiresAt) {
    const fiveMinutes = 5 * 60 * 1000;  // 5 minutes in milliseconds
    return Date.now() + fiveMinutes >= new Date(expiresAt).getTime();
  }
  
  async refreshToken(refreshToken) {
    const response = await fetch('https://api.cheqi.io/oauth2/refresh', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        grant_type: 'refresh_token',
        refresh_token: refreshToken,
        client_id: process.env.CHEQI_CLIENT_ID,
        client_secret: process.env.CHEQI_CLIENT_SECRET
      })
    });
    
    const tokens = await response.json();
    await this.saveTokens(tokens);
    return tokens.access_token;
  }
}

Available Scopes

Request only the scopes your application needs:

ScopeAuthorization LevelDescription
write_receiptsCompanySend receipts to customers on behalf of the company
read_receiptsIdentifierRead receipt data from authorized identifiers (cards, payment accounts, emails)
read_storesCompanyList store locations for authorized companies
write_storesCompanyCreate and manage stores for authorized companies
company_accessCompanyAccess company-level information and features
account_accessIdentifierAccess user account information and settings
Scope Authorization Levels

Company-level scopes (write_receipts, read_stores, write_stores, company_access) require the merchant to authorize specific companies.

Identifier-level scopes (read_receipts, account_access) require the merchant to authorize specific identifiers:

  • Cards - Credit/debit card payment accounts
  • Payment Accounts - Bank accounts (IBAN)
  • Emails - Email addresses

Example:

// Company-level access (POS system)
const scope = "write_receipts read_stores";
// Merchant selects which companies to authorize

// Identifier access (expense tracker)
const scope = "read_receipts account_access";
// Merchant selects which identifiers to authorize (cards, payment accounts, emails)

Security Best Practices

Protect Client Secret

Protect Client Secret

Never expose your client secret in client-side code or version control.

// ❌ BAD - Client secret in frontend
const clientSecret = 'your_client_secret';

// ✅ GOOD - Client secret on backend only
const clientSecret = process.env.CHEQI_CLIENT_SECRET;

Validate State Parameter

Always verify the state parameter to prevent CSRF attacks:

// Generate random state before redirect
const state = crypto.randomBytes(32).toString('hex');
req.session.oauthState = state;

// Verify state in callback
if (req.query.state !== req.session.oauthState) {
  throw new Error('Invalid state parameter');
}

Store Tokens Securely

  • Encrypt tokens at rest
  • Use secure session storage
  • Never log tokens
  • Implement token rotation
// Encrypt before storing
const encryptedToken = encrypt(accessToken, encryptionKey);
await db.tokens.create({
  companyId,
  accessToken: encryptedToken,
  expiresAt: Date.now() + (expiresIn * 1000)
});

Handle Token Expiration

Implement automatic token refresh:

async function makeAuthenticatedRequest(url, options) {
  let token = await getValidToken();
  
  let response = await fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      'Authorization': `Bearer ${token}`
    }
  });
  
  // If 401, refresh and retry once
  if (response.status === 401) {
    token = await refreshToken();
    response = await fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        'Authorization': `Bearer ${token}`
      }
    });
  }
  
  return response;
}

Multi-Company Support

Merchants can authorize access to multiple companies:

// Token exchange returns array of tokens
const tokens = await exchangeCodeForTokens(code);

// Store tokens per company
for (const token of tokens) {
  await saveTokens(token.merchant_id, {
    access_token: token.access_token,
    refresh_token: token.refresh_token,
    expires_at: new Date(token.expires_at).getTime(),
    scope: token.scope
  });
}

// Use appropriate token for each company
const token = await getTokenForCompany(companyId);

Error Handling

Authorization Errors

ErrorDescriptionSolution
access_deniedUser denied authorizationShow friendly message, allow retry
invalid_clientInvalid client ID or secretVerify credentials
invalid_scopeRequested scope not allowedRequest only authorized scopes
invalid_grantInvalid or expired authorization codeRestart OAuth flow

Token Errors

Status CodeErrorSolution
401Invalid or expired tokenRefresh token or re-authorize
403Insufficient scopeRequest additional scopes
429Rate limit exceededImplement backoff and retry

Example Error Response:

{
  "error": "invalid_grant",
  "error_description": "The authorization code has expired",
  "timestamp": "2024-01-13T21:00:00Z"
}

Testing

Sandbox Environment

Test OAuth flow in sandbox:

const authUrl = new URL('https://sandbox.api.cheqi.io/oauth2/authorize');
authUrl.searchParams.append('client_id', 'sandbox_client_id');
authUrl.searchParams.append('redirect_uri', 'http://localhost:3000/callback');
authUrl.searchParams.append('scope', 'read_receipts write_receipts');

Token Validation

Verify tokens are valid:

curl https://api.cheqi.io/oauth2/token/validate \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

Response:

true

Next Steps