Installation callback (url_callback)
This guide details what happens between POST /app/authorize and the merchant credentials generation in POST /app/client/generate, focusing on the url_callback parameter and the token delivered to it. Use this page when you need to:
- Understand the exact format of the URL Appmax calls at the end of the authorization.
- Point the callback to a dedicated microservice (onboarding, provisioning, credentials) instead of your dashboard front-end.
For the end-to-end flow, see App installation.
Flow overview
The url_callback receives the token directly as a query string, in the merchant's browser redirect. There is no additional authenticated server-to-server handshake before the callback — which is exactly why this endpoint can live in a microservice independent from the integrator's dashboard.
Request parameters for POST /app/authorize
This endpoint requires an access token issued for the app credentials (see Authentication).
curl --location 'https://api.appmax.com.br/app/authorize' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer APP_ACCESS_TOKEN' \
--data '{
"app_id": "8f2c1d3e-5a4b-4c7d-9e1f-2a3b4c5d6e7f",
"external_key": "store_42",
"url_callback": "https://onboarding.myapp.com/appmax/callback",
"domain_names": ["mystore.com"]
}'app_idstringobrigatorioApp UUID (do not use the Numerical ID). E.g., 8f2c1d3e-5a4b-4c7d-9e1f-2a3b4c5d6e7f.
external_keystringobrigatorioKey provided by the partner platform to identify the origin of the installation (e.g., store_id, merchant_id). It is echoed back in the health check.
url_callbackstringobrigatorioAbsolute URL (scheme https://) where the merchant will be redirected after authorization. Appmax appends the token to this URL (see next section).
domain_namesstring[]opcionalList of authorized store domains. Use when the app operates on multiple domains.
domain_namestringopcionalSingular alternative to domain_names. Kept for backwards compatibility.
Required if you're going to use Apple Pay
Neither is required for the installation itself, but one of them is essential if the store will process Apple Pay payments — it's from the domain informed here that Appmax registers the domain with Apple. See Domain configuration for Apple Pay.
Response:
{
"data": {
"token": "12083w36219d223f33ecf48f2a7f5ccf143b0bc554"
}
}The token returned here is an opaque single-use hash, valid for 1 hour (cache TTL). You use it both in the redirect and later in POST /app/client/generate.
Token format
The token is not a JWT. It is an opaque identifier (SHA1) that references the installation data kept in Appmax's cache. Do not try to decode it — just pass the value through as received.
Redirect to authorization
Once you have the hash, redirect the merchant to Appmax's authorization URL:
| Environment | Redirect URL |
|---|---|
| Sandbox | https://breakingcode.sandboxappmax.com.br/appstore/integration/HASH |
| Production | https://admin.appmax.com.br/appstore/integration/HASH |
Replace HASH with the token returned by /app/authorize. The merchant signs in, reviews the permissions, and confirms the installation on the Appmax panel.
Callback format
Once the merchant authorizes the installation, Appmax issues an HTTP 302 from the merchant's browser to your url_callback, appending the token as a query string.
Parameter name
The parameter is always called token. It is not code, access_token, or authorization_code.
Concatenation rule
Appmax concatenates the token honoring any existing query string:
- If
url_callbackhas no query string:<url_callback>?token=<hash> - If
url_callbackalready has a query string:<url_callback>&token=<hash>
Examples
url_callback sent on /app/authorize:
https://onboarding.myapp.com/appmax/callbackURL the merchant hits:
https://onboarding.myapp.com/appmax/callback?token=12083w36219d223f33ecf48f2a7f5ccf143b0bc554url_callback with your own parameters (useful to carry context/state):
https://onboarding.myapp.com/appmax/callback?merchant_ref=42&state=xyzURL the merchant hits:
https://onboarding.myapp.com/appmax/callback?merchant_ref=42&state=xyz&token=12083w36219d223f33ecf48f2a7f5ccf143b0bc554Callback method
The callback is a GET issued by the merchant's browser (302 redirect). It is not a POST and has no body. All context data must live in the query string of the url_callback you originally sent.
Exchanging the token for credentials
The token received in the callback is not a credential — it is a short-lived ticket. To obtain the merchant's definitive credentials, your backend must call POST /app/client/generate using the app access token (not the merchant's, which doesn't exist yet).
curl --location 'https://api.appmax.com.br/app/client/generate' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer APP_ACCESS_TOKEN' \
--data '{
"token": "12083w36219d223f33ecf48f2a7f5ccf143b0bc554"
}'Response:
{
"data": {
"client": {
"client_id": "MERCHANT_CLIENT_ID",
"client_secret": "MERCHANT_CLIENT_SECRET"
}
}
}While processing this call, Appmax triggers the health check on the validation URL registered in the app panel. If that URL does not respond with HTTP 200 and a valid UUID external_id, credential generation fails.
external_id is different from token
Do not confuse the two values that appear in this flow:
token— arrives in the redirect query string (?token=...), is single-use, and exists only to be exchanged for credentials.external_id— the UUID you return in the health check; stays persisted tied to the store and is used on every CDN call (headerexternal-id) from then on.
Persist the external_id in your database next to the client_id and client_secret. Full reference at external-id.
Single use
The token is consumed on the first successful call to /app/client/generate — Appmax removes it from the cache. Reusing it returns Invalid token. If it fails, start a new /app/authorize.
Callback handler example
Minimal Node.js (Express) handler showing the three responsibilities of the callback: extract the token, swap it for credentials, persist and acknowledge.
import express from 'express'
import axios from 'axios'
const app = express()
const APP_CLIENT_ID = process.env.APPMAX_APP_CLIENT_ID
const APP_CLIENT_SECRET = process.env.APPMAX_APP_CLIENT_SECRET
const AUTH_URL = 'https://auth.appmax.com.br/oauth2/token'
const API_URL = 'https://api.appmax.com.br'
// Gets the APP token (an in-memory cache is recommended in production)
async function getAppAccessToken () {
const body = new URLSearchParams({
grant_type: 'client_credentials',
client_id: APP_CLIENT_ID,
client_secret: APP_CLIENT_SECRET
})
const { data } = await axios.post(AUTH_URL, body)
return data.access_token
}
app.get('/appmax/callback', async (req, res) => {
const { token, merchant_ref } = req.query
if (!token) {
return res.status(400).send('missing token')
}
try {
const accessToken = await getAppAccessToken()
const { data } = await axios.post(
`${API_URL}/app/client/generate`,
{ token },
{ headers: { Authorization: `Bearer ${accessToken}` } }
)
const { client_id, client_secret } = data.data.client
// Persist the merchant credentials bound to your merchant_ref
await saveMerchantCredentials({
merchant_ref,
client_id,
client_secret
})
return res.redirect('/onboarding/done')
} catch (err) {
console.error('appmax callback failed', err.response?.data ?? err.message)
return res.status(502).send('failed to generate merchant credentials')
}
})
app.listen(3000)Equivalent in plain PHP:
<?php
// GET /appmax/callback?token=...&merchant_ref=...
$token = $_GET['token'] ?? null;
$merchantRef = $_GET['merchant_ref'] ?? null;
if (!$token) {
http_response_code(400);
exit('missing token');
}
// 1. Obtain the APP token (application credentials)
$auth = curl_init('https://auth.appmax.com.br/oauth2/token');
curl_setopt_array($auth, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'grant_type' => 'client_credentials',
'client_id' => getenv('APPMAX_APP_CLIENT_ID'),
'client_secret' => getenv('APPMAX_APP_CLIENT_SECRET'),
]),
]);
$appToken = json_decode(curl_exec($auth), true)['access_token'];
curl_close($auth);
// 2. Swap the received hash for the merchant credentials
$generate = curl_init('https://api.appmax.com.br/app/client/generate');
curl_setopt_array($generate, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $appToken,
],
CURLOPT_POSTFIELDS => json_encode(['token' => $token]),
]);
$response = json_decode(curl_exec($generate), true);
curl_close($generate);
$clientId = $response['data']['client']['client_id'] ?? null;
$clientSecret = $response['data']['client']['client_secret'] ?? null;
if (!$clientId || !$clientSecret) {
http_response_code(502);
exit('failed to generate merchant credentials');
}
// 3. Persist the credentials bound to the merchant
saveMerchantCredentials($merchantRef, $clientId, $clientSecret);
header('Location: /onboarding/done');Why this enables microservice-based integrations
Because the token reaches url_callback via a browser redirect, the URL can point to any public HTTP service — not necessarily the same host that kicked off /app/authorize. This unlocks a few common architectures:
- Dedicated onboarding microservice: the integrator's dashboard starts the flow, but the callback points to an isolated service responsible solely for provisioning the merchant (creating a tenant, generating resources, storing credentials). The onboarding service does not need to know about the dashboard internals.
- Serverless function: point
url_callbackat a Lambda/Cloud Function. It swaps the token for credentials and writes to a database or secret vault — no long-running server required. - Domain separation: the dashboard can live on
app.mydomain.comwhile the callback points toonboarding.mydomain.com— each with its own deployment and security posture.
The key point is that there is no authenticated server-to-server handshake between Appmax and your url_callback before the callback arrives. The token itself carries the authorization — whoever has the token and the app credentials can complete the flow. This simplifies the microservice design, which only needs to:
- Expose a public HTTPS endpoint.
- Have access to the app credentials (typically via a secret vault/environment variables).
- Have access to the storage where merchant credentials will be persisted.
Security
- Use HTTPS for
url_callback. Tokens in query strings over HTTP are visible to intermediaries and leak into logs. - Validate context: include your own identifier in the
url_callback(e.g.,?merchant_ref=42) and confirm it matches a legitimate installation attempt on your side before calling/app/client/generate. - Treat the token as a short-lived secret: 1h TTL in Appmax's cache, consumed on first successful exchange. Don't write the token to structured logs without masking.
- Protect the app credentials: the app's
client_id/client_secretlive in the callback microservice. Use a secret manager (AWS Secrets Manager, Vault, Parameter Store) in production. - Idempotency: since the token is single-use, repeated calls to
/app/client/generatewith the same token returnInvalid token. If the merchant refreshes the success page, design your handler to detect the already-provisioned state before trying to exchange the token again. - Mandatory health check: the validation URL registered in the app panel must be publicly reachable and return
HTTP 200with a UUIDexternal_idduring/app/client/generate. Without it, the callback receives the token but the exchange fails. See Health check.
Common errors
| Symptom | Likely cause | Fix |
|---|---|---|
Callback hit without token query param | url_callback already had a fragment (#) or a malformed URL | Send url_callback without a fragment; Appmax only manipulates the query string |
/app/client/generate returns Invalid token | Token already consumed, expired (>1h), or never authorized by the merchant | Start a new /app/authorize and run the flow again |
Callback arrives but the microservice cannot call /app/client/generate | Missing APP token in the microservice (app credentials) | Ensure the microservice has access to the app's client_id/client_secret |
/app/client/generate returns 500 | Health check failed on the validation URL | See Installation troubleshooting |
See also
- App installation — full flow with health check
- Authentication — difference between app and merchant credentials
- Create an app — registration of the validation URL used in the health check