Building an app (OAuth)
An app is an integration many companies or merchants can install, each approving what it may do. Your own store connecting to your own account does not need one: use an API token.
Apps use the OAuth 2.1 authorization code flow with PKCE. There is no SDK to learn; every step is a plain HTTPS request.
1. Open a developer account and register an app
Sign in to the dashboard, accept the developer terms and register your app. You choose:
- Name, description, homepage, privacy and support URLs. People see them when they approve.
- Redirect URIs (1 to 10). Exact matches only:
https://…, orhttp://localhost…while you build. A redirect that is not on the list is refused. - Scopes the app may ask for (see Authentication & scopes).
- Confidential or public. A server app is confidential: it gets a client secret
(
aps_…) shown once, which you can rotate. A mobile or single-page app is public: it has no secret and relies on PKCE alone.
You get a client id (app_…). It is public.
The same is available over the API under /v1/developer/… for signed-in people (not tokens).
2. Send the person to approve
Open the user's browser at the web app's consent page, with a PKCE challenge and a state you
check when they come back:
https://cloud.antivio.app/oauth/authorize
?response_type=code
&client_id=app_Ab3xK9mQ2wL7pZ4nR8tY1vC5
&redirect_uri=https%3A%2F%2Facme.example%2Foauth%2Fcallback
&scope=shipments%3Aread%20shipments%3Awrite
&state=<random string you keep>
&code_challenge=<base64url(sha256(code_verifier))>
&code_challenge_method=S256
Make a fresh code_verifier (43 to 128 characters of A-Z a-z 0-9 - . _ ~) for every attempt
and keep it on your server (or in the session). state and PKCE are both required.
The person signs in, picks the company (or merchant account) and approves the scopes. Only people who can manage integrations there can install an app, and they can only give scopes their own role holds. Antivio then sends the browser to your redirect URI:
https://acme.example/oauth/callback?code=atc_…&state=<your state>
If they decline, the redirect carries error=access_denied and your state. Check state
before you use the code. The code works once and for 60 seconds.
3. Exchange the code for tokens
curl -X POST https://api.antivio.app/oauth/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=authorization_code \
-d code="$CODE" \
-d redirect_uri="https://acme.example/oauth/callback" \
-d code_verifier="$CODE_VERIFIER"
Confidential apps authenticate with HTTP Basic (above) or client_id and client_secret in the
form body; public apps send only client_id.
{
"access_token": "atk_live_…",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "atr_…",
"scope": "shipments:read shipments:write"
}
The access token is the same bearer as an API token: send it as Authorization: Bearer … to the
public API. It acts as the person who approved,
limited to the granted scopes, so it stops working if they leave the company. It lasts an hour.
4. Refresh
curl -X POST https://api.antivio.app/oauth/token -u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=refresh_token -d refresh_token="$REFRESH_TOKEN"
Every refresh returns a new refresh token and spends the old one. Store the new one before you use the new access token. Refresh tokens last 30 days.
If a refresh token or a code is used twice, Antivio assumes it leaked and revokes every token of that installation; the person has to approve again. Do not retry a refresh in parallel.
5. Revoke
curl -X POST https://api.antivio.app/oauth/revoke -u "$CLIENT_ID:$CLIENT_SECRET" \
-d token="$REFRESH_TOKEN"
Revoking an access token ends only that token; revoking a refresh token ends all of the
installation's tokens. The answer is 200 even for a token we do not know.
6. Receive webhooks
With webhooks:write, your access token adds webhook endpoints like any API
token (POST /v1/webhook-endpoints). They belong to the installation:
- your token sees and changes only the endpoints it added (
installation_idshows which), never the company's own; they count toward a limit of 5 per installation; - the company sees them in its Developers settings and can remove them;
- uninstalling your app deletes them, and while Antivio suspends your app they receive nothing.
7. App events and privacy requests
Set your app's events URL in the developer portal (or PUT /v1/developer/apps/{id}/events
with {"url": "https://…"}). Antivio sends there, for every company that installed your app,
messages signed with your app's events secret (whsec_…, shown in the portal) the same way as
webhooks: verify webhook-signature with the same code, answer 2xx, use
webhook-id to ignore repeats. Failed messages are retried for about three days.
| Type | When | data |
|---|---|---|
app.uninstalled | the company removed your app; its tokens and webhook endpoints are gone | installation_id, customer_account_id, uninstalled_at |
customer.data_request | a recipient asked the company for the data held about them | privacy_request_id, installation_id, customer_account_id, recipient (email, phone), shipment_ids, requested_at |
customer.redact | a recipient asked to be erased | the same as customer.data_request |
- The token answer carries
installation_id,tenant_idandcustomer_account_id: store them to match these messages to your records. The envelope'stenant_idis the company. - On
customer.data_request, send the company what your app stores about that person; oncustomer.redact, delete it (keep only what the law requires you to).shipment_idsare the shipments of that recipient your installation can reach (at most 500). - Rotate the secret in the portal: the previous one keeps signing for 24 hours. The portal also lists the latest messages and their status.
8. Publish in the App Store
A private app is installed from a link you share, with no review. To let any company find and install it, submit it for review; once Antivio approves it, it is listed in the App Store inside every company's dashboard.
Before you submit, the app needs a privacy policy URL, a support URL and an events URL
(it must receive app.uninstalled and privacy requests). Then:
curl -X POST https://api.antivio.app/v1/developer/apps/$APP_ID/submissions \
-H "Authorization: Bearer $USER_TOKEN" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" -d '{
"listing": {
"tagline": "Keep your store'"'"'s orders and shipments in sync",
"description_en": "Sends every order to Antivio and brings the tracking back.",
"description_es": "Envía cada pedido a Antivio y trae el seguimiento.",
"category": "ecommerce",
"app_url": "https://acme.example/install",
"scope_justifications": [
{ "scope": "shipments:read", "justification": "Shows each order'"'"'s delivery status to the buyer." }
]
},
"note": "First version."
}'
app_urlis where installing starts: your own page that begins the OAuth flow described above. The store's Install button opens it. Antivio never sends an authorization code to an app that did not ask for one.- Every scope needs a justification (20-300 characters) in your own words. Companies see them before they install.
- A submission is a snapshot: the listing and the app's name, description, URLs, redirect URIs,
scopes and events URL as submitted. Send
"app": {...}and"events_url"to propose new values. - Automated checks run when you submit and the reviewer sees the results: your privacy,
support and
app_urlpages answer; your events URL rejects a message with a wrong signature (answer4xx) and accepts a correctly signedapp.uninstalledtest message (answer2xx,"test": true, no real company). A failing check does not reject the app by itself, but a reviewer will ask you to fix it. Results are inchecksof each submission. - States:
submitted→in_review→approved|rejected|changes_requested; you canwithdrawwhile it waits.GET /v1/developer/apps/{id}/submissionsshows the status and the reviewer's note. After 3 rejections in 90 days new submissions wait 30 days. - Once published, an app's reviewed fields are locked (
409 app.published_requires_review): to change its scopes, redirect URIs, URLs or events URL, submit an update. Until it is approved, the live app and the store keep what was approved. Companies that already installed it keep the scopes they granted. POST /v1/developer/apps/{id}/unpublishtakes it out of the store and makes it private again; companies that installed it keep it. Antivio can also delist or suspend an app that breaks the rules; a suspended app's tokens, events and webhooks stop at once.
Errors
The token endpoint answers with RFC 6749 errors, not problem details:
error | Status | Meaning |
|---|---|---|
invalid_request | 400 | A parameter is missing or malformed (code_verifier shape, form body) |
invalid_client | 401 | Unknown, suspended or wrongly authenticated app |
invalid_grant | 400 | The code or refresh token is unknown, used, expired or for another app or redirect URI; or the app was uninstalled |
unsupported_grant_type | 400 | Only authorization_code and refresh_token exist. There is no password, implicit or client-credentials grant |
Server metadata
GET /.well-known/oauth-authorization-server (RFC 8414)
lists the endpoints, S256, and the supported scopes.
Staying safe
- Keep the client secret on your server. Rotate it from the dashboard if it leaks: the old one stops working at once.
- Only
S256PKCE is accepted. Always checkstate. - Treat a refresh failure with
invalid_grantas "the person must approve again".