Authentication

Product APIs take an OAuth 2 access token as a bearer token. You get access tokens from the token endpoint with the refresh-token grant (RFC 6749 section 6). This page is the whole of it: the request, the response, every error, how long things last, and how to store what we give you.

The parts

Backup developers

An integration has one owner: whoever created it in the developer portal. You can appoint colleagues as backup developers so the integration is not a single point of failure — from the integration's detail screen, add them by the email address they sign in to the portal with. They must already have signed in once: this never creates an account for them.

A backup developer can do everything the owner can — view and edit the integration, generate, rotate and revoke its refresh token, and see which brokerages have granted it access — except delete the integration or change who else is a backup developer. Those two stay the owner's own levers, so a brokerage's worth of colleagues can never lock the owner out of an integration they created.

Getting a grant

A refresh token lets your integration authenticate; it does nothing for a brokerage until that brokerage grants it access. There is no API call for this — a person at the brokerage decides it, on our consent screen, the same way Atlassian, Google and Microsoft ask.

Send the brokerage's user to:

https://suredocs.ie/authorize?client_id=YOUR_CLIENT_ID&broker_id=THEIR_BROKER_ID&scope=upload_file&state=YOUR_STATE&redirect_uri=YOUR_REDIRECT_URI
Parameter Meaning
client_id Your integration id.
broker_id The brokerage you are asking. You supply this — often from your own onboarding flow — we do not have a directory for you to browse. By default it is their domain name, to guarantee uniqueness.
scope The scopes you are asking for, separated by spaces. Ask for only what you need: a grant carries exactly the scopes shown on the screen.
state Opaque to us; sent back on the redirect unchanged, for you to correlate the answer with the request that sent it.
redirect_uri Where we send the user back. Must be one you registered when we set your integration up, matched exactly: no prefix, no wildcard. Ask us to register a new one.

We reply by redirecting to your redirect_uri with two query parameters, state (yours, unchanged) and result (granted or denied). There is no code to exchange: the grant itself is the outcome, and the next call to the token endpoint (above) is how you find out it is live. A denied or abandoned request looks the same to you as one nobody ever opened — we do not tell you which.

/authorise (the Irish spelling) works identically, if you would rather use it.

Exchange a refresh token

POST https://api.suredocs.ie/oauth/token

Send a form (application/x-www-form-urlencoded, the standard) or JSON.

Parameter Required Meaning
grant_type Yes Always refresh_token.
client_id Yes Your integration id.
refresh_token Yes The refresh token: the last one you received.
broker_id Yes The brokerage the access token is for.
scope No Scopes wanted, separated by spaces. Without it, you get every scope your refresh token and the brokerage's grant share. Asking for one that is not available is an error. It narrows the access token only.
curl -sS https://api.suredocs.ie/oauth/token \
  --data-urlencode grant_type=refresh_token \
  --data-urlencode client_id="$CLIENT_ID" \
  --data-urlencode refresh_token="$REFRESH_TOKEN" \
  --data-urlencode broker_id="$BROKER_ID"

The response is 200 with a JSON body:

{
  "access_token": "<the access token, a long opaque string>",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "<the NEXT refresh token>",
  "scope": "upload_file"
}
Field Meaning
access_token The bearer token.
token_type Always Bearer.
expires_in Seconds the access token is valid for.
refresh_token The next refresh token. It replaces the one you sent.
scope The scopes granted, separated by spaces. Absent when there are none.

The response is never to be cached, and carries Cache-Control: no-store, because it holds credentials.

Every exchange returns a new refresh token

Each successful exchange returns a new refresh token beside the access token, and the one you presented stops working (RFC 9700 section 4.14.2). A refresh token that is presented after it has been used is treated as stolen: we cannot tell which of two holders is the thief, so we revoke the integration's live refresh token, and it has to be reissued by us. Ordinary OAuth libraries already handle a new refresh token in a response. These rules are what your code has to add:

How long things last

What Lifetime
Access token 1 hour.
Refresh token, absolute 365 days from the token we issued you. Rotation does not restart this: once a year we issue a new token, which is when somebody confirms the integration is still wanted. We email your integration's contact address 30 days before.
Refresh token, unused 90 days. A token not exchanged for that long dies. Every exchange issues a new token, so this restarts at each one.
Retry of a lost response 10 minutes from the first exchange, with the previous refresh token.

