Merion API

Authentication

The Merion API uses OpenID Connect with ES256-signed JWTs and PKCE S256. This page walks through the full flow: discovery, authorisation, token exchange, validation, and refresh.

OIDC authorisation code flow with PKCE S256: steps from your app through auth.merion.com.au to api.merion.com.au and back
OIDC authorisation code + PKCE S256 flow — from code challenge generation to authenticated API call.

Overview

Merion uses OpenID Connect (OIDC) Core 1.0 for all authenticated API access. The authorisation server is auth.merion.com.au. Tokens are issued as ES256 JWTs — signed with ECDSA using the P-256 curve and SHA-256. The authorisation code flow requires PKCE with the S256 challenge method; plain challenges are rejected.

Before you start, you need approved credentials — an OIDC client_id issued by Merion. See Requesting access below.

Step 1 — Fetch the discovery document

Your OIDC library should auto-configure from the discovery document rather than hardcoding endpoint URLs. Fetch it once at startup and cache it:

curl -s https://auth.merion.com.au/.well-known/openid-configuration | python3 -m json.tool

Key fields to read from the response:

issuer
The canonical issuer URI. You must validate every incoming JWT's iss claim against this value.
authorization_endpoint
Where to redirect the user to begin the authorisation code flow.
token_endpoint
Where to POST to exchange an authorisation code for tokens, or to refresh an access token.
jwks_uri
The JWKS endpoint — fetch this to get the public keys for verifying JWT signatures.
code_challenge_methods_supported
Will include "S256". Plain is not listed and will be rejected.

Step 2 — Generate a PKCE code verifier and challenge

PKCE (Proof Key for Code Exchange, RFC 7636) prevents authorisation code interception. Generate a cryptographically random code_verifier and derive the code_challenge from it using SHA-256:

# Shell — generate code_verifier (43–128 chars, URL-safe base64, no padding)
CODE_VERIFIER=$(openssl rand -base64 64 | tr -d '=+/' | tr '/+' '_-' | head -c 96)

# Derive code_challenge = BASE64URL(SHA256(code_verifier))
CODE_CHALLENGE=$(echo -n "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr -d '=' | tr '+/' '-_')

In JavaScript/TypeScript (Node 18+ or browser with Web Crypto):

async function generatePKCE() {
  // code_verifier: 96 random URL-safe chars
  const array = new Uint8Array(72);
  crypto.getRandomValues(array);
  const verifier = btoa(String.fromCharCode(...array))
    .replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");

  // code_challenge: S256 = BASE64URL(SHA256(verifier))
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const digest = await crypto.subtle.digest("SHA-256", data);
  const challenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
    .replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");

  return { verifier, challenge };
}

Store the verifier securely (session storage or server-side session). You will need it at step 4. Never expose the verifier in a URL or log.

Step 3 — Redirect to the authorisation endpoint

Redirect the user's browser to the authorization_endpoint from the discovery document with these query parameters:

response_type=code
Request an authorisation code.
client_id=YOUR_CLIENT_ID
Your OIDC client ID issued by Merion.
redirect_uri=https://yourapp.example.com/callback
Must exactly match a URI registered with Merion during onboarding.
scope=openid
Include openid as a minimum. Additional scopes are granted per-client.
code_challenge=CODE_CHALLENGE
The S256 challenge from step 2.
code_challenge_method=S256
Must be S256. Plain is rejected.
state=RANDOM_STATE
A CSRF token — a random string you generate and verify on return.
const params = new URLSearchParams({"
  response_type: "code",
  client_id: "YOUR_CLIENT_ID",
  redirect_uri: "https://yourapp.example.com/callback",
  scope: "openid",
  code_challenge: challenge,
  code_challenge_method: "S256",
  state: csrfToken,
});
window.location.href = `${authorizationEndpoint}?${params}`;

Step 4 — Exchange the authorisation code for tokens

After the user authenticates, auth.merion.com.au redirects back to your redirect_uri with ?code=AUTH_CODE&state=STATE. Verify the state matches what you sent, then POST to the token_endpoint:

curl -s -X POST \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=AUTH_CODE" \
  -d "redirect_uri=https://yourapp.example.com/callback" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "code_verifier=CODE_VERIFIER" \
  "$(TOKEN_ENDPOINT)"

A successful response:

{
  "access_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "rt_…",
  "id_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9…"
}

Step 5 — Validate the access token

Before trusting any JWT, validate:

  1. Fetch the JWKS from jwks_uri in the discovery document. Cache the key set with a sensible TTL (e.g., 24 hours); re-fetch if you encounter an unknown kid.
  2. Verify the signature using the matching public key (kid in the JWT header must match a key in the JWKS).
  3. Check iss equals the issuer from the discovery document exactly.
  4. Check aud includes your client_id.
  5. Check exp is in the future (account for a small clock skew, e.g., ±30 seconds).

Use a battle-tested library for this — do not implement JWT validation from scratch. Any library that supports ES256 and fetches JWKS by URI will work (e.g., jose for Node.js/browser, python-jose for Python, System.IdentityModel.Tokens.Jwt for .NET).

Step 6 — Call authenticated endpoints

Include the access token in every request to a protected endpoint:

curl -s \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  https://api.merion.com.au/me

The API validates the bearer token on every request. A missing or invalid token returns 401 Unauthorized; a valid token with insufficient scope returns 403 Forbidden.

Token refresh

Access tokens are short-lived (the expires_in field is authoritative — do not hardcode a duration). When the access token expires, use the refresh token to obtain a new one without re-authenticating the user:

curl -s -X POST \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=REFRESH_TOKEN" \
  -d "client_id=YOUR_CLIENT_ID" \
  "$(TOKEN_ENDPOINT)"

The response has the same shape as the initial token response. Store the new refresh_token if one is returned — some servers rotate refresh tokens.

CORS policy

The API accepts cross-origin requests from *.merion.com.au origins only. If your integration runs in a browser on a third-party domain, you must proxy API calls through your own server — direct browser requests will be blocked by the browser's CORS preflight. The token exchange should always happen server-side.

Requesting access

There is no self-service sign-up. All integrations are reviewed before credentials are issued:

  1. Email [email protected] with the subject "API Access Request".
  2. Describe your use case, the integration you are building, and your existing relationship with Merion (partner, client, integrator).
  3. The Merion team will respond within two business days and, if your use case is approved, initiate the partner onboarding flow.
  4. On approval you will receive an OIDC client_id and the list of redirect_uri values to register.

See also: Integration Checklist for the full pre-launch sequence, and Errors & Rate Limits for how authentication errors are surfaced.

Get started

Ready to integrate with Merion?

API access is available to approved partners and integrators. Contact us to start the conversation — no commitment required.