> For the complete documentation index, see [llms.txt](https://docs.musepay.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.musepay.io/integration/authentication.md).

# Authentication

Configure RSA credentials and sign requests to the MusePay API.

MusePay authenticates API requests with RSA signatures. You generate and retain the private key, then upload the corresponding public key to MusePay. MusePay never needs access to your private key.

## Generate an RSA key pair

Generate a 2048-bit RSA private key in PKCS #8 PEM format, then export its public key:

```bash
openssl genpkey -algorithm RSA -out muse_secret.key -pkeyopt rsa_keygen_bits:2048
openssl pkey -in muse_secret.key -pubout -out muse_public.key
```

A private key file has the following format. The middle of this example is masked and is not a usable key:

```
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSj...REDACTED...AQAB
-----END PRIVATE KEY-----
```

{% hint style="danger" %}
Store `muse_secret.key` securely. Never upload it, send it to MusePay, commit it to source control, print it in logs, or embed it in client-side code.
{% endhint %}

Open the [Partner Portal](https://partner.musepay.io), go to the **API Key** tab, and configure your partner public key by uploading the contents of `muse_public.key`. MusePay uses this key to verify your API request signatures.

Before uploading, remove the `-----BEGIN PUBLIC KEY-----` and `-----END PUBLIC KEY-----` lines, then remove all line breaks. Upload only the remaining Base64-encoded public key as a single line.

<figure><img src="https://4031060132-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F847OCU8wHOemogNwwSWc%2Fuploads%2Fgit-blob-7e55746d6e616e348aeffd754916b811abe1f802%2Fimage%20(15).png?alt=media" alt="Partner Portal API Key page showing the Partner ID and public-key configuration"><figcaption><p>Configure your public key and download the MusePay public key in the Partner Portal.</p></figcaption></figure>

Download and store the MusePay public key as well. You use the MusePay public key—not your partner public key—to verify signed [webhook notifications](/integration/webhook.md).

## Required authentication fields

Include the following fields in every API request body. Endpoint-specific fields must be included in the same body before the signature is generated.

| Name         | Type   | Description                                                                                            |
| ------------ | ------ | ------------------------------------------------------------------------------------------------------ |
| `partner_id` | String | Partner ID shown in the Partner Portal.                                                                |
| `sign_type`  | String | Fixed value: `RSA`.                                                                                    |
| `timestamp`  | String | Current 10-digit Unix timestamp in seconds.                                                            |
| `nonce`      | String | Unique random value for this request. Do not reuse a nonce; duplicates within 60 seconds are rejected. |
| `sign`       | String | Base64-encoded RSA signature generated as described below.                                             |

See [Common Parameters](/reference/api-reference/common-parameters.md) for the shared request-field reference.

## Build the signature

Use the exact request values that you will send to MusePay:

1. Remove the `sign` field.
2. Exclude fields whose values are `null` or an empty string.
3. Sort the remaining fields by field name in ascending, case-sensitive alphabetical order.
4. Format each field as `key=value` and join the pairs with `&`. Do not URL-encode this canonical string.
5. Encode the canonical string as UTF-8 and sign it with your RSA private key using `SHA1WithRSA`.
6. Base64-encode the generated signature and send it as `sign`.

{% hint style="warning" %}
`SHA1WithRSA` is the algorithm required by the current MusePay API. Do not substitute a different signature algorithm unless MusePay confirms that your integration has been migrated.
{% endhint %}

### Example payload

The unsigned request body:

```json
{
  "partner_id": "200001",
  "request_id": "202609110001",
  "sign_type": "RSA",
  "timestamp": "1789056000",
  "nonce": "5K8264ILTKCH16CQ2502SI8ZNMTM67VS"
}
```

The canonical string is:

```
nonce=5K8264ILTKCH16CQ2502SI8ZNMTM67VS&partner_id=200001&request_id=202609110001&sign_type=RSA&timestamp=1789056000
```

After signing, add the Base64-encoded result to the request body:

```json
{
  "partner_id": "200001",
  "request_id": "202609110001",
  "sign_type": "RSA",
  "timestamp": "1789056000",
  "nonce": "5K8264ILTKCH16CQ2502SI8ZNMTM67VS",
  "sign": "<Base64-encoded RSA signature>"
}
```

## Code examples

The examples below read the private key from `muse_secret.key`. Keep this file outside your source repository and restrict access to it.

<details>

<summary>Java</summary>

```java
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.Instant;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;

static String canonicalize(Map<String, ?> params) {
    return params.entrySet().stream()
        .filter(entry -> !"sign".equals(entry.getKey()))
        .filter(entry -> entry.getValue() != null)
        .filter(entry -> !String.valueOf(entry.getValue()).isEmpty())
        .sorted(Map.Entry.comparingByKey())
        .map(entry -> entry.getKey() + "=" + entry.getValue())
        .collect(Collectors.joining("&"));
}

static PrivateKey loadPrivateKey(Path path) throws Exception {
    String pem = Files.readString(path, StandardCharsets.UTF_8)
        .replace("-----BEGIN PRIVATE KEY-----", "")
        .replace("-----END PRIVATE KEY-----", "")
        .replaceAll("\\s", "");
    byte[] keyBytes = Base64.getDecoder().decode(pem);
    return KeyFactory.getInstance("RSA")
        .generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
}

static String sign(String content, PrivateKey privateKey) throws Exception {
    Signature signer = Signature.getInstance("SHA1WithRSA");
    signer.initSign(privateKey);
    signer.update(content.getBytes(StandardCharsets.UTF_8));
    return Base64.getEncoder().encodeToString(signer.sign());
}

Map<String, Object> requestBody = new HashMap<>();
requestBody.put("partner_id", "200001");
requestBody.put("request_id", "202609110001");
requestBody.put("sign_type", "RSA");
requestBody.put("timestamp", String.valueOf(Instant.now().getEpochSecond()));
requestBody.put("nonce", UUID.randomUUID().toString().replace("-", ""));

PrivateKey privateKey = loadPrivateKey(Path.of("muse_secret.key"));
requestBody.put("sign", sign(canonicalize(requestBody), privateKey));
```

</details>

<details>

<summary>JavaScript (Node.js)</summary>

```javascript
import { readFileSync } from 'node:fs'
import { randomUUID, sign } from 'node:crypto'

function canonicalize(params) {
  return Object.entries(params)
    .filter(([key, value]) => key !== 'sign' && value !== null && value !== undefined && value !== '')
    .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
    .map(([key, value]) => `${key}=${value}`)
    .join('&')
}

const requestBody = {
  partner_id: '200001',
  request_id: '202609110001',
  sign_type: 'RSA',
  timestamp: Math.floor(Date.now() / 1000).toString(),
  nonce: randomUUID().replaceAll('-', '')
}

const privateKey = readFileSync('muse_secret.key', 'utf8')
const content = canonicalize(requestBody)
requestBody.sign = sign('RSA-SHA1', Buffer.from(content, 'utf8'), privateKey).toString('base64')
```

</details>

<details>

<summary>PHP</summary>

```php
<?php
$requestBody = [
    "partner_id" => "200001",
    "request_id" => "202609110001",
    "sign_type" => "RSA",
    "timestamp" => (string) time(),
    "nonce" => bin2hex(random_bytes(16))
];

$signingFields = array_filter(
    $requestBody,
    fn($value, $key) => $key !== "sign" && $value !== null && $value !== "",
    ARRAY_FILTER_USE_BOTH
);
ksort($signingFields, SORT_STRING);

$pairs = [];
foreach ($signingFields as $key => $value) {
    $pairs[] = $key . "=" . $value;
}
$content = implode("&", $pairs);

$privateKeyPem = file_get_contents("muse_secret.key");
if ($privateKeyPem === false) {
    throw new RuntimeException("Unable to read the private key");
}

if (!openssl_sign($content, $signature, $privateKeyPem, OPENSSL_ALGO_SHA1)) {
    throw new RuntimeException("Unable to sign the request");
}

$requestBody["sign"] = base64_encode($signature);
```

</details>

## Troubleshooting signature errors

If MusePay rejects a signature, verify that:

* the public key uploaded in the Partner Portal matches the private key used to sign the request;
* the canonical string contains every non-empty request field except `sign`;
* field names and values exactly match the final request body;
* fields are sorted by name and are not URL-encoded;
* the content is UTF-8 encoded and signed with `SHA1WithRSA`;
* `timestamp` is a 10-digit Unix timestamp in seconds; and
* a new `nonce` is generated for every request.
