Obtaining Access Tokens
DVS uses the OAuth2 client credentials grant for service-to-service authentication. The OSIGU Auth Server uses HTTP Basic auth for client identification and query string for the grant type — this page shows you the exact request shape and copy-paste implementations in 5 languages.
Endpoints
| Environment | Token URL |
|---|---|
| Sandbox | https://sandbox.osigu.com/v1/oauth/token |
| Production | https://api.osigu.com/v1/oauth/token |
Credentials are environment-scoped — sandbox client_id does not work against the production token URL.
Request shape
grant_typestringrequiredSent as a query string parameter (not in the body). Always client_credentials.
AuthorizationheaderrequiredHTTP Basic auth: Basic <base64(client_id:client_secret)>. Use the credentials OSIGU issued for the target environment.
Method: POST
Body: empty
Content-Type: not required (no body)
Response
{
"access_token": "7dd4f350-676e-4257-9d7b-f3c5ac4dfi14",
"token_type": "bearer",
"expires_in": 86399,
"scope": "read write",
"extensions": {
"provider_slug": "br-gamma"
}
}
| Field | Description |
|---|---|
access_token | Opaque token (not a JWT). Send as Authorization: Bearer <token> on subsequent DVS API calls. |
token_type | Always bearer. |
expires_in | Seconds until expiration. Default is 86399 (~24 hours). |
scope | Coarse OAuth2 scope granted to the token (e.g., read write). DVS's finer per-endpoint authorities are enforced server-side from your client_id configuration. |
extensions | Free-form metadata map. Includes your provider_slug (use it to log which tenant the token belongs to). May include other tenant-scoped fields OSIGU adds in the future. |
Code samples
- curl
- Python
- Node.js
- PHP
- Java
curl --location --request POST \
"https://sandbox.osigu.com/v1/oauth/token?grant_type=client_credentials" \
--header "Authorization: Basic $(echo -n "$DVS_CLIENT_ID:$DVS_CLIENT_SECRET" | base64)"
import base64
import os
import time
import httpx
from threading import Lock
TOKEN_URL = "https://sandbox.osigu.com/v1/oauth/token" # switch to api.osigu.com in prod
CLIENT_ID = os.environ["DVS_CLIENT_ID"]
CLIENT_SECRET = os.environ["DVS_CLIENT_SECRET"]
_cache = {"access_token": None, "expires_at": 0.0, "provider_slug": None}
_lock = Lock()
def get_access_token() -> str:
"""Return a valid access token, refreshing it 60s before expiry."""
with _lock:
if _cache["access_token"] and time.time() < _cache["expires_at"] - 60:
return _cache["access_token"]
basic = base64.b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode()
response = httpx.post(
TOKEN_URL,
params={"grant_type": "client_credentials"},
headers={"Authorization": f"Basic {basic}"},
timeout=10,
)
response.raise_for_status()
payload = response.json()
_cache["access_token"] = payload["access_token"]
_cache["expires_at"] = time.time() + payload["expires_in"]
_cache["provider_slug"] = payload.get("extensions", {}).get("provider_slug")
return _cache["access_token"]
const TOKEN_URL = "https://sandbox.osigu.com/v1/oauth/token"; // switch to api.osigu.com in prod
let cached = { token: null, expiresAt: 0, providerSlug: null };
async function getAccessToken() {
if (cached.token && Date.now() < cached.expiresAt - 60_000) {
return cached.token;
}
const basic = Buffer.from(
`${process.env.DVS_CLIENT_ID}:${process.env.DVS_CLIENT_SECRET}`,
).toString("base64");
const response = await fetch(
`${TOKEN_URL}?grant_type=client_credentials`,
{
method: "POST",
headers: { Authorization: `Basic ${basic}` },
},
);
if (!response.ok) {
throw new Error(`Auth failed: ${response.status}`);
}
const payload = await response.json();
cached = {
token: payload.access_token,
expiresAt: Date.now() + payload.expires_in * 1000,
providerSlug: payload.extensions?.provider_slug ?? null,
};
return cached.token;
}
<?php
const TOKEN_URL = "https://sandbox.osigu.com/v1/oauth/token"; // switch to api.osigu.com in prod
function getAccessToken(): string {
static $cache = ["token" => null, "expires_at" => 0, "provider_slug" => null];
if ($cache["token"] && time() < $cache["expires_at"] - 60) {
return $cache["token"];
}
$basic = base64_encode(getenv("DVS_CLIENT_ID") . ":" . getenv("DVS_CLIENT_SECRET"));
$ch = curl_init(TOKEN_URL . "?grant_type=client_credentials");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Basic $basic"]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException("Auth failed: HTTP $status");
}
$payload = json_decode($body, true);
$cache["token"] = $payload["access_token"];
$cache["expires_at"] = time() + $payload["expires_in"];
$cache["provider_slug"] = $payload["extensions"]["provider_slug"] ?? null;
return $cache["token"];
}
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Base64;
import org.json.JSONObject;
public class DvsAuth {
private static final String TOKEN_URL = "https://sandbox.osigu.com/v1/oauth/token"; // switch in prod
private static final HttpClient HTTP = HttpClient.newHttpClient();
private static volatile String cachedToken;
private static volatile Instant cachedExpiresAt = Instant.EPOCH;
private static volatile String cachedProviderSlug;
public static synchronized String getAccessToken() throws Exception {
if (cachedToken != null && Instant.now().isBefore(cachedExpiresAt.minusSeconds(60))) {
return cachedToken;
}
String creds = System.getenv("DVS_CLIENT_ID") + ":" + System.getenv("DVS_CLIENT_SECRET");
String basic = Base64.getEncoder().encodeToString(creds.getBytes(StandardCharsets.UTF_8));
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(TOKEN_URL + "?grant_type=client_credentials"))
.header("Authorization", "Basic " + basic)
.POST(HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HTTP.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new RuntimeException("Auth failed: " + response.statusCode());
}
JSONObject json = new JSONObject(response.body());
cachedToken = json.getString("access_token");
cachedExpiresAt = Instant.now().plusSeconds(json.getInt("expires_in"));
if (json.has("extensions")) {
JSONObject ext = json.getJSONObject("extensions");
cachedProviderSlug = ext.optString("provider_slug", null);
}
return cachedToken;
}
}
Best practices
Cache tokens
Tokens are valid for ~24 hours by default. Cache them in memory (or shared cache like Redis for multi-instance services) and refresh ~60 seconds before expiry. Calling the OAuth endpoint on every API request will rate-limit you and add unnecessary latency.
Store secrets in a vault, not env files
Use AWS Secrets Manager, HashiCorp Vault, Doppler, or equivalent. Rotate client_secret annually (or after any suspected exposure) by contacting OSIGU.
Handle 401 by refreshing
If a request returns 401, the token may have expired or been revoked. Drop the cached token, request a new one, and retry the original request once. Don't retry infinitely.
Don't log tokens
Strip Authorization headers from your application logs. A leaked token is valid for up to 24 hours.
Use extensions.provider_slug for observability
Log the provider_slug from the token response alongside your request IDs. It makes correlating issues with OSIGU support much faster.
Errors
| Status | Body | Cause |
|---|---|---|
400 | error: "invalid_request" | Missing grant_type query string. |
401 | error: "invalid_client" | Wrong client_id or client_secret, or wrong environment (sandbox creds against production URL). |
405 | Method Not Allowed | You used GET instead of POST. |
429 | rate limited | Too many token requests. Cache and slow down. |
5xx | server error | Auth Server outage. Retry with exponential backoff (max 3). |