OAuth 2.0 Integration Guide

How an operator grants your app access to their Monta data, and how to obtain and use tokens.

Use this guide to implement the OAuth handshake that lets Monta operators grant your Partner app access to their data.

📘

Who this is for

Engineers at a partner company integrating with the Monta Partner API on behalf of Monta operators. If you authenticate with a client ID and secret issued directly to you (Basic auth), you are on the legacy credential flow and this guide does not apply.

📘

Read this after the sandbox guide

This page is the reference for the OAuth handshake itself. Build and test everything else—endpoints, webhooks, and the install lifecycle—in the sandbox first, then certify the handshake against production as described in Going live. The handshake is not available in sandbox by design: it is certified against the endpoints you actually ship with.

Use a Partner app when

  • You build an integration that many Monta operators can install.
  • You need each operator to explicitly grant your app scoped access to their data.
  • You plan to distribute the app publicly in the marketplace or to a targeted operator allowlist.

The moving parts

TermMeaning
Partner organisationYour company, registered and approved by Monta.
AppYour integration's catalogue entry. It owns your OAuth client, requested scopes, and webhook URL.
OperatorA Monta customer (a charge-point operator) who decides whether to install your app.
InstallOne operator's grant of your app. It is created at consent and revoked on uninstall.
ConsumerThe API credential Monta mints per install. Your token resolves to it; you never handle it directly.

One app can have many installs. Your client credentials are per-app and long-lived, while access to an operator's data is per-install and that operator can revoke it without affecting other installs.

Onboarding

  1. Submit your app with its name, description, requested scopes, redirect URIs, and webhook URL.
  2. Wait for Monta to review and approve the app.
  3. Open the one-time reveal link sent to your registered contact address and store the credentials.

The reveal link shows these values once:

  • client_id
  • client_secret
  • webhook_secret — the HMAC key for verifying webhook deliveries
🚧

Store all three values immediately

Monta stores the client secret only as a hash and cannot show it again. If you lose it, Monta must re-issue the client. Re-issuing rotates both client_id and client_secret and invalidates the previous pair.

Your client is registered as a confidential client:

PropertyValue
grant_typesauthorization_code, refresh_token
response_typescode
token_endpoint_auth_methodclient_secret_post
scopeExactly the scopes your app requested.
redirect_urisExactly the URIs you registered—exact string match, with no wildcards or path suffixes.

Start the authorization flow

The operator starts the flow from Monta Hub. They find your app in the catalogue, select Connect, and Monta builds the authorization request before redirecting their browser to Monta's identity provider.

Do not construct the authorization URL or add a “Connect with Monta” button in your product. Your only inbound surface is your registered redirect_uri.

📘

Why this flow does not use PKCE

Monta initiates the authorization request and you redeem the code, so no single party holds both halves of a PKCE challenge. Confidential clients authenticate with client_secret_post and do not send code_challenge. Public clients must use PKCE and are not eligible for this flow.

End-to-end sequence

When the operator approves access, Monta creates the install and its consumer before redirecting to your app. Monta also binds the install, operator, and consumer identifiers to the token session, allowing the Partner API to resolve the token to the correct operator.

Redeem the code

Redeem the authorization code immediately in your callback handler:

curl -X POST {issuer}/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=authorization_code \
  -d code=ory_ac_... \
  -d redirect_uri=https://your.app/callback \
  -d client_id=<your_client_id> \
  -d client_secret=<your_client_secret>

A successful response includes an access token and its granted scopes:

{
  "access_token": "ory_at_...",
  "refresh_token": "ory_rt_...",
  "token_type": "bearer",
  "expires_in": 3599,
  "scope": "charge-points:read charge-transactions:read offline_access"
}
ItemLifetime
Authorization code1 minute, single use
Access token1 hour
Refresh token30 days, rotating—each refresh returns a new token, so always persist the latest value.
Login / consent request30 minutes
🚧

Redeem the code in the callback handler

The authorization code is valid for one minute and can only be used once. Do not send it to a background queue. The redirect_uri in the token request must exactly match the URI used in the authorization request.

Refresh the token

Refresh before the access token's one-hour expiry:

curl -X POST {issuer}/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=refresh_token \
  -d refresh_token=ory_rt_... \
  -d client_id=<your_client_id> \
  -d client_secret=<your_client_secret>

Refresh tokens rotate: each response contains a new refresh_token and invalidates the previous one. Always persist the newest value. Replaying an already-used refresh token revokes the whole chain, and the operator must then re-install.

offline_access is added by Monta rather than requested by your app. It appears on the operator's consent screen and in your token response's scope, and it is what allows Monta to issue a refresh token. On its own it grants no access to data.

Call the Partner API

curl {partnerApiBaseUrl}/charge-points?perPage=50 \
  -H "Authorization: Bearer ory_at_..."
  • Use /api/v1 as the base path. See the API Reference for available endpoints.
  • Treat access tokens as opaque bearer strings. They have an ory_at_ prefix, but are not JWTs and must not be decoded or validated locally.
  • Monta introspects each token, resolves it to the operator's consumer, and enforces consumer status, re-authorization requirements, and granted scopes.

