Using OAuth

This documentation will focus on OAuth 2.0, which is the recommended Authentication method. For the full API Reference see here.

Integration client

You will need an OAuth client to be used by your integration to perform the OAuth 2.0 authentication flow.

To create such a client please open a ticket in our Support Center asking for the creation of an integration client.
Please specify in the request the name of the client and the redirect_uri towards which you will receive the callback requests.

We will provide you with a client_id and client_secret.

The redirect URI

Sometimes called "callback URL", it's a URI pointing to your integration. For more information see RFC 6819, section 3.5.

Penneo supports the use of any domain name, including localhost, with or without SSL and additional port numbers.

Following RFC 6749, section 3.1.2, no wildcard subdomain or URI fragment is allowed as only absolute URIs are considered valid.

Query parameters may be included in the URI, but their value must not change once registered and that's because the redirect URI passed during the authentication request must be exactly identical to the registered one.

Also, it must be URL encoded when performing the authentication request to avoid collisions with the other request parameters. See more below.

Choosing the grant type

Select from two grant type for the initial auth

  1. The Authorization Code Grant is suited for integrations with user interaction. It allows for multiple different Penneo users to login via SSO or Penneo credentials.
  2. The API Keys Grant is suited for headless, fully automatic integrations, which don't have any user interaction.

When completing these grants, you will receive an access token. The access token has a short lifespan and if you need to perform actions after it has expired, you have to generate a new token.

If you're using the API Keys Grant, you have to use the same grant again, to receive a new access token.

If you're using the Authorisation Code Grant, you will receive a refresh token, which you can exchange for a new token, using the Refresh Token Grant.


The PHP SDK fully supports the Authorisation Code Grant, API Keys Grant and the legacy WSSE authentication flow.

The .Net C# SDK currently only supports the legacy WSSE authentication flow, thus it's not suitable for OAuth 2.0.

If you're working in a different programming language, you can build a custom integration from scratch by using the OAuth 2.0 API endpoint documentation.

Authorization Code Grant

The Authorization Code Grant type is used to exchange an authorization code for an access and refresh token pair.

Every time your user wants to use your integration, they will perform the authentication towards the Penneo API by logging in with their own credentials.

Start the authentication flow in your integration by redirecting the user to the /authorize endpoint.

    ?client_id=<your client id>
    &redirect_uri=<your redirect uri>
    &state=<your optional CSRF token>

You will need the client_id and the redirect_uri.

The redirect_uri must be provided identically as it was provided upon registration.
As mentioned before, it may include query parameters, in which case the query must be URL encoded.
For example, if the integration client was registered with http://localhost?foo=bar, during the request you must pass it as http://localhost/?foo%3Dbar.

The response_type parameter needs to always be code in order to obtain an authorization code that you can exchange for an access_token later.

For additional security, we strongly recommend to also include the state parameter with the value of your own CSRF token. See RFC 6749, section 10.12 for more information.

See API Reference - Authenticate a user with Penneo

The user will then be redirected to the Penneo Login page, asking them to perform a login with the method of their preference among the ones available (username + password, eID, Google or Microsoft SSO, ...).
Once logged in, the application will redirect the user back to the specified redirect_uri, adding a code parameter in the request.

If you included the state parameter in your request, it will also be included as-is in the redirect, so it can be validated on your side.

Your redirect_uri needs to point to a route, a script, a lambda function, or whatever piece of code that is capable of parsing the URL, grabbing the code parameter, and then performing an HTTP POST request to exchange that code for an access_token, specifying that the grant type should be authorization_code.

See API Reference - Obtain or refresh an access_token


Authorization code expiration

The authorization code will expire after 60 seconds if not used to obtain an access_token.

The response will contain a JSON object that represents the access token.
See Performing authenticated requests.


Access Token expiration

The access token is short lived and will expire after 3600 seconds (1 hour). After that it must be either refreshed using a refresh token or starting a new authentication flow.

API Keys Grant

The API Keys Grant is designed for server-to-server communication where user interaction is not possible or required. It allows your application to authenticate and receive an access token using your Penneo API keys. Here's how you can implement this grant type:

To use the API Keys Grant, you will first need to prepare a hashed digest of the API secret. This is done to protect the secret in case of the initial request information being leaked. Where can I find API keys for my account?

The digest consists of three parts:

  1. A nonce - a randomly generated string, which is no longer than 64 characters.
  2. The current date time in either UTC or including timezone information (most common formats are supported).
  3. The API secret

The parts are concatenated, hashed using SHA1 and then Base64-encoded.

// @see:
const { UsernameToken } = require('wsse');
// @see:
const axios = require('axios');

const penneoOAuthBaseURL = '<<PENNEO_OAUTH_BASE_URL>>';

const clientId = '<<YOUR_CLIENT_ID>>';
const clientSecret = '<<YOUR_CLIENT_SECRET>>';

const apiKey = '<<YOUR_API_KEY>>';
const apiSecret = '<<YOUR_API_SECRET>>';

const token = new UsernameToken({
  username: apiKey,
  password: apiSecret

const payload = {
  client_id: clientId,
  client_secret: clientSecret,
  grant_type: 'api_keys',
  key: apiKey,
  nonce: token.getNonceBase64(),
  created_at: token.getCreated(),
  digest: token.getPasswordDigest(),

  .post(penneoOAuthBaseURL, payload)
  .then((response) => {
    console.log("Got response",;
  .catch((error) => {
    console.error("Got an error", error);
// You can use the PHP SDK. See the usage examples in
using System.Security.Cryptography;
using System.Text;

public class Program
    private const string API_KEY = "<<YOUR_API_KEY>>";
    private const string API_SECRET = "<<YOUR_API_SECRET>>";

    private const string CLIENT_ID = "<<YOUR_CLIENT_ID>>";
    private const string CLIENT_SECRET = "<<YOUR_CLIENT_SECRET>>";

    private const string PENNEO_OAUTH_BASE_URL = "<<PENNEO_OAUTH_BASE_URL>>";
    public static Task Main()
        string created = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");
        string nonce = GenerateNonce(64);
        string digest = GenerateDigest(nonce, created, API_SECRET);

        return MakeRequest(new
            client_id = CLIENT_ID,
            client_secret = CLIENT_SECRET,
            grant_type = "api_keys",
            key = API_KEY,
            nonce = Convert.ToBase64String(Encoding.UTF8.GetBytes(nonce)),
            created_at = created,

    private static async Task MakeRequest(Object payload)
        var handler = new HttpClientHandler();
        handler.ServerCertificateCustomValidationCallback = (_,_,_,_) => true;

        using HttpClient httpClient = new HttpClient(handler);
        var payloadJson = Newtonsoft.Json.JsonConvert.SerializeObject(payload);
        var content = new StringContent(payloadJson, Encoding.UTF8, "application/json");

        HttpResponseMessage response =
            await httpClient.PostAsync(PENNEO_OAUTH_BASE_URL, content);

        if (response.IsSuccessStatusCode)
            string responseContent = await response.Content.ReadAsStringAsync();
            Console.WriteLine("Got response: " + responseContent);
            Console.WriteLine("HTTP error: " + response.StatusCode);

    private static string GenerateNonce(int length)
        var random = new Random();
        const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        var nonceChars = new char[length];

        for (var i = 0; i < nonceChars.Length; i++)
            nonceChars[i] = chars[random.Next(chars.Length)];

        return new string(nonceChars);

    private static string GenerateDigest(string nonce, string created, string secret)
        using var sha1 = SHA1.Create();
        var inputBytes = Encoding.UTF8.GetBytes(nonce + created + secret);
        var hashBytes = sha1.ComputeHash(inputBytes);
        return Convert.ToBase64String(hashBytes);

To use the access token in the response, you should see Performing authenticated requests.

If your access token has expired, but you still need to perform API calls, you have to use the same grant again, to receive a new access token.

Refresh Token Grant

The Refresh Token Grant type is used by your integration to exchange a previously obtained refresh token for a new fresh access token when the previous one has expired.

This allows your integration to continue to have a valid access token without having the user log in again, as long as the refresh_token is not expired or the client scopes aren't changed.


Refresh Token expiration

The refresh token will expire immediately after use, if the requested scopes have changed, or after 432000 seconds (5 days) if not used to obtain an access_token.

See API Reference - Obtain or refresh an access_token

The response will contain a JSON object that represents the access token and a new refresh token.
See Performing authenticated requests.

Performing authenticated requests

This is what the JSON Token object looks like:

  "access_token": "<encoded alphanumeric JWT>",
  "access_token_expires_at": integer,
  "token_type": "bearer",
  "expires_in": integer,
  "refresh_token": "<encoded alphanumeric JWT>", // omitted when using `api_keys` grant
  "refresh_token_expires_at": integer, // omitted when using `api_keys` grant
  • access_token and refresh_token are JWTs. You can verify their content with this useful tool:
  • token_type specifies the type of token. We only support bearer.
  • expires_in specifies the number of seconds before the access_token is invalidated.
  • access_token_expires_at specifies the timestamp after which the access_token is invalidated.
    Fixed at 3600 seconds (1 hour).
  • refresh_token_expires_at specifies the timestamp after which the refresh_token is invalidated.
    Fixed at 432000 seconds (5 days) if not used to obtain a new access_token, but will expire immediately after use and cannot be used twice.

Then you can use the access token in each requests header.
The following is an example of how to get a list of CaseFiles that belong to the authorized user.

const access_token = '<your access token>';

  method: 'GET',
  url: '<<PENNEO_SIGN_BASE_URL>>/api/v3/casefiles',
  headers: {
		'Authorization': 'Bearer ' + access_token,
		'Accept': 'application/json'
}).then(response => {
  const listOfCaseFiles =
// You can use the PHP SDK. See the usage examples in
using System;
using System.Net;
using System.IO;

class Program
    static void Main()
        string access_token = "<your access token>";

        string apiUrl = "<<PENNEO_SIGN_BASE_URL>>/api/v3/casefiles";

        var request = (HttpWebRequest)WebRequest.Create(apiUrl);
        request.Method = "GET";
        request.Headers.Add("Authorization", "Bearer " + access_token);
        request.Headers.Add("Accept", "application/json");

        using (var response = (HttpWebResponse)request.GetResponse())
            using (var reader = new StreamReader(response.GetResponseStream()))
                string responseText = reader.ReadToEnd();

Full reference for the Penneo Sign API available at [](

See API integration essentials

Validating a token

As said, every token adheres to the JWT standard defined by RFC 7519 and thus contains a set of predefined attributes.

You can always validate the structure, the content and the signature of a Penneo JWT using (to validate the signature use this JWTK endpoint: <<PENNEO_OAUTH_BASE_URL>>/oauth/token/jwks).

To validate a token and verify that it's still valid and not expired you can then decode the JWT and grab the value of the exp (expire date) attribute, expressed in unix seconds.

To do so, you should start by splitting the JWT into its three parts (header, payload and signature) delimited by a dot, decoding the base64-encoded payload and parse it as JSON. Then you will be able to compare the exp timestamp with the current timestamp.

const jwtToken = "<your access token>";
const [header, payload, signature] = jwtToken.split('.');

// Decoding the payload
const decodedPayload = JSON.parse(atob(payload));

// Retrieving the expiration timestamp
const expirationTimestamp = decodedPayload.exp * 1000; // Convert to milliseconds

// Getting the current timestamp in seconds
const currentTimestamp = Math.floor( / 1000);

// Comparing the expiration timestamp with the current timestamp
if (expirationTimestamp < currentTimestamp) {
  console.log('Token has expired');
} else {
  console.log('Token is valid');
// You can use the PHP SDK. See the usage examples in
using System;
using System.Text;

class Program
    static void Main()
        string jwtToken = "<your access token>";
        string[] tokenParts = jwtToken.Split('.');

        // Decoding the payload
        byte[] payloadBytes = Convert.FromBase64String(tokenParts[1]);
        string payload = Encoding.UTF8.GetString(payloadBytes);

        // Parsing the payload as JSON
        dynamic decodedPayload = Newtonsoft.Json.JsonConvert.DeserializeObject(payload);

        // Retrieving the expiration timestamp
        long expirationTimestamp = (long)decodedPayload.exp;

        // Getting the current timestamp
        long currentTimestamp = DateTimeOffset.Now.ToUnixTimeSeconds();

        // Comparing the expiration timestamp with the current timestamp
        if (expirationTimestamp < currentTimestamp)
            Console.WriteLine("Token has expired");
            Console.WriteLine("Token is valid");