Connect merchant accounts
How a merchant account authorizes your integration with OAuth, and how to use, refresh, and revoke its tokens.
By the end, your integration holds the idToken of each linked merchant account, to create charges on its behalf, and knows how to refresh it, revoke it and unlink.
The OAuth flow between your backend, the merchant's browser and the Auth API.
- Your backend redirects to
GET /integration/oauth2/authorizewith a randomstate. - The merchant signs in on Chytapay and authorizes.
- The Auth API returns to your
redirect_uriwithcodeandstate. - Your backend validates the
state. - Your backend sends
code,clientIdandclientSecrettoPOST /integration/oauth2/token. - The Auth API returns the merchant's
idTokenandrefreshToken.
Before you begin
All from the same environment, Sandbox or Production:
- The
clientIdandclientSecret, in the Portal under Credenciales (Credentials). TheclientSecretgoes only from your backend to the Auth API (/token,/refreshand/revoke). - An
httpsredirect_uriregistered in the Portal under Configuración (Settings). - A merchant account: the one that receives the payments, not your integration admin user.
The flow runs against the Auth API, under /integration/oauth2/: https://auth-api.test.chytapay.com.ar in Sandbox and https://auth-api.chytapay.com.ar in Production.
1Send the merchant to authorize
In your backend, generate a random state, store it in the session and redirect the browser to GET /integration/oauth2/authorize:
| Parameter | Required | Value |
|---|---|---|
response_ |
Yes | code |
client_ |
Yes | Your integration's clientId |
redirect_ |
Yes | A registered redirect_, identical |
state |
No, but always use it | Random, one per attempt |
scope |
No | Defaults to openid email profile |
The camelCase names (clientId, redirectUri, responseType) are also accepted, but use snake_case in new code.
import crypto from 'node:crypto';
const AUTH_API = 'https://auth-api.test.chytapay.com.ar';
const REDIRECT_URI = 'https://yourapp.com/oauth/callback';
// Express handler: the merchant clicks "Connect with Chytapay".
app.get('/connect-chytapay', (req, res) => {
const state = crypto.randomBytes(32).toString('base64url');
req.session.chytapayState = state;
const url = new URL(`${AUTH_API}/integration/oauth2/authorize`);
url.search = new URLSearchParams({
response_type: 'code',
client_id: process.env.CHYTAPAY_CLIENT_ID,
redirect_uri: REDIRECT_URI,
state,
}).toString();
res.redirect(url.toString());
});The authorization screen shows your integration's name and logo, and the fee if you have an active offer. The merchant account signs in there, never the integration admin. The screen uses POST /integration/oauth2/complete-login, which your integration doesn't call.
2Receive the code and validate the state
When the merchant authorizes, Chytapay redirects their browser to your redirect_uri:
https://yourapp.com/oauth/callback?code=<code>&state=<the state you sent>Validating the state stops an attacker from linking your user to someone else's merchant account.
- Compare the
statein the query, which Chytapay returns unchanged, with the one in the session. If it's missing or doesn't match, stop the flow. - Delete the
statefrom the session: it's valid for one attempt. - Exchange the
codefor tokens: it lasts 10 minutes and is single use. If it expires, the merchant authorizes again.
3Exchange the code for tokens
In your backend, send POST /integration/oauth2/token a JSON body with grantType: "authorization_code", the code, your clientId and clientSecret, and the same redirectUri from /authorize, identical:
cURL
curl -X POST https://auth-api.test.chytapay.com.ar/integration/oauth2/token \
-H "Content-Type: application/json" \
-d '{
"grantType": "authorization_code",
"code": "'"$CODE"'",
"clientId": "'"$CHYTAPAY_CLIENT_ID"'",
"clientSecret": "'"$CHYTAPAY_CLIENT_SECRET"'",
"redirectUri": "https://yourapp.com/oauth/callback"
}'Response
{
"idToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIn0...",
"tokenType": "Bearer",
"expiresIn": 3600
}With this response the merchant is linked. Store its refreshToken: it lets you keep operating without a new authorization. If the merchant authorizes again, you get new tokens.
Note: In Sandbox, you can skip OAuth with a test merchant account. It doesn't exist in Production.
4Confirm which merchant authorized
With the idToken you just obtained, call GET /me on the Integration API to find out which merchant account it belongs to, and store its merchantUserId next to the link in your system:
cURL
curl https://integration-api.test.chytapay.com.ar/me \
-H "Authorization: Bearer $ID_TOKEN"Response
{
"merchantUserId": "3f8a1c52-7d4e-4b19-9a60-2c5e8d1f0b47",
"email": "[email protected]",
"fantasyName": "Papelería del Sur",
"accountHolderName": "Del Sur Papelería S.R.L.",
"accountHolderTaxId": "30123456781",
"integration": { "name": "My integration" },
"tenantKey": "sede-centro",
"linkedAt": "2026-05-20T14:30:00.000Z"
}| Field | What it is |
|---|---|
merchant |
The merchant account's identifier in Chytapay. It is stable: store it to recognize the account. |
email |
The merchant account's email. |
fantasy |
The merchant's trade name. |
account |
The account holder: the legal name for a legal entity, or first and last name for an individual. |
account |
The holder's CUIT or CUIL, without hyphens. |
integration. |
Your integration's name, the one the merchant sees when authorizing. |
tenant |
The key of the site the account is linked to. Absent if the account isn't linked to a site. |
linkedAt |
The date, in ISO 8601 UTC, of the first authorization. If the merchant unlinks and authorizes again, it doesn't change. |
The identity comes from the idToken: GET /me takes no parameters and ignores any you send. A merchant account that isn't linked or was unlinked gets 403.
Authenticate your API calls
Every Integration API request carries the idToken in Authorization:
curl https://integration-api.test.chytapay.com.ar/payment-request/invoice-1001 \
-H "Authorization: Bearer $ID_TOKEN"Its aud claim is your clientId and its sub is the merchant account: that's how Chytapay knows on whose behalf you operate. /token returns no access token: you use the idToken. The integration admin token gets 401, and an unlinked merchant account gets 403.
| Token | Lasts | How to replace it |
|---|---|---|
code |
10 minutes, single use | The merchant authorizes again |
id |
1 hour (expiresIn: 3600) |
POST / |
refresh |
180 days | The merchant authorizes again |
Before the idToken expires, or on a 401, request a new one from POST /integration/oauth2/refresh with grantType: "refresh_token", the merchant's stored refreshToken, your clientId and your clientSecret. The merchant isn't involved. The response carries a new idToken and no refreshToken: the refresh token doesn't rotate, keep using the same one until you revoke it or it expires.
Revoke a refresh token
When you stop operating for a merchant, revoke its refresh token with POST /integration/oauth2/revoke:
curl -X POST https://auth-api.test.chytapay.com.ar/integration/oauth2/revoke \
-H "Content-Type: application/json" \
-d '{
"token": "'"$REFRESH_TOKEN"'",
"tokenTypeHint": "refresh_token",
"clientId": "'"$CHYTAPAY_CLIENT_ID"'",
"clientSecret": "'"$CHYTAPAY_CLIENT_SECRET"'"
}'It returns 200 with no body. tokenTypeHint is optional and its value is refresh_token. Revoking doesn't unlink: the merchant can authorize again. To end the link, unlink the merchant.
Unlink a merchant
In the Portal, under Cuentas de comercio (Merchant accounts), use Desvincular (Unlink) on the merchant's row. From then on, its tokens get 403 from the Integration API until the merchant authorizes again. If the account was already unlinked, the Portal tells you it isn't linked.
Warning: Unlinking doesn't cancel pending charges. They can still be paid and the money reaches the merchant, but your integration stops seeing them. If you don't want them paid, cancel them first with POST /payment-request/{referenceId}/cancel.
Show your logo on the authorization screen
Without a logo, the merchant sees only your integration's name. Upload it in the Portal under Configuración (Settings), Logo section: the Portal scales it to 512 px on its longest side.
It accepts PNG, JPEG or WEBP up to 5 MB, and rejects SVG. Each upload replaces the previous one.
The redirect_uri isn't the webhook URL
redirect_ |
Webhook URL | |
|---|---|---|
| Purpose | Receive the code when a merchant authorizes |
Receive payment events |
| Who calls it | The merchant's browser, with a GET | Chytapay's backend, with a POST |
| When | Once per authorization | Every time something happens to a charge |
Configure the webhook URL in Webhooks.
Errors
Errors carry code, message and id, which you use to trace the case. Other API errors are in Operations and errors.
See the OAuth errors
| Where | HTTP | code |
Cause | What to do |
|---|---|---|---|---|
/ |
400 | Invalid |
client_, redirect_ or response_ missing, or the redirect_ isn't identical (there's no redirect_). |
Compare protocol, domain, path and trailing slash. |
/ |
401 | Invalid |
client_ from another environment. |
Use the one in Credenciales for the same environment. |
| Authorization screen | 400 | Invalid |
"No encontramos una cuenta con ese email." ("We couldn't find an account with that email."): the admin or an account from another environment signed in. | Have the merchant account authorize. |
| Authorization screen | 401 | Invalid |
Wrong password. | Try again. |
/ |
400 | Invalid |
code expired, used or from another clientId, or a different redirect. |
Authorize again with the same redirect_. |
/, /, / |
401 | Invalid |
Wrong clientId or client. |
Check Credenciales. |
/ |
500 | Integration |
The link wasn't completed; the code is already consumed. |
Restart from /. |
/ |
400 | Invalid |
Refresh token revoked or invalid. | Authorize again. |
| Integration API | 401 | Token missing, expired or not a merchant account's. | Refresh the id. |
|
| Integration API | 403 | Merchant account not linked or unlinked. | Authorize. | |
GET / |
403 | The merchant account isn't linked to your integration. The body is plain text, not JSON. | Authorize. |