Connect an integration to SagaPay
OAuth 2.0 authorization code with PKCE (S256). Public clients only — no client secret. Discovery: https://auth.sagapay.no/.well-known/oauth-authorization-server.
1. Send the merchant to authorize
GET https://auth.sagapay.no/authorize ?response_type=code &client_id=woocommerce &redirect_uri=https%3A%2F%2Fshop.example%2Fwp-admin%2Fadmin.php%3Fpage%3Dwc-settings%26tab%3Dcheckout%26section%3Dsaga_payments &state=<your nonce, 8–512 printable chars> &code_challenge=<base64url(sha256(code_verifier))> &code_challenge_method=S256 &scope=payments &store=<optional: the Saga store id you already use>
Percent-encode every value (the redirect URI carries its own query). The redirect URI must be an https URL on the shop's own origin with wp-admin as a path segment. The merchant signs in (e-mail code or password), picks the store, presses «Gi tilgang», and comes back to redirect_uri&code=…&state=… — or &error=access_denied&state=… if they cancel at any step. A bad client or redirect URI is answered with a page, never a redirect; other faults come back as error=…&error_description=…&state=….
2. Redeem the code, server to server, within 5 minutes, once
POST https://auth.sagapay.no/token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code&code=…&redirect_uri=…&client_id=woocommerce&code_verifier=…
redirect_uri is the same URL you sent to authorize (compared after URL normalisation, so an upper-case host or a default port do not matter). JSON bodies are accepted too. A parameter given twice is invalid_request.
200 {"token_type":"saga_api_key","access_token":"sg_live_…","api_secret":"sgs_…","scope":"payments",
"merchant_id":"…","merchant_name":"…","store_id":"…","store_name":"…","environment":"live",
"webhook":{"id":"…","url":"https://shop.example/wc-api/saga_payments_webhook","secret":"whsec_…","events":[…]},
"webhook_reason":null,"capabilities":{"methods":["card","vipps"],"methodsRaw":[…],"currency":"NOK"},"connected_at":"…"}
400 {"error":"invalid_grant"} — used, expired, unknown, wrong redirect_uri or wrong verifier (one answer for all; a wrong verifier does not burn the code)
429 {"error":"slow_down"} — with Retry-After
Propagation: the key is written to the API's key store at grant time and becomes valid at every edge within about a minute. A 401 INVALID_API_KEY or INVALID_API_SECRET inside the first minute after the token response is that lag, not a wrong secret — retry with backoff (measured 21 Sept 2026: 200 within 2–12 s at most edges, once 401 until ~30 s).
Use access_token as API-KEY and api_secret as API-SECRET against the Saga API, with MERCHANT-ID and — the key is store-scoped — STORE-ID on every call. The prefix decides the host: sg_live_ → live, sg_test_ → sandbox. Overwrite your stored webhook secret on every successful token response; webhook_reason WEBHOOK_UNCHANGED means keep the one you hold; any other reason means there is no webhook. A code that is never redeemed is cleaned up within a few minutes — its key and webhook are revoked.
3. Revoke
POST https://auth.sagapay.no/revoke
Content-Type: application/json
X-Signature: <HMAC-SHA256 hex of the raw body, keyed with api_secret>
{"token":"sg_live_…"}
200 {"revoked":true,"already_revoked":false,"webhook_removed":true}
401 {"error":"invalid_client"} — unknown token or wrong signature
The signature is the proof of the client (metadata: revocation_endpoint_auth_methods_supported: ["saga_hmac_sha256"]). Reconnecting the same shop and store returns the same key while it is whole; the webhook keeps its id and gets a new secret every time; a webhook shared by two stores of the same shop is removed only when the last one revokes.