> For the complete documentation index, see [llms.txt](https://partner.doc.bunq.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://partner.doc.bunq.com/documentation/bunq-as-a-service/chapter-0-setting-up-api-context/signing.md).

# Signing

Signing proves that a request or response was not changed on the way. You sign the body of some calls with your private key. bunq checks the signature. If the body was changed, the check fails.

## When you need to sign

You must sign **any request that creates or accepts a payment**. Most other calls need no signature. If you forget to sign such a request, the API returns [error 466](https://doc.bunq.com/basics/errors).

bunq also signs its responses. You can verify a response with the public key you got during [installation](https://doc.bunq.com/tutorials/your-first-payment/creating-the-api-context). See [Verify a bunq response](#verify-a-bunq-response) below.

{% hint style="warning" %}
**The one rule that causes almost every 466: sign the exact bytes you send.**

Sign the request body as the exact byte string that goes on the wire — same spaces, same order, no added or removed line breaks. Many JSON libraries re-format a body for readability. If the sent bytes differ from the signed bytes by one character, the signature is invalid.
{% endhint %}

## The algorithm

| Item           | Value                                      |
| -------------- | ------------------------------------------ |
| Hash           | SHA-256                                    |
| Signature      | RSA, PKCS#1 v1.5 padding                   |
| Encoding       | Base64                                     |
| Signed content | The request body only. No headers. No URL. |
| Header         | `X-Bunq-Client-Signature`                  |
| Key to sign    | Your installation private key              |

## Sign a request

You sign the body, base64-encode the result, and add it as the `X-Bunq-Client-Signature` header.

{% tabs %}
{% tab title="PHP" %}

```php
// $body is the exact request body string you send.
// $privateKey is your installation private key.
openssl_sign($body, $signature, $privateKey, OPENSSL_ALGO_SHA256);
$header = base64_encode($signature);

// Add $header to the request as X-Bunq-Client-Signature.
```

{% endtab %}

{% tab title="Python" %}

```python
import base64
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding

# body: the exact request bytes you send.
# private_key_pem: your installation private key, PEM bytes.
private_key = serialization.load_pem_private_key(private_key_pem, password=None)
signature = private_key.sign(body, padding.PKCS1v15(), hashes.SHA256())
header = base64.b64encode(signature).decode()

# Add header to the request as X-Bunq-Client-Signature.
```

{% endtab %}

{% tab title="Node.js" %}

```javascript
const crypto = require('crypto');

// body: the exact request bytes you send.
// privateKeyPem: your installation private key, PEM string.
const signer = crypto.createSign('RSA-SHA256');
signer.update(body);
signer.end();
const header = signer.sign(privateKeyPem, 'base64');

// Add header to the request as X-Bunq-Client-Signature.
```

{% endtab %}

{% tab title="Java" %}

```java
import java.security.Signature;
import java.util.Base64;

// privateKey: your installation private key.
// body: the exact request bytes you send.
Signature signer = Signature.getInstance("SHA256withRSA");
signer.initSign(privateKey);
signer.update(body);
String header = Base64.getEncoder().encodeToString(signer.sign());

// Add header to the request as X-Bunq-Client-Signature.
```

{% endtab %}
{% endtabs %}

The request then carries the header. The `--data-raw` bytes below are the exact bytes that were signed.

```bash
curl --location 'https://partner-api.sandbox.bunq.com/v1/user/126/monetary-account/222/payment' \
  --header 'X-Bunq-Client-Signature: lLKQnlnC8yVENb8RlnJTitBkXa2S+WBekR72Okz5KRmm5EC3/JsQ83Xaz+5gwumvSxrkf0DcqrShQGIYvBnW7Vjvak5DTt5opybpOstdVGswbVKnSRAURgYYBZrIxbol9ioVRX9ztumEmv0elDZiNwkP1PqKdEUTPGfkX4fmsZU0pRmlK5ceIXJgeghnaHvsmD69BOWDJpycFskEFqqlDTy8edC+NlZI3b0IoSP+BHLI7AunDgD2HctrM2Z0lAObBThwqoUgPmrqxJqXAHIURpa+wiT3JqGCItOEXkkw8ghGA1i84Ud5u3soRcFe0+GP/IZoVWssDMEmro3vR+xfhQ==' \
  --header '[other headers]' \
  --data-raw '{"amount":{"value":"12.50","currency":"EUR"},"counterparty_alias":{"type":"EMAIL","value":"bravo@bunq.com"},"description":"Payment for drinks."}'
```

## Test vector

Use this to check your code offline, before any live call. Sign the body below with the private key below. Your output must match the expected signature exactly. If it does not, your code has a bug — check the padding, the hash, whitespace in the body, and the base64 step.

{% hint style="info" %}
This key pair is for testing only. It is not a real bunq key. Never use it in production.
{% endhint %}

**Private key**

```
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDOaE3eoA/LoCvt
NZaIQ/wV5PfKAa2jNjylP3ZuXvYfM7hxTNcdkDruL1v3hUysuAsntVuZjt5DhBfB
eE2HWzA/j0HQXEcNGetVEOCZzbQxKaSzwDtKPOFiJ42bJyTu3NsC6D9nHv+Q+c+M
sh4tOCEAMQJmHXmIUR8hkoKZVsSCH09T3KIvbLNDByOHZi3IqHRmek1T0CkEgm4K
DssYZNUc87J52d5/EQIq8j4cJR7OZ7duRKhchWmTvOkhJBDwk3WS3nJ+MRD3SjTo
ia5VEy7ro8C0bTlJ3VT9We/OuQQQc41rLLePtEQbh+dzdfBZnuk5AcUWeOsEktuY
1HGudN+FAgMBAAECggEACzJ62PMncDUz9NYVoLZJcFmfvJhrNkGsuA0xjErAANDS
NmlD+TFYFRBCJbMYRf1k84ZB3hpzQcfibwcq84cVnrkTia1b2GDZ1J9KA+X2Gr2m
1u58eOpFEslRBAPzW7bk9asqN1ql5BQ9N2V5QBw9pBBEkEwUifnWncBgL9fWaLWX
qzMuSQN9cXkEksaNL0VwN2YyMXpTTQk1Qm28EkHLsb3ksnn0eh33TYO+z7onBoqf
c0mh9aezY2wa/YVzjyqtVVFakiY9FY1WE8RESYXoFl3Fp2mLeGzyvuRHtSvzZduP
oKMtgJCq1C3bfk7Qc+/zRds5GH2qwGEJc1HblkNNIQKBgQDpw4YvNnbKm0dib9iy
HKXWmL+cVR+pNFYBLLrKQXzGBW4D8G3QaM6sEHf/cl8bz2wXqJyRfOif14ImCSbp
pCUR8++K4morGOEHE8EfWKz1XDXbxEvh+YpzRRS4U9/Q6QbF4MpTEYbTzxelceeR
LK/bB2TAdCAtDmwGSf+neMQ+ZQKBgQDiCp1ywq8hPJ/n/X7VK51Ewv0XqcZZ4Drz
AJNZySUWc6pyAcmYK+/lIIpi/gX4k8FIHGsHPtQ/ZwvyREJGDgC5Rcat5TNyzMsG
sDvIVgPbP57bZuPfzbp34doi6Fcm0Zi0TPWG8seA6DXiZ+1B1TggaEQvFZwRgN5s
3484en/6oQKBgExp5MAkLpq2PZR4+T3nzXIjxmblEI9wEzkZFQoudAoLFlZlTpBU
4lIuo/s1uM9Pc4ccT+njCevON7YVQcoYJSIEOmaM8qWZ4V094OBNTWR7OwPW2/NH
bjGWAFrs1/njYppmi9yxmwA77v/fBxJWqDmR5iZfD3GLCFbZ858cBBJdAoGBALWM
awsKPJjIVXKB0oWqIwVgSuDflhUHmLElzDN375bJzaMFtoHHAEWaTivV9LvDCKE/
WeMW3mgR/rFVi5kyk/g6h6mRhjH1NDeqAkoUc8J7pb54NBvD47l3p7kTvTPLcgeW
LDl38y6g0u6GJDQU6aB1vhXHGHOieoMdR63IDdQhAoGAS6JZeEtcEyLYsaQsFKfa
76idVr+qkM3Np53rGRTh2jw019Yw1KHAtyCRkblbFQ4m/8PTeiN9DSxi6Gpy+jsJ
ieFJ91T/o00/+Mdftr6N5J/b34J5EvXUPhd5pnmQCPiWdt3nC8OEI4CztrVA1xSG
jdfd4o1vwQHR4E6l/bH9rKs=
-----END PRIVATE KEY-----
```

**Body to sign** (144 bytes, one line, no trailing line break)

```json
{"amount":{"value":"12.50","currency":"EUR"},"counterparty_alias":{"type":"EMAIL","value":"bravo@bunq.com"},"description":"Payment for drinks."}
```

**Expected signature**

```
lLKQnlnC8yVENb8RlnJTitBkXa2S+WBekR72Okz5KRmm5EC3/JsQ83Xaz+5gwumvSxrkf0DcqrShQGIYvBnW7Vjvak5DTt5opybpOstdVGswbVKnSRAURgYYBZrIxbol9ioVRX9ztumEmv0elDZiNwkP1PqKdEUTPGfkX4fmsZU0pRmlK5ceIXJgeghnaHvsmD69BOWDJpycFskEFqqlDTy8edC+NlZI3b0IoSP+BHLI7AunDgD2HctrM2Z0lAObBThwqoUgPmrqxJqXAHIURpa+wiT3JqGCItOEXkkw8ghGA1i84Ud5u3soRcFe0+GP/IZoVWssDMEmro3vR+xfhQ==
```

## Verify a bunq response

You can check that a response really came from bunq. Verify the response body against the `X-Bunq-Server-Signature` header with bunq's public key.

{% tabs %}
{% tab title="PHP" %}

```php
// $responseBody is the exact response body bunq sent.
// $publicKey is the bunq public key from installation.
$valid = openssl_verify($responseBody, base64_decode($header), $publicKey, OPENSSL_ALGO_SHA256) === 1;
```

{% endtab %}

{% tab title="Python" %}

```python
from cryptography.exceptions import InvalidSignature

public_key = serialization.load_pem_public_key(public_key_pem)
try:
    public_key.verify(base64.b64decode(header), response_body,
                      padding.PKCS1v15(), hashes.SHA256())
    valid = True
except InvalidSignature:
    valid = False
```

{% endtab %}

{% tab title="Node.js" %}

```javascript
const verifier = crypto.createVerify('RSA-SHA256');
verifier.update(responseBody);
verifier.end();
const valid = verifier.verify(publicKeyPem, header, 'base64');
```

{% endtab %}

{% tab title="Java" %}

```java
Signature verifier = Signature.getInstance("SHA256withRSA");
verifier.initVerify(publicKey);
verifier.update(responseBody);
boolean valid = verifier.verify(Base64.getDecoder().decode(header));
```

{% endtab %}
{% endtabs %}

## Troubleshooting error 466

Error 466 means the signature is invalid. Check each item:

* You have a key pair. You sign with the private key of that pair.
* You use SHA-256 with RSA and PKCS#1 v1.5 padding.
* You sign **only** the request body. No headers. No URL.
* The signed bytes equal the sent bytes, character for character. No added spaces. No trailing line break. No library re-formatting.
* You base64-encode the signature before you add it to the header.
* You add the value under `X-Bunq-Client-Signature`.
