> This page is for Developers.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://zuora.ferndocs.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://zuora.ferndocs.com/_mcp/server.

# Authentication

Zuora uses **OAuth 2.0 client credentials** for API authentication. You exchange a client ID and secret for a time-limited Bearer token, then include that token in every API request.

> **Warn**
>
> Legacy API keys (`apiAccessKeyId` / `apiSecretAccessKey`) are deprecated. Migrate to OAuth 2.0 for all new integrations.

## Create an OAuth Client

### Open Zuora Settings

Log in to the Zuora UI and navigate to **Settings > Administration > Manage Users**.

### Create a new OAuth client

Click **Create OAuth Client**. Enter a descriptive name (e.g., "Billing Integration") and select the appropriate user whose permissions the client should inherit.

### Save your credentials

Copy the **Client ID** and **Client Secret** immediately. The secret is only displayed once.

> **Error**
>
> Store your client secret securely. Never commit it to version control or expose it in client-side code.

## Get a Bearer Token

Call the `/oauth/token` endpoint with your credentials:

#### cURL

```bash
curl -X POST https://rest.zuora.com/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id=b64e42ba-7e1a-4bc6-9146-5e1b420306b5" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "grant_type=client_credentials"
```

#### Python

```python
import requests

response = requests.post(
    "https://rest.zuora.com/oauth/token",
    data={
        "client_id": "b64e42ba-7e1a-4bc6-9146-5e1b420306b5",
        "client_secret": "YOUR_CLIENT_SECRET",
        "grant_type": "client_credentials",
    },
)

token_data = response.json()
access_token = token_data["access_token"]
expires_in = token_data["expires_in"]  # 3599 seconds

print(f"Token expires in {expires_in} seconds")
```

#### TypeScript

```typescript
interface TokenResponse {
  access_token: string;
  token_type: "bearer";
  expires_in: number;
  scope: string;
  jti: string;
}

async function getToken(): Promise<string> {
  const response = await fetch("https://rest.zuora.com/oauth/token", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      client_id: "b64e42ba-7e1a-4bc6-9146-5e1b420306b5",
      client_secret: "YOUR_CLIENT_SECRET",
      grant_type: "client_credentials",
    }),
  });

  const data: TokenResponse = await response.json();
  return data.access_token;
}
```

#### Java

```java
import java.net.URI;
import java.net.http.*;

public class ZuoraAuth {
    public static String getToken() throws Exception {
        HttpClient client = HttpClient.newHttpClient();
        String body = "client_id=b64e42ba-7e1a-4bc6-9146-5e1b420306b5"
            + "&client_secret=YOUR_CLIENT_SECRET"
            + "&grant_type=client_credentials";

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://rest.zuora.com/oauth/token"))
            .header("Content-Type", "application/x-www-form-urlencoded")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse<String> response = client.send(
            request, HttpResponse.BodyHandlers.ofString());

        // Parse JSON to extract access_token
        return parseAccessToken(response.body());
    }
}
```

### Token Response

```json
{
  "access_token": "c652cbc0ea384b9f81856a93a2a74538",
  "token_type": "bearer",
  "expires_in": 3599,
  "scope": "user.7c4d5433dc234c369a01b9719ecd059f entity.1a2b7a37-3e7d-4cb3-b0e2-883de9e766cc tenant.19",
  "jti": "c652cbc0ea384b9f81856a93a2a74539"
}
```

| Field          | Description                                             |
| -------------- | ------------------------------------------------------- |
| `access_token` | The Bearer token to use in API requests                 |
| `expires_in`   | Token lifetime in seconds (typically 3599 = \~1 hour)   |
| `scope`        | Permissions granted, including user, entity, and tenant |
| `token_type`   | Always `bearer`                                         |

## Use the Token

Include the token in the `Authorization` header of every API request:

```bash
curl -X GET https://rest.zuora.com/v1/accounts \
  -H "Authorization: Bearer c652cbc0ea384b9f81856a93a2a74538" \
  -H "Content-Type: application/json"
```

## Token Management Best Practices

> **Info**
>
> Tokens expire after approximately 1 hour. Implement token caching and automatic refresh in your integration.

### Recommended pattern

```python
import time
import requests

class ZuoraClient:
    def __init__(self, client_id: str, client_secret: str, base_url: str):
        self.client_id = client_id
        self.client_secret = client_secret
        self.base_url = base_url
        self._token = None
        self._token_expiry = 0

    def _get_token(self) -> str:
        if self._token and time.time() < self._token_expiry - 60:
            return self._token

        response = requests.post(
            f"{self.base_url}/oauth/token",
            data={
                "client_id": self.client_id,
                "client_secret": self.client_secret,
                "grant_type": "client_credentials",
            },
        )
        response.raise_for_status()

        data = response.json()
        self._token = data["access_token"]
        self._token_expiry = time.time() + data["expires_in"]
        return self._token

    def request(self, method: str, path: str, **kwargs) -> requests.Response:
        headers = kwargs.pop("headers", {})
        headers["Authorization"] = f"Bearer {self._get_token()}"
        headers["Content-Type"] = "application/json"
        return requests.request(
            method, f"{self.base_url}{path}", headers=headers, **kwargs
        )
```

### Key recommendations

1. **Cache tokens** -- Reuse a token until it expires instead of requesting a new one per API call.
2. **Refresh proactively** -- Refresh the token 60 seconds before expiry to avoid failed requests.
3. **Handle 401 errors** -- If you receive a `401 Unauthorized`, refresh the token and retry.
4. **Avoid rate limits** -- The `/oauth/token` endpoint has its own rate limit. Requesting too many tokens triggers a `429`.
5. **Scope awareness** -- The token scope determines which entities and operations are accessible.

## Multi-Entity Authentication

If your Zuora tenant uses Multi-entity, include the `Zuora-Entity-Ids` header to target a specific entity:

```bash
curl -X GET https://rest.zuora.com/v1/accounts \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Zuora-Entity-Ids: 11e643f4-a3ee-8bad-b061-0025904c756d" \
  -H "Content-Type: application/json"
```

The entity ID must be a valid UUID. You can find entity IDs in **Settings > Administration > Manage Entities**.