---
url: /authentication.md
---
# Authentication

The Maytes merchant API uses **OAuth 2.0 client credentials**. Your server exchanges its `client_id` and `client_secret` for a short-lived access token, then uses that token as a Bearer credential.

All calls are server-to-server. **Never put your `client_secret` in browser or mobile code.**

## Environments

Maytes has two environments, each with its **own base URL** and **own credentials** from the merchant portal. Every example on this site uses the production base URL:

```
https://api.maytes.co
```

Build against the **sandbox** first; no real money moves there. To go live, switch the base URL and credentials to production. The API shape is identical in both environments. See [Sandbox environment](/sandbox-environment) for the sandbox base URL and test cards.

## Get your credentials

Get your `client_id` and `client_secret` from the **Maytes merchant portal**. Each environment has a separate pair that works only with its matching base URL. Store credentials server-side in environment variables or a secret manager. See [Security](/security).

On the portal's **API credentials** page, generate a credential pair and view your `client_id`. The `client_secret` appears **only once** when generated or rotated, so store it immediately.

Only **merchant admin** portal users can access this page. Ask Maytes to grant admin access if your team needs it.

### Rotating your client secret

If you lose the secret or suspect a leak, rotate it from the portal's **API credentials** page and deploy the new value immediately. Rotation has **no grace window**: the old secret stops working at once. Existing access tokens remain valid until they expire, providing a short buffer before the next token request requires the new secret.

## Request an access token

```bash
curl -s https://api.maytes.co/oauth/token \
  -H 'content-type: application/json' \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET"
  }'
```

The response contains an `access_token`. Send it on every merchant API call:

```http
Authorization: Bearer <access_token>
```

## Token lifetime, caching, and refresh

* Access tokens are **short-lived**. Cache the token and reuse it across requests.
* **Don't compute expiry yourself.** When a token expires, the API returns **`401`**; fetch a new token and retry the request once.
* The official [backend SDKs](/build-your-integration) implement this "use until 401, then refresh and retry" pattern with a shared token cache and safe concurrent refresh.

```mermaid
sequenceDiagram
  autonumber
  participant M as Your backend
  participant K as Maytes API
  M->>K: POST /oauth/token (client_credentials)
  K-->>M: { access_token }
  M->>K: API call with Bearer token
  Note over M,K: ...later, token expires...
  M->>K: API call with stale token
  K-->>M: 401
  M->>K: POST /oauth/token (refresh)
  K-->>M: { access_token }
  M->>K: Retry the original call
  K-->>M: 200
```

## Use the SDK instead

With a [backend SDK](/build-your-integration), construct the client once with your credentials. It handles token requests, caching, and refresh after a 401:

```ts
import { createMaytesApiClient } from '@maytes/api-client-js';

const maytes = createMaytesApiClient({
  endpoint: 'https://api.maytes.co',
  clientId: process.env.MAYTES_CLIENT_ID!,
  clientSecret: process.env.MAYTES_CLIENT_SECRET!,
});
```

## Auth errors

| HTTP | Meaning |
|---|---|
| `401` | Missing, invalid, or expired access token — fetch a new token and retry. |
| `403` | Your merchant account is disabled or not permitted for this operation. |

See the full [error reference](/api-reference).
