Quick start

From internet banking to the first balance. Set aside a few minutes and a server with a synced clock (NTP): the signature rejects a skew greater than 5 minutes.

Before you start

  1. Store clientId, clientSecret, X-Tenant-Id and accountId in environment variables — not in the repository.
  2. The private key lives only on the server that will sign. The public key is already registered; write down the kid.
  3. That server’s egress IP must be on the credential allowlist.
  4. Wait for activeFrom (18 hours after issuance). Until then every call returns 403 credential_not_yet_active. That is protection, not a defect.

If the credential does not exist yet, ask the holder to issue it in the bank’s internet banking. These pages do not cover the bank’s screens.

1. Request the token

The token lasts 5 minutes. Store it and reuse it until it is close to expiring — one token per request exhausts the issuer and can be refused.

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=$CLIENT_ID" \
-d "client_secret=$CLIENT_SECRET"
{
"access_token": "eyJraWQiOiJ…",
"expires_in": 300,
"token_type": "Bearer"
}

2. Check the signature without moving money

Every call to https://client.api.corpx.com/v1/** carries, besides the token and X-Tenant-Id:

HeaderValue
X-Request-TimestampUnix seconds (same value as in the canonical string)
X-Content-SHA256SHA-256 of the body as lowercase hex. Empty body = hash of empty
X-Request-SignatureDetached JWS — see Signing

Before the first balance, use POST /v1/security/signature/verify. It returns the canonical string we built and valid: true|false, and creates nothing. When valid is false, compare canonicalString character by character: the problem is almost always CRLF, a path without the query, or a timestamp in milliseconds.

There is a test vector with a public key, canonical string and JWS ready for offline validation.

3. First balance

Once the signature checks out:

curl -X GET "https://client.api.corpx.com/v1/accounts/$ACCOUNT_ID/balance" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Tenant-Id: $TENANT_ID" \
-H "X-Request-Timestamp: $TS" \
-H "X-Content-SHA256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" \
-H "X-Request-Signature: $SIG"

GET has no body: the hash is always the empty one above.

{
"accountId": "6ff57bc1-e4a9-403b-be62-42378b8aafd7",
"total": 18254.17,
"locked": 500.00,
"available": 17754.17,
"currency": "BRL",
"updatedAt": "2026-02-05T22:00:00Z"
}

Monetary values are BRL with at most 2 decimal places.

If you point this credential at https://tenant.api.corpx.com, the response is 403 signed_host_required. The old host does not verify a signature per request.

4. Confirm who you are

GET /v1/me (also on the signed host) returns clientId, scopes and the contexts the token is valid for. Use it to check that the X-Tenant-Id you will send is on the list.

First-day checklist

  • Token reused until expires_in
  • POST /v1/security/signature/verify with valid: true
  • GET /v1/accounts/{accountId}/balance on client.api.corpx.com
  • Server clock on NTP
  • Private key out of the repository and out of logs

After that

Contract: API reference. Filter operations whose x-audience contains ib.