OAuth Token Authentication
To make authorized API calls to Nopan, clients must first obtain an OAuth 2.0 access token.
This token is issued using the client_credentials grant, should be signed, and requires mutual TLS (mTLS) with your client certificate.
The resulting access token must be included in all subsequent API requests using the Authorization header.
All token requests must use mutual TLS (mTLS) with your registered client certificate. Without a valid certificate, the token endpoint will reject your request.
All token requests must have proper HTTP message signature headers to prevent MitM tampering with the request. Without a valid signature, the token endpoint will reject your request.
Requesting an OAuth Token
Use the following examples to request an OAuth 2.0 access token for authenticating with Nopan APIs:
- cURL
- Java
- JavaScript
curl --cert <YOUR_CERT_SERIAL_NUMBER>.pem --key client-key.pem \
-X POST https://api.nopan.io/auth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Content-Digest: sha-256=:<BASE64_SHA256_OF_BODY>:" \
-H "Content-Length: <BODY_LENGTH>" \
-H "Signature: nopan_sig=:<signature>:" \
-H "Signature-Input: nopan_sig=(\"@authority\" \"@method\" \"@request-target\" \"content-digest\" \"content-type\" \"content-length\");keyid=\"<your-key-id>\";created=<unix-timestamp>" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "client_id={{your_organization_id}}" \
--data-urlencode "scope=payments:process"
private static final String TOKEN_ENDPOINT_URL = "https://api.nopan.io/auth/token";
private static final String ORGANIZATION_ID = "your-organization-id";
private static final String KEY_ID = "your-key-id";
public HttpResponse tokenRequest() throws IOException
{
String form = Map.of(
"grant_type", "client_credentials",
"client_id", ORGANIZATION_ID,
"scope", "payments:process"
)
.entrySet()
.stream()
.map(e -> e.getKey() + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8))
.collect(Collectors.joining("&"));
// Compute Content-Digest (SHA-256 of body, base64-encoded, wrapped in colons)
byte[] bodyBytes = form.getBytes(StandardCharsets.UTF_8);
String contentDigest = "sha-256=:" + Base64.getEncoder().encodeToString(
MessageDigest.getInstance("SHA-256").digest(bodyBytes)) + ":";
String contentLength = String.valueOf(bodyBytes.length);
long created = Instant.now().getEpochSecond();
// Build the signature base and sign it
String signatureParams = "nopan_sig=(\"@authority\" \"@method\" \"@request-target\" \"content-digest\" \"content-type\" \"content-length\");keyid=\"" + KEY_ID + "\";created=" + created;
String signatureBase =
"\"@authority\": api.nopan.io\n" +
"\"@method\": POST\n" +
"\"@request-target\": /auth/token\n" +
"\"content-digest\": " + contentDigest + "\n" +
"\"content-type\": application/x-www-form-urlencoded\n" +
"\"content-length\": " + contentLength + "\n" +
"\"@signature-params\": " + signatureParams;
String signature = signWithPrivateKey(signatureBase, loadPrivateKey(KEY_FILE_PATH));
// Create an http request
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(TOKEN_ENDPOINT_URL))
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Content-Digest", contentDigest)
.header("Content-Length", contentLength)
.header("Signature", "nopan_sig=:" + signature + ":")
.header("Signature-Input", signatureParams)
.POST(HttpRequest.BodyPublishers.ofString(form))
.build();
// Create a client with the certificate and key
SSLContext sslContext = ;
HttpClient client = HttpClientBuilder.create()
.setSSLSocketFactory(createSslContext(CERT_FILE_PATH, KEY_FILE_PATH, KEY_PASSWORD))
.build();
// Send the request and get the response
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
return response;
}
private static SSLContext createSslContext(String certFilePath, String keyFilePath, String keyPassword) throws Exception {
// Load Certificate from PEM file
X509Certificate certificate;
try (FileReader certReader = new FileReader(certFilePath); PEMParser pemParser = new PEMParser(certReader)) {
X509CertificateHolder certificateHolder = (X509CertificateHolder) pemParser.readObject();
certificate = new JcaX509CertificateConverter().setProvider("BC").getCertificate(certificateHolder);
}
// Load Private Key from PEM file
KeyPair keyPair;
try (FileReader keyReader = new FileReader(keyFilePath); PEMParser pemParser = new PEMParser(keyReader)) {
Object pemObject = pemParser.readObject();
if (pemObject instanceof PEMKeyPair) {
keyPair = new JcaPEMKeyConverter().setProvider("BC").getKeyPair((PEMKeyPair) pemObject);
} else if (pemObject instanceof PrivateKeyInfo) {
keyPair = new KeyPair(null, new JcaPEMKeyConverter().setProvider("BC").getPrivateKey((PrivateKeyInfo) pemObject));
} else {
throw new IOException("Unsupported key type in PEM file: " + pemObject.getClass().getName());
}
}
// Create an in-memory KeyStore
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null); // Initialize a new empty KeyStore
// Add the certificate and private key to the KeyStore
char[] passwordChars = keyPassword.toCharArray();
keyStore.setKeyEntry("client-alias", keyPair.getPrivate(), passwordChars, new Certificate[]{certificate});
// Create KeyManagerFactory to manage our private key
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, passwordChars);
// Create TrustManagerFactory (using default system truststores)
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
// Create and initialize the SSLContext
SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
return sslContext;
}
// Read the certificate and private key files from the filesystem.
const cert = fs.readFileSync(CERT_FILE_PATH);
const key = fs.readFileSync(KEY_FILE_PATH);
async function getToken() {
try {
// Create a custom HTTPS agent configured for mutual TLS (mTLS).
// This agent will present the client certificate during the TLS handshake.
const httpsAgent = new https.Agent({
cert: cert,
key: key,
});
// Prepare the form data for the request body.
const formData = {
grant_type: 'client_credentials',
client_id: ORGANIZATION_ID,
scope: 'payments:process'
};
const requestBody = querystring.stringify(formData);
// Compute Content-Digest (SHA-256 of body, base64-encoded, wrapped in colons)
const bodyBuffer = Buffer.from(requestBody, 'utf8');
const contentDigest = 'sha-256=:' + crypto.createHash('sha256').update(bodyBuffer).digest('base64') + ':';
const contentLength = String(bodyBuffer.length);
const created = Math.floor(Date.now() / 1000);
// Build the signature base and sign it
const signatureParams = `nopan_sig=("@authority" "@method" "@request-target" "content-digest" "content-type" "content-length");keyid="${KEY_ID}";created=${created}`;
const signatureBase = [
'"@authority": api.nopan.io',
'"@method": POST',
'"@request-target": /auth/token',
`"content-digest": ${contentDigest}`,
'"content-type": application/x-www-form-urlencoded',
`"content-length": ${contentLength}`,
`"@signature-params": ${signatureParams}`,
].join('\n');
const privateKeyPem = fs.readFileSync(SIGNING_KEY_FILE_PATH, 'utf8');
// Pick the algorithm that matches your key's curve:
// P-256 → 'SHA256'
// P-384 → 'SHA384'
// P-521 → 'SHA512'
const signer = crypto.createSign('SHA512');
signer.update(signatureBase);
const signature = signer.sign(privateKeyPem, 'base64');
// Send the request. We pass the custom httpsAgent in the request configuration.
const response = await axios.post(TOKEN_ENDPOINT_URL, requestBody, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Digest': contentDigest,
'Content-Length': contentLength,
'Signature': `nopan_sig=:${signature}:`,
'Signature-Input': signatureParams,
},
httpsAgent: httpsAgent
});
} catch (error) {
console.error("\nAn error occurred while making the token request.");
// Axios provides rich error details
if (error.response) {
// The request was made and the server responded with a status code
console.error("Error Status: ", error.response.status);
console.error("Error Data: ", error.response.data);
console.error("Error Headers: ", error.response.headers);
} else if (error.request) {
// The request was made but no response was received
console.error("No response received for the request: ", error.request);
} else {
// Something happened in setting up the request that triggered an Error
console.error("Error setting up request: ", error.message);
}
}
}
Make sure to select the correct API base url for the environment:
- sandbox: https://api.sandbox.nopan.dev
- production: https://api.nopan.io
Scopes
We currently support next scopes:
payments:readpayments:processdata:reportsTo request non-default processing scope, include it in the token request:
curl --cert client-cert.pem --key client-key.pem \
-X POST https://api.nopan.io/auth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "client_id={{your_organization_id}}" \
--data-urlencode "scope=payments:process"
Inspecting Your OAuth Access Token
Access tokens are issued as JWTs. You usually don’t need to decode the JWT because expires_in within the /auth/token response tells you token lifetime.
Your response will look like this:
{
"access_token": "...",
"expires_in": 7200,
"refresh_expires_in": 0,
"token_type": "Bearer",
"not-before-policy": 0,
"scope": "payments:process"
}
expires_in: lifetime in seconds (e.g., 7200 = 2 hours).refresh_expires_in: always0. There are no refresh tokens; always request a new access token when the old one expires.
Same token could be used for multiple requests within the allowed scope.
Renew tokens automatically a few minutes before expires_in elapses (e.g., 5 minutes before expiration).
Check the OpenAPI spec for /auth/token spec for the response format.
Using the Token
After you obtain the token, include it in your API calls:
curl -X POST https://api.nopan.io/payments/initiate \
-H "Authorization: Bearer <access-token>" \
-H "Idempotency-Key: 63c2e3f0-12aa-41bb-ae62-f3d91fdbb762" \
-H "Signature: nopan_sig=:<signature>:" \
-H "Signature-Input: nopan_sig=(\"@authority\" \"@method\" \"@request-target\" \"content-digest\" \"content-type\" \"content-length\");keyid=\"your-key-id\";created=1766678793" \
-H "Content-Type: application/json" \
-d '{
"clientTransactionId": "order123",
"transactionType": "ONE_TIME",
"paymentDetails": {
"amount": 100,
"currency": "PLN",
"country": "PL",
"description": "Product ID 1234 purchase"
},
"providerDetails": {
"providerId": "BLIK"
},
"payerDetails": {
"payerId": "payerUUID",
"oneTimeCode": "123456"
}
}'
For signing requests, see HTTP Signatures.
Common errors
Debugging Access Token
Decode the token only when debugging auth issues or validating configuration. There are a few ways to inspect your JWT token.
Using the Command Line
Decode the token payload locally for safe inspection:
TOKEN=eyJhbGciOi...<snip>...XYZ
# Extract payload and decode
echo "$TOKEN" |
cut -d '.' -f2 |
base64 -d | jq
Using the JWT.io Debugger
Paste your token into https://jwt.io to decode and inspect its contents.
Avoid using real production tokens in online tools.
Example output:
{
"exp": 1735750000,
"iat": 1735745000,
"scope": "payments:process",
"client_id": "your-organization-id",
...
}