Skip to content
ESENGo to the Portal

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.

  1. Your backend redirects to GET /integration/oauth2/authorize with a random state.
  2. The merchant signs in on Chytapay and authorizes.
  3. The Auth API returns to your redirect_uri with code and state.
  4. Your backend validates the state.
  5. Your backend sends code, clientId and clientSecret to POST /integration/oauth2/token.
  6. The Auth API returns the merchant's idToken and refreshToken.

Before you begin

All from the same environment, Sandbox or Production:

  • The clientId and clientSecret, in the Portal under Credenciales (Credentials). The clientSecret goes only from your backend to the Auth API (/token, /refresh and /revoke).
  • An https redirect_uri registered 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_type Yes code
client_id Yes Your integration's clientId
redirect_uri Yes A registered redirect_uri, 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.

Node
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:

Código
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.

  1. Compare the state in the query, which Chytapay returns unchanged, with the one in the session. If it's missing or doesn't match, stop the flow.
  2. Delete the state from the session: it's valid for one attempt.
  3. Exchange the code for 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
merchantUserId The merchant account's identifier in Chytapay. It is stable: store it to recognize the account.
email The merchant account's email.
fantasyName The merchant's trade name.
accountHolderName The account holder: the legal name for a legal entity, or first and last name for an individual.
accountHolderTaxId The holder's CUIT or CUIL, without hyphens.
integration.name Your integration's name, the one the merchant sees when authorizing.
tenantKey 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
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
idToken 1 hour (expiresIn: 3600) POST /integration/oauth2/refresh
refreshToken 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
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.

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_uri 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
/authorize 400 InvalidRequestError client_id, redirect_uri or response_type missing, or the redirect_uri isn't identical (there's no redirect_uri_mismatch). Compare protocol, domain, path and trailing slash.
/authorize 401 InvalidClientError client_id from another environment. Use the one in Credenciales for the same environment.
Authorization screen 400 InvalidGrantError "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 InvalidCredentialsError Wrong password. Try again.
/token 400 InvalidGrantError code expired, used or from another clientId, or a different redirectUri. Authorize again with the same redirect_uri.
/token, /refresh, /revoke 401 InvalidClientError Wrong clientId or clientSecret. Check Credenciales.
/token 500 IntegrationSetupError The link wasn't completed; the code is already consumed. Restart from /authorize.
/refresh 400 InvalidGrantError Refresh token revoked or invalid. Authorize again.
Integration API 401 Token missing, expired or not a merchant account's. Refresh the idToken.
Integration API 403 Merchant account not linked or unlinked. Authorize.
GET /me 403 The merchant account isn't linked to your integration. The body is plain text, not JSON. Authorize.