Scope format: resource or resource:permission, where permission is read, write, or delete. For example: charge-points:read and teams:write. A bare resource is read-only. Request resource:write or resource:delete explicitly if you need more. Request the narrowest scopes you need because the operator sees the exact list during consent.

Rate limits: Each install has its own budget of 1,000 requests per 60 seconds by default. Traffic from one operator cannot exhaust another operator's limit.

Handle scope changes

If you later request additional scopes, existing installs are not upgraded automatically. Monta flags each affected install, and API calls return HTTP 403 with NEEDS_REAUTH:

{
  "errorCode": "NEEDS_REAUTH",
  "message": "Re-authorisation required for install 41 after a scope change",
  "context": {
    "install_id": 41,
    "reauth_url": "https://…/41/reauth"
  }
}

Handle NEEDS_REAUTH separately from other 403 responses: stop calling the API for that install, show the re-authorization state in your product, and send the operator to reauth_url. After the operator grants consent again, your existing tokens work with the widened scopes. Narrowing scopes does not trigger re-authorization.

Verify webhooks

EventMeaning
install.createdAn operator installed your app. It carries install_id, operator_id, and slug. Your credentials come from the reveal link.
install.revokedThe operator uninstalled your app. Stop using tokens for that install.
install.config.updatedThe operator changed your app's per-install configuration.

Verify every delivery before parsing its body:

  • Read X-Monta-Signature: sha256=<hex>.
  • Calculate HMAC-SHA256 over the raw request body with your webhook_secret.
  • Compare signatures in constant time.
  • Use the signed delivered_at epoch timestamp to reject stale or replayed deliveries.
import hashlib
import hmac

expected = "sha256=" + hmac.new(
    secret.encode(), raw_body, hashlib.sha256
).hexdigest()

if not hmac.compare_digest(expected, request.headers["X-Monta-Signature"]):
    abort(401)

Your endpoint must use HTTPS, be publicly resolvable, and return a 2xx response within 10 seconds.

🚧

Webhook delivery is best-effort

Monta does not automatically retry a timeout, non-2xx response, or transport error. Monta can replay deliveries for your app on request. Treat webhooks as an accelerator and reconcile authoritative state from the API; do not make install.created your only source of credentials because you already receive them through the reveal link.

Handle revocation and uninstall

  • An operator can uninstall at any time. Monta revokes the install, disables its consumer, and sends install.revoked.
  • Existing access tokens for that install stop working.
  • Your app-level client_id and client_secret remain valid, and other operators' installs continue to work.
  • Only one active install can exist for an app and operator. A re-install creates a new install with a new identifier.

Security summary

ConcernHow it is handled
Client authenticationclient_secret_post; Monta stores the secret only as a hash.
Token formatOpaque and introspected server-side, enabling immediate revocation without key distribution.
Redirect URIsExact-match allowlist; a removed URI stops being accepted immediately.
ConsentPer operator and displays the exact scope list; widening scopes forces re-consent.
Webhook authenticityHMAC-SHA256 over the raw body with a per-app secret and a signed timestamp.
Blast radiusCredentials are per-app; data access is per-install and independently revocable.

Endpoints

The OAuth handshake exists in production only. See the sandbox guidance at the top of this page before you begin certification.

PurposeProduction
Issuer / OAuth baseOn Request
Authorize{issuer}/oauth2/auth (Monta builds this)
Token{issuer}/oauth2/token
Discovery{issuer}/.well-known/openid-configuration
Partner APIhttps://partner-api.monta.com/api/v1

For sandbox endpoints and the credential exchange used there, see Building in the sandbox.

Troubleshooting

SymptomCause and fix
invalid_client — “The requested OAuth 2.0 Client does not exist”Your client is not registered with the identity provider or was removed. Contact Monta to re-issue it; this rotates your secret.
invalid_grant during token exchangeThe code expired, was already used, or the redirect_uri differs from the authorization request.
401 on API callsThe access token expired or the install was revoked. Refresh the token; if refresh fails, the install was revoked and the operator must re-install.
403 with NEEDS_REAUTHYour app's scopes widened. Follow the re-authorization flow described in Handle scope changes.
No install.created receivedDelivery is best-effort. Confirm that your HTTPS endpoint is reachable and returns 2xx within 10 seconds, then ask Monta to replay the event.

Monta performs webhook replays, credential re-issues, and operator-allowlist expansions for you. Contact SUPPORT_CHANNEL_TO_FILL to request these operations.

Integration checklist

  • Store the client credentials and webhook secret from the reveal link.
  • Register the deployed HTTPS redirect_uri exactly.
  • Redeem the authorization code immediately within the one-minute window.
  • Handle refresh-token rotation and always persist the newest refresh token.
  • Treat access tokens as opaque strings.
  • Verify each webhook signature in constant time before parsing its body, and check delivered_at.
  • Return 2xx from the webhook endpoint in under 10 seconds.
  • Handle 403 NEEDS_REAUTH separately from other 403 errors.
  • Stop all use of an install after install.revoked.
  • Respect the 1,000 requests per 60 seconds, per-install limit and apply backoff.
  • Reconcile state without depending solely on webhook delivery.


Did this page help you?