When we revoke a refresh token or suspend an integration, the token endpoint refuses you at once. Access tokens already issued keep working until they expire, which is at most 1 hour, apart from a suspension or a revoked grant, which are refused within about a minute.

Calling an API

Send the access token in the Authorization header, and nowhere else:

Authorization: Bearer <access token>

A token in a query string or a request body is treated as no token at all. Never send a token over plain HTTP.

Scopes

The scopes are a closed list, and no scope implies another.

Scope Lets a token call
upload_file POST /files/v1

Errors from the token endpoint

The token endpoint answers errors in the shape RFC 6749 section 5.2 gives, because that is what OAuth libraries parse. This is different from the product APIs' error shape below.

{
  "error": "invalid_grant",
  "error_description": "The refresh token cannot be used."
}

Branch on error and the HTTP status. error_description is for a person, may be reworded at any time, and must never be matched.

error Status What it means What to do
invalid_request 400 A parameter is missing, repeated or malformed, or the body could not be read. Fix the request. Retrying it unchanged fails again.
unsupported_grant_type 400 grant_type is not refresh_token. Send refresh_token. It is the only grant.
invalid_client 401 The client_id is unknown, the integration is suspended, or it does not own that refresh token. One answer covers all three. Check the id and token belong together. If they do, contact us.
invalid_grant 400 The refresh token is unknown, expired, revoked, already used (outside the retry window), or replaced by a newer one we issued. Stop. Do not retry, and do not loop. If only one server should be exchanging, look for a second. Contact us and we will issue a new token.
invalid_target 400 The brokerage does not exist, or has not granted your integration access. We do not say which, so the endpoint cannot be used to discover our brokerages. Check broker_id. Ask the brokerage to confirm its grant.
invalid_scope 400 A scope you asked for is not available for this refresh token and brokerage. Ask for fewer scopes, or omit scope.
server_error 500 Our fault. Retry with the same refresh token after a short pause, backing off. Quote the Request-Id header if it persists.
rate_limited_token 429 Too many exchanges for your integration in a minute. Wait Retry-After seconds. A correct integration exchanges about once an hour per brokerage.
rate_limited_token_daily 429 Too many exchanges for your integration today. Wait Retry-After seconds, and look for a loop.
rate_limited_token_ip 429 Too many exchanges from your IP address in a minute. Wait Retry-After seconds.

Errors from a product API

A product API answers every error with one shape:

{
  "status": 403,
  "reason": "insufficient_scope",
  "message": "This token does not carry the upload_file scope.",
  "requestId": "2f4a5f8e-9d3c-4b6e-8f4a-0c5b1e7d2a90"
}

reason is what your code branches on. message is for a person and is never matched. requestId is the Request-Id header again: quote it when you report a problem.

reason Status What it means What to do
invalid_token 401 There is no access token, or it is malformed, expired, wrongly signed or not ours. The WWW-Authenticate header says Bearer, with error="invalid_token" when a token was sent. Exchange your refresh token for a new access token once, and repeat the call once. If that fails too, stop.
insufficient_scope 403 The token does not carry a scope the operation needs. The WWW-Authenticate header's scope attribute names the scopes the operation needs. Get a token that carries them. That needs the scope on your refresh token and in the brokerage's grant. A retry with the same token fails again.
not_granted 403 Your integration is suspended, or the brokerage has no live grant for it, has revoked it, or the grant lacks a scope. One wording covers every cause, so it cannot be used to discover which brokerages exist. Do not retry, and a new access token will not help. Ask the brokerage, or contact us.
rate_limited_<policy> 429 A rate limit refused the call. See Rate limits. Wait Retry-After seconds.

Operations also have their own reasons, listed in the API reference.

Do not retry a 401 in a loop

A 401 is not a reason to send the same request again. It means the access token is not usable, so nothing changes until you have a different one. The right response is one exchange for a new access token, and one repeat of the call. If the repeat is a 401 as well, something else is wrong: stop, and report the Request-Id of the failing call. A loop that exchanges on every 401 will exhaust the token endpoint's rate limits, and a loop that sends several exchanges at once looks like theft and revokes your refresh token.

Storing a refresh token safely

A refresh token is a credential for every brokerage that has granted your integration access. Treat it as you would a database password.