Skip to main content

Authentication Guide

This guide explains step by step how to obtain an access token to use the CorpX PIX API.

Overview

The CorpX API uses OAuth 2.0 with the Client Credentials flow for authentication. You will need your credentials (client_id and client_secret) to obtain a valid access token.

Prerequisites

Before you begin, make sure you have:

  • Client ID - Your unique client identifier
  • Client Secret - Secret key for authentication
  • X-Tenant-Id - Your tenant identifier (e.g., tenant-suaempresa)
info

If you don't have credentials yet, contact our support team.

Environments

EnvironmentAuthentication URLAPI URLNotes
Sandbox (dev)Temporarily unavailable.
Productionhttps://auth.api.corpx.com/oauth2/tokenhttps://tenant.api.corpx.com

Step 1: Request an Access Token

Request

curl -X POST "https://auth.api.corpx.com/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET"

Parameters

ParameterTypeRequiredDescription
grant_typestringYesAlways client_credentials
client_idstringYesYour client identifier
client_secretstringYesYour secret key
scopestringNoSpace-separated subset of the credential's scopes. Omitting it returns a token with all of them

Success Response (200 OK)

{
"access_token": "eyJraWQiOiJ0emwzZWVYWGx1eVlDWHFwQXdBTzJWYWJNQ0llMFMyMXVRWGV2Y281N2RRPSIsImFsZyI6IlJTMjU2In0...",
"expires_in": 300,
"token_type": "Bearer"
}
FieldDescription
access_tokenJWT token for authentication in API calls
expires_inValidity time in seconds
token_typeToken type (always Bearer)
Always trust expires_in

The TTL is not the same for every credential: the ones created in the backoffice panel last 5 minutes (expires_in: 300), while older, manually provisioned credentials still last an hour. Do not hardcode 3600 in your client — read expires_in from the response and renew comfortably ahead of it.

Error Response (401 Unauthorized)

{
"error": "invalid_client",
"error_description": "Client authentication failed"
}

Step 2: Use the Token in Requests

With the obtained token, include it in the Authorization header of all API requests.

Example: Check Balance

curl -X GET "https://tenant.api.corpx.com/v1/accounts/{accountId}/balance" \
-H "Authorization: Bearer eyJraWQiOiJ0emwzZWVYWGx1eVlDWHFwQXdBTzJWYWJNQ0llMFMyMXVRWGV2Y281N2RRPSIsImFsZyI6IlJTMjU2In0..." \
-H "X-Tenant-Id: tenant-suaempresa" \
-H "Content-Type: application/json"

Required Headers

HeaderDescription
AuthorizationAccess token in Bearer {access_token} format
X-Tenant-IdYour tenant identifier
Content-Typeapplication/json for requests with a body

Step 3: Renew the Token

The token expires after the time indicated in expires_in. We recommend:

  1. Store the token in cache with the expires_in that came in the response
  2. Renew before expiration — with a 5-minute TTL, renewing in the last minute is already tight
  3. Handle the 403 for expired tokens — the API answers 403 Forbidden, not 401; when you get it, obtain a new token and retry the call

Automatic Renewal Example (Bash)

#!/bin/bash

# Variables
CLIENT_ID="your_client_id"
CLIENT_SECRET="your_client_secret"
AUTH_URL="https://auth.api.corpx.com/oauth2/token"

# Function to get token
get_token() {
response=$(curl -s -X POST "$AUTH_URL" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=$CLIENT_ID" \
-d "client_secret=$CLIENT_SECRET")

echo "$response" | jq -r '.access_token'
}

# Get token
TOKEN=$(get_token)

# Use the token
curl -X GET "https://tenant.api.corpx.com/v1/accounts/{accountId}/balance" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Tenant-Id: tenant-suaempresa"

Full Examples in Different Languages

Python

import requests

# Credentials
CLIENT_ID = "your_client_id"
CLIENT_SECRET = "your_client_secret"
TENANT_ID = "tenant-suaempresa"

# Get token
auth_response = requests.post(
"https://auth.api.corpx.com/oauth2/token",
data={
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET
}
)

token = auth_response.json()["access_token"]

# Use the token
headers = {
"Authorization": f"Bearer {token}",
"X-Tenant-Id": TENANT_ID,
"Content-Type": "application/json"
}

response = requests.get(
"https://tenant.api.corpx.com/v1/accounts/{accountId}/balance",
headers=headers
)

print(response.json())

Node.js

const axios = require('axios');

const CLIENT_ID = 'your_client_id';
const CLIENT_SECRET = 'your_client_secret';
const TENANT_ID = 'tenant-suaempresa';

async function getToken() {
const response = await axios.post(
'https://auth.api.corpx.com/oauth2/token',
new URLSearchParams({
grant_type: 'client_credentials',
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET
}),
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
);
return response.data.access_token;
}

async function getBalance(accountId) {
const token = await getToken();

const response = await axios.get(
`https://tenant.api.corpx.com/v1/accounts/${accountId}/balance`,
{
headers: {
'Authorization': `Bearer ${token}`,
'X-Tenant-Id': TENANT_ID,
'Content-Type': 'application/json'
}
}
);

return response.data;
}

Credential scopes

Every credential (client_id/client_secret) carries a set of scopes that defines exactly what it can do. You pick the scopes when creating the credential in the backoffice panel (API Credentials, available to users with the tenant manager profile).

Scopes you grant yourself:

ScopeUnlocks
readAccount-wide queries: balance, statement, timeline, entries
qrcode.manageStatic and dynamic QR code: create, cancel and check the payment of your own QR
pix_keys.managePIX keys: list, create and delete
webhooks.manageWebhook subscriptions, deliveries and retries
exports.createStatement exports: create, track and download
med.defendMED: query, answer and attach evidence (does not decide the refund)

Every scope already queries its own domain. That is what makes an inflow-only credential possible: grant qrcode.manage alone and it issues QR codes and sees whether they were paid without access to the balance and the statement. Only check read when the credential genuinely needs the account-wide queries.

Scopes that move money are issued by CorpX, never from the panel:

ScopeUnlocks
pix_out.createPIX out in every variant (/pix/out/*), with the status of its own payments
refund.createRefund of a received PIX, with the status of its own refunds
internal_transfer.createInternal transfer, with the destination lookup
ted.createTED, with the status of its own transfers
boleto_payment.createBoleto preview, payment and status
med.decideDecide a MED refund

Requesting one of these from the panel is refused with 403 scope_not_self_service — contact support and we issue the credential.

Per-account restriction

A credential can be restricted to specific accounts of your tenant. That is how you give a subsystem a credential that only issues QR codes for one account. Calls to accounts outside the set answer 403 forbidden — the generic permission error, not insufficient_scope: the scope is there, the account is not. The message deliberately does not distinguish a nonexistent account from one outside the set.

Combining both dimensions: qrcode.manage plus a single account yields a credential that charges by QR code on that account and sees nothing else — neither its balance nor the tenant's other accounts.

When a scope is missing

If the credential lacks the scope for the route, the API answers 403 Forbidden naming what is missing:

{ "errorCode": "insufficient_scope", "message": "esta credencial não tem escopo para esta operação; é necessário o escopo qrcode.manage" }

Credentials issued before granular scopes keep working unchanged: they retain the coarse pair (api2/read api2/write) and full access.

Access suspended by a pendency

When there is an open pendency with CorpX, a tenant's access can be suspended temporarily. Suspension does not invalidate your credential: the same client_id/client_secret keeps issuing tokens normally, and the refusal comes on the API call.

StateWhat keeps workingResponse on other calls
SuspendedReads (GET) — balance, statement, operation status403 tenant_suspended on writes
DisabledNothing403 tenant_disabled on every route
{ "errorCode": "tenant_suspended", "message": "há uma pendência em aberto: operações de escrita estão suspensas até a regularização, consultas seguem disponíveis" }

Two things worth knowing:

  • Money does not stop. Incoming PIX is still credited and you still receive the webhooks for those events. What suspension blocks is starting new operations.
  • Reactivation is immediate. Once the pendency is cleared, access returns on the next call — no new credential and no new token needed.

The reason for the pendency is not in the error body: it shows up in the backoffice panel, for your team's operators.

Common Errors

ErrorCauseSolution
invalid_clientIncorrect Client ID or SecretCheck your credentials
invalid_grantInvalid grant typeUse client_credentials
401 UnauthorizedMissing Authorization headerSend Authorization: Bearer <token>
403 Forbidden (invalid/expired token)Expired token, invalid token, or bad signatureObtain a new token. Note that expiry is 403, not 401
403 insufficient_scopeCredential without the route's scopeCreate a credential with the scope named in the message
403 tenant_suspendedOpen pendency: writes are suspendedClear it with CorpX support; reads keep working
403 tenant_disabledTenant disabledContact CorpX support to restore access
403 forbiddenValid token, but no permission on that tenant or accountCheck X-Tenant-Id and whether the credential is restricted to other accounts

Best Practices

  1. Never expose the client_secret in client-side code (frontend)
  2. Use environment variables to store credentials
  3. Implement token caching to avoid unnecessary requests
  4. Monitor expiration and renew tokens proactively
  5. Use HTTPS for all communications

Next Steps

Now that you know how to authenticate, explore the other guides: