User authorization
Act on behalf of a person with OAuth 2.0 Authorization Code and PKCE.
Use this flow when your application acts on behalf of a person: they sign in
to Ecomiq and authorize the permissions on the application's store. If a server
acts as the application, use client_credentials from
Authentication.
/ connect/ authorizeredirect_uri with codePOST / connect/ token with code_verifieraccess_token and refresh_tokenBearerPOST / connect/ token with refresh_tokenRequirements
- Create the application in the dashboard with the With sign-in connection and its redirect URLs. They are matched exactly.
- The application can already renew access: request the
offline_accessscope to receive a refresh token. - A dashboard application can only be authorized on the store where it was created.
- Mobile and browser apps check No server: they are public clients and have
no
client_secret.
1. Generate the PKCE
PKCE is required and only S256 is accepted. Store verifier and state in
the user's session.
import { createHash, randomBytes } from "node:crypto";
export const verifier = randomBytes(32).toString("base64url");
export const challenge = createHash("sha256").update(verifier).digest("base64url");
export const state = randomBytes(16).toString("base64url");2. Redirect to authorize
https://auth.ecomiq.pe/connect/authorize
?response_type=code
&client_id=<client-id>
&redirect_uri=https://your-app.com/oauth/callback
&scope=api_admin%20offline_access
&state=<state>
&code_challenge=<challenge>
&code_challenge_method=S256| Parameter | Use |
|---|---|
response_type | Always code. |
redirect_uri | One of the registered URLs, identical. |
scope | api_admin for the Admin API; offline_access for a refresh token. |
state | A random value; compare it when they return. |
code_challenge_method | Only S256. |
prompt | login forces a new sign-in. |
The person signs in, picks the store, and sees your application's permissions.
When they accept, they return to your redirect_uri with code and state.
3. Exchange the code
The code works once and expires after 5 minutes.
curl -X POST https://auth.ecomiq.pe/connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=<code>" \
-d "redirect_uri=https://your-app.com/oauth/callback" \
-d "code_verifier=$VERIFIER" \
-d "client_id=$ECOMIQ_CLIENT_ID" \
-d "client_secret=$ECOMIQ_CLIENT_SECRET"{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "CfDJ8...",
"scope": "api_admin offline_access"
}A public client omits client_secret.
4. Call the Admin API
Send the token in Authorization: Bearer. The store comes from the token; if
you send x-Store-Id, it must match or you get a 403. The permissions are
your application's permissions that the person still has in that store.
5. Renew the token
The access token lasts 1 hour and the refresh token 14 days. Every renewal returns a new refresh token: always keep the latest one.
curl -X POST https://auth.ecomiq.pe/connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token=$REFRESH_TOKEN" \
-d "client_id=$ECOMIQ_CLIENT_ID" \
-d "client_secret=$ECOMIQ_CLIENT_SECRET"Renewal fails with invalid_grant if the person changed their password, lost
access to the store, or the application was deleted. Go back to step 2.
Full example
A dependency-free Node.js server: / redirects to Ecomiq and
/ checks state and exchanges the code.
import { createServer } from "node:http";
import { createHash, randomBytes } from "node:crypto";
const AUTH = "https://auth.ecomiq.pe";
const REDIRECT_URI = "https://your-app.com/oauth/callback";
const pending = new Map();
createServer(async (req, res) => {
const url = new URL(req.url, REDIRECT_URI);
if (url.pathname === "/login") {
const verifier = randomBytes(32).toString("base64url");
const state = randomBytes(16).toString("base64url");
pending.set(state, verifier);
const authorize = new URL("/connect/authorize", AUTH);
authorize.search = new URLSearchParams({
response_type: "code",
client_id: process.env.ECOMIQ_CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: "api_admin offline_access",
state,
code_challenge: createHash("sha256").update(verifier).digest("base64url"),
code_challenge_method: "S256",
}).toString();
res.writeHead(302, { Location: authorize.toString() }).end();
return;
}
if (url.pathname === "/oauth/callback") {
const state = url.searchParams.get("state");
const verifier = pending.get(state);
pending.delete(state);
if (!verifier || url.searchParams.has("error")) {
res.writeHead(400).end(url.searchParams.get("error") ?? "invalid_state");
return;
}
const response = await fetch(`${AUTH}/connect/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code: url.searchParams.get("code"),
redirect_uri: REDIRECT_URI,
code_verifier: verifier,
client_id: process.env.ECOMIQ_CLIENT_ID,
client_secret: process.env.ECOMIQ_CLIENT_SECRET,
}),
});
const tokens = await response.json();
// Save tokens.access_token and tokens.refresh_token in your database.
res.writeHead(200).end("Store connected.");
return;
}
res.writeHead(404).end();
}).listen(3000);Errors
| Where | error | Cause |
|---|---|---|
| authorize | access_denied | The person declined or can't authorize on that store. |
| authorize | invalid_scope | You requested a scope the application doesn't have. |
| authorize | invalid_request | A parameter, the redirect URL, or the store is not valid. |
| token | invalid_grant | Code expired or reused, wrong code_verifier, or access revoked. |
Last updated: