> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gnosispay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Authenticate your backend

> Sign a client assertion with your private key, exchange it for an access token, and use the token on every Partner API request.

The Partner API uses the OAuth 2.0 [client credentials](https://oauth.net/2/grant-types/client-credentials/) grant with [private key JWT](https://oauth.net/private-key-jwt/) client authentication ([RFC 7523](https://www.rfc-editor.org/rfc/rfc7523), updated by [RFC 10070](https://www.rfc-editor.org/rfc/rfc10070)). Your backend signs a short JWT, the **client assertion**, with its private key. Gnosis Pay verifies the signature with the public key that you registered and returns an access token.

> [!WARNING]
>
> Run this flow only on a trusted backend. Never put your private key, client assertions, or access tokens in a browser or mobile app.

## Create a key pair

This guide uses an Ed25519 key, which signs with the `EdDSA` algorithm. The `token_endpoint_auth_signing_alg_values_supported` field of the [server metadata](#get-and-use-a-token) lists all the algorithms that the server accepts.

Use [OpenSSL](https://www.openssl.org/) 3.0 or later, and run the commands on your own machine. Never use a web tool to create a private key.

> [!NOTE]
>
> On macOS, the `openssl` command is LibreSSL. Older LibreSSL versions cannot create Ed25519 keys and show the error `Algorithm ED25519 not found`. Install OpenSSL 3 with [Homebrew](https://brew.sh/) and put it first in your `PATH`:
>
> ```bash theme={null}
> brew install openssl@3
> export PATH="$(brew --prefix openssl@3)/bin:$PATH"
> openssl version   # must show OpenSSL 3.x
> ```

**1. Create the private key**

The command writes a PEM file in the PKCS#8 format:

```bash theme={null}
openssl genpkey -algorithm ED25519 -out private.pem
```

**2. Derive the public key**

```bash theme={null}
openssl pkey -in private.pem -pubout -out public.pem
```

**3. Verify the key pair**

Make sure that the public key comes from the private key. The two fingerprints must be equal:

```bash theme={null}
openssl pkey -in private.pem -pubout -outform DER | openssl sha256
openssl pkey -in public.pem -pubin -outform DER | openssl sha256
```

> [!WARNING]
>
> Never share `private.pem`. Anyone who has it can get tokens in the name of your organization. Store it in a secret manager.

## Register your public key

Send `public.pem` to Gnosis Pay. Never send the private key. If you send a private key by mistake, create a new key pair.

Gnosis Pay registers the key and supplies these values for each environment. Store them as configuration, not in code.

| Variable | Description |
| - | - |
| `PARTNER_AUTH_ISSUER` | The authorization server issuer. Use it to read the server metadata and as the `aud` claim of the client assertion (see [Sandbox vs Production](https://docs.gnosispay.com/platform-integration/configuring-environment.md#base-urls)). |
| `PARTNER_CLIENT_ID` | Your client ID. Gnosis Pay sends it to you when it registers your public key. You do not create it. Use it as the `iss` and `sub` claims of the client assertion. |
| `PARTNER_KEY_ID` | The `kid` of your public key. Use it as the `kid` header of the client assertion. |
| `PARTNER_API_BASE_URL` | Partner API base URL (see [Sandbox vs Production](https://docs.gnosispay.com/platform-integration/configuring-environment.md#base-urls)) |

The `kid` is the [RFC 7638](https://www.rfc-editor.org/rfc/rfc7638) JWK thumbprint of the public key, with SHA-256 and base64url without padding. Your backend can also compute it from the key. The [Go sample](#go-sample) shows how.

> [!TIP]
>
> You can have at most 5 active keys. To rotate a key, register the new key, move your backend to it, and then ask Gnosis Pay to revoke the old key.

## Get and use a token

**1. Read the server metadata**

The server metadata ([RFC 8414](https://www.rfc-editor.org/rfc/rfc8414)) gives the `issuer`, the `token_endpoint`, and the accepted signing algorithms:

```bash cURL theme={null}
PARTNER_AUTH_ISSUER=https://core.sandbox.gnosispay.in/

curl --request GET "${PARTNER_AUTH_ISSUER%/}/.well-known/oauth-authorization-server"
```

The token endpoint is `POST <issuer>/oauth/token`.

**2. Sign a client assertion**

Create a compact JWS with one signature and these headers and claims:

| Field | Rule |
| - | - |
| Header `alg` | An algorithm from `token_endpoint_auth_signing_alg_values_supported`. It must be the algorithm of the key that `kid` names. Use `EdDSA` for an Ed25519 key. |
| Header `kid` | Required. The `kid` of an active key. |
| Header `typ` | Required. It must be `client-authentication+jwt`. |
| `iss` | Required. Your client ID. |
| `sub` | Required. Your client ID. It must be equal to `iss`. |
| `aud` | Required. The `issuer` as a single string, not an array. |
| `exp` | Required. At most 5 minutes after `iat`. Without `iat`, at most 5 minutes after the current time. |
| `iat` | Optional. The time at which you signed the assertion. |
| `jti` | Required. A unique value, for example a UUID. The server accepts each `jti` one time only. |

The server accepts a clock skew of 30 seconds. Sign a new assertion for each token request. Do not reuse an assertion.

**3. Request an access token**

Send the client assertion to the token endpoint as an `application/x-www-form-urlencoded` body:

```bash cURL theme={null}
PARTNER_AUTH_ISSUER=https://core.sandbox.gnosispay.in/

curl --request POST "${PARTNER_AUTH_ISSUER%/}/oauth/token" \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=client_credentials' \
  --data-urlencode 'client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer' \
  --data-urlencode "client_assertion=$CLIENT_ASSERTION"
```

A success response has status `200`:

```json theme={null}
{
  "access_token": "<JWT>",
  "token_type": "Bearer",
  "expires_in": 900,
  "scope": "full_access"
}
```

The access token is valid for 15 minutes.

**4. Cache the token**

Use the same access token for all API calls until its refresh time. Then request a new token. Don't request a new token for every API call.

> [!TIP]
>
> Set the refresh time to 1 minute before the token expires: the time of the token response, plus `expires_in`, minus 60 seconds. This margin covers a difference between your clock and the server clock, and a slow network. It also keeps your backend below the limit of 60 accepted token requests per minute.

**5. Send the token with every request**

Pass the token as a bearer token in the `Authorization` header:

```bash cURL theme={null}
curl --request GET "$PARTNER_API_BASE_URL/kyc/terms-of-service" \
  --header "Authorization: Bearer $PARTNER_API_TOKEN"
```

> [!NOTE]
>
> A token is scoped to one partner organization, so you don't send an organization ID in Partner API requests. The token gives access to every resource in your organization. Check that a resource belongs to the requesting user before you act on it.

## Token request reference

### Parameters

Send each parameter one time only. The server reads the parameters only from the body, not from the query string.

| Parameter | Value |
| - | - |
| `grant_type` | `client_credentials` |
| `client_assertion_type` | `urn:ietf:params:oauth:client-assertion-type:jwt-bearer` |
| `client_assertion` | The signed client assertion |
| `client_id` | Optional. When you send it, it must be equal to the `sub` claim. |
| `scope` | Optional. Space-separated scopes. Without it, the token gets all the scopes of your application. |

### Errors

An error response follows [RFC 6749 §5.2](https://www.rfc-editor.org/rfc/rfc6749#section-5.2):

| Status | `error` | Cause |
| - | - | - |
| `400` | `invalid_request` | The body is not a valid form, or a parameter is missing or sent two times. |
| `400` | `unsupported_grant_type` | The `grant_type` is not `client_credentials`. |
| `400` | `invalid_scope` | Your application does not have a requested scope. |
| `401` | `invalid_client` | The server refused the assertion. The description is always `client authentication failed`. The server does not tell which rule failed. |
| `429` | `too_many_requests` | You reached the rate limit. The `Retry-After` header gives the wait time in seconds. |

### Rate limits

| Limit | Requests per minute | Counted per |
| - | - | - |
| Accepted token requests | 60 | Application |
| Refused token requests | 60 | Source IP |

## Go sample

The sample uses [`github.com/lestrrat-go/jwx/v4`](https://github.com/lestrrat-go/jwx) and needs Go 1.27 or later. It does these steps:

1. Load the private key from `private.pem` and compute its `kid`.
2. Sign the client assertion.
3. Exchange the client assertion for an access token.
4. Print the access token on stdout.

Replace the `issuer` and `clientID` constants with the values from your integration.

> [accordion] main.go
>
> ```go main.go theme={null}
> package main
>
> import (
> 	"context"
> 	"crypto"
> 	"encoding/base64"
> 	"encoding/json"
> 	"errors"
> 	"fmt"
> 	"io"
> 	"net/http"
> 	"net/url"
> 	"os"
> 	"strings"
> 	"time"
> 	"uuid"
>
> 	"github.com/lestrrat-go/jwx/v4/jwa"
> 	"github.com/lestrrat-go/jwx/v4/jwk"
> 	"github.com/lestrrat-go/jwx/v4/jws"
> )
>
> // Values that you get from Gnosis Pay. The issuer is the "issuer" field
> // of GET <issuer>/.well-known/oauth-authorization-server.
> const (
> 	issuer         = "https://core.sandbox.gnosispay.in/"
> 	clientID       = "<PARTNER_CLIENT_ID>"
> 	privateKeyPath = "private.pem"
> )
>
> const clientAssertionType = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
>
> type tokenResponse struct {
> 	AccessToken string `json:"access_token"`
> 	TokenType   string `json:"token_type"`
> 	ExpiresIn   int64  `json:"expires_in"`
> 	Scope       string `json:"scope"`
> }
>
> type tokenErrorResponse struct {
> 	Error            string `json:"error"`
> 	ErrorDescription string `json:"error_description"`
> }
>
> func main() {
> 	ctx := context.Background()
>
> 	key, err := loadPrivateKey(privateKeyPath)
> 	if err != nil {
> 		fmt.Fprintln(os.Stderr, err)
> 		os.Exit(1)
> 	}
>
> 	assertion, err := signClientAssertion(key)
> 	if err != nil {
> 		fmt.Fprintln(os.Stderr, err)
> 		os.Exit(1)
> 	}
>
> 	token, err := requestAccessToken(ctx, assertion)
> 	if err != nil {
> 		fmt.Fprintln(os.Stderr, err)
> 		os.Exit(1)
> 	}
>
> 	// Step 4: print the access token. Only the token goes to stdout, so that
> 	// PARTNER_API_TOKEN=$(go run .) captures it.
> 	fmt.Fprintf(os.Stderr, "token expires in %d seconds, scope %q\n", token.ExpiresIn, token.Scope)
> 	fmt.Println(token.AccessToken)
> }
>
> // Step 1: load the private key and set its kid. The kid is the RFC 7638 JWK
> // thumbprint of the public key (SHA-256, base64url without padding). It is
> // equal to the kid that Gnosis Pay returned when it registered the public key.
> func loadPrivateKey(path string) (jwk.Key, error) {
> 	pemBytes, err := os.ReadFile(path)
> 	if err != nil {
> 		return nil, fmt.Errorf("read private key: %w", err)
> 	}
>
> 	key, err := jwk.ParseKey(pemBytes, jwk.WithX509(true))
> 	if err != nil {
> 		return nil, fmt.Errorf("parse private key: %w", err)
> 	}
>
> 	publicKey, err := key.PublicKey()
> 	if err != nil {
> 		return nil, fmt.Errorf("derive public key: %w", err)
> 	}
> 	thumbprint, err := publicKey.Thumbprint(crypto.SHA256)
> 	if err != nil {
> 		return nil, fmt.Errorf("compute kid: %w", err)
> 	}
> 	kid := base64.RawURLEncoding.EncodeToString(thumbprint)
> 	if err := key.Set(jwk.KeyIDKey, kid); err != nil {
> 		return nil, fmt.Errorf("set kid: %w", err)
> 	}
> 	return key, nil
> }
>
> // Step 2: sign the client assertion. The claims are a JSON object, so that
> // "aud" is a single string and not an array.
> func signClientAssertion(key jwk.Key) (string, error) {
> 	now := time.Now()
> 	claims, err := json.Marshal(map[string]any{
> 		"iss": clientID,
> 		"sub": clientID,
> 		"aud": issuer,
> 		"jti": uuid.NewV7().String(),
> 		"iat": now.Unix(),
> 		"exp": now.Add(time.Minute).Unix(),
> 	})
> 	if err != nil {
> 		return "", fmt.Errorf("encode claims: %w", err)
> 	}
>
> 	kid, _ := key.KeyID()
> 	headers := jws.NewHeaders()
> 	if err := headers.Set(jws.KeyIDKey, kid); err != nil {
> 		return "", fmt.Errorf("set kid header: %w", err)
> 	}
> 	if err := headers.Set(jws.TypeKey, "client-authentication+jwt"); err != nil {
> 		return "", fmt.Errorf("set typ header: %w", err)
> 	}
>
> 	// The server accepts only alg "EdDSA" for an Ed25519 key, so use jwa.EdDSA()
> 	// and not jwa.EdDSAEd25519(), which writes alg "Ed25519". Use jwa.ES256()
> 	// for an EC P-256 key.
> 	signed, err := jws.Sign(claims, jws.WithKey(jwa.EdDSA(), key, jws.WithProtectedHeaders(headers)))
> 	if err != nil {
> 		return "", fmt.Errorf("sign client assertion: %w", err)
> 	}
> 	return string(signed), nil
> }
>
> // Step 3: exchange the client assertion for an access token.
> func requestAccessToken(ctx context.Context, assertion string) (tokenResponse, error) {
> 	form := url.Values{
> 		"grant_type":            {"client_credentials"},
> 		"client_assertion_type": {clientAssertionType},
> 		"client_assertion":      {assertion},
> 	}
>
> 	tokenEndpoint := strings.TrimSuffix(issuer, "/") + "/oauth/token"
> 	req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenEndpoint, strings.NewReader(form.Encode()))
> 	if err != nil {
> 		return tokenResponse{}, fmt.Errorf("build token request: %w", err)
> 	}
> 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
>
> 	resp, err := http.DefaultClient.Do(req)
> 	if err != nil {
> 		return tokenResponse{}, fmt.Errorf("send token request: %w", err)
> 	}
> 	defer resp.Body.Close()
>
> 	body, err := io.ReadAll(resp.Body)
> 	if err != nil {
> 		return tokenResponse{}, fmt.Errorf("read token response: %w", err)
> 	}
>
> 	if resp.StatusCode != http.StatusOK {
> 		var tokenErr tokenErrorResponse
> 		if err := json.Unmarshal(body, &tokenErr); err != nil {
> 			return tokenResponse{}, fmt.Errorf("token request failed with status %d", resp.StatusCode)
> 		}
> 		return tokenResponse{}, errors.New("token request failed: " + tokenErr.Error + ": " + tokenErr.ErrorDescription)
> 	}
>
> 	var token tokenResponse
> 	if err := json.Unmarshal(body, &token); err != nil {
> 		return tokenResponse{}, fmt.Errorf("decode token response: %w", err)
> 	}
> 	return token, nil
> }
> ```

## Next step

> [card] [Configure webhooks](https://docs.gnosispay.com/platform-integration/webhook/setup-webhook.md)
>
> Set up an endpoint to receive state changes from Gnosis Pay.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.