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

# V1 signing (legacy)

> Sign requests with the API-Nonce header, for existing API keys that use V1 signing.

> [!WARNING]
>
> This page describes V1 signing, which sends an `API-Nonce` header. It's for existing integrations whose API keys use
> **V1 (nonce)** signing. For new integrations, use [V2
> signing](https://docs.services.payward.com/tabs/developer-documentation/get-started/authentication.md) instead. To move an existing integration to V2, see
> [Which version does my key
> use?](https://docs.services.payward.com/tabs/developer-documentation/get-started/authentication.md#which-version-does-my-key-use).

## Authentication parameters

V1 signing uses HMAC. Every request signed with V1 must include the following headers:

* `API-Key` HTTP header parameter: your public API key.
* `API-Nonce` HTTP header parameter: monotonically increasing integer nonce.
* `API-Sign` HTTP header parameter: HMAC-SHA512 signature of the request.

## Setting the `API-Key` parameter

The value for the `API-Key` HTTP header parameter is your public API key. Create new keys for V2 signing instead, as described in [Authentication](https://docs.services.payward.com/tabs/developer-documentation/get-started/authentication.md#setting-the-api-key-parameter).

## Setting the `API-Nonce` parameter

Set `API-Nonce` to an integer greater than the last value you used with the same API key.

Use a high-resolution timestamp, such as nanoseconds since the Unix epoch, unless your integration already has a stronger monotonic counter.

Don't use a timer with an arbitrary origin, such as `process.hrtime()` in Node.js. It resets when the machine restarts, so your next nonce can be lower than the last one you sent.

## Setting the `API-Sign` parameter

The value for the `API-Sign` HTTP header parameter is an HMAC-SHA512 signature of the request, base64-encoded.

```text theme={null}
base64(HMAC-SHA512(secret, path_with_query + SHA256(nonce + body)))
```

### Algorithm steps

1. **Choose a nonce:** generate a monotonically increasing integer.
2. **Build the nonce payload:** concatenate the nonce and the exact request body bytes.
   * For requests without a body: use the nonce only.
   * For requests with a body: use the exact bytes you send on the wire.
3. **Hash the nonce payload:** generate a SHA-256 digest.
4. **Build the message:** concatenate the URL path, including query string, and the SHA-256 digest.
5. **Sign:** generate HMAC-SHA512 over the message using your base64-decoded secret.
6. **Encode:** base64-encode the signature.

### Examples

The following code snippets demonstrate how to generate the signature in Python and JavaScript.

```python Python theme={null}
import os
import hashlib
import hmac
import base64
import time
import urllib.parse


def get_payward_signature(urlpath, body, secret, nonce, params=None):
    """
    Generate Payward Services API signature.

    Args:
        urlpath: API endpoint including query string (e.g., '/v1/webhooks')
        body: Request body string, or None for requests without a body
        secret: Base64-encoded API secret
        nonce: Monotonically increasing integer nonce
        params: Optional query parameters

    Returns:
        Base64-encoded signature string
    """
    nonce_payload = str(nonce).encode("utf-8") if body is None else (str(nonce) + body).encode("utf-8")

    sign_path = urlpath
    if params:
        sign_path += "?" + urllib.parse.urlencode(params)

    message = sign_path.encode("utf-8") + hashlib.sha256(nonce_payload).digest()
    mac = hmac.new(base64.b64decode(secret), message, hashlib.sha512)
    return base64.b64encode(mac.digest()).decode()


last_nonce = 0


# Returns nanoseconds since the Unix epoch. time.time_ns() can repeat a value
# or step back with the system clock, so if it hasn't advanced since the last
# call, next_nonce() returns the last nonce plus 1.
def next_nonce():
    global last_nonce
    last_nonce = max(time.time_ns(), last_nonce + 1)
    return last_nonce


api_secret = os.environ["PWS_API_SECRET"]
endpoint = "/v1/webhooks"
nonce = next_nonce()

signature = get_payward_signature(endpoint, None, api_secret, nonce)
print(f"API-Nonce: {nonce}")
print(f"API-Sign: {signature}")
```

```javascript JavaScript theme={null}
import crypto from 'crypto';

/**
 * Generate Payward Services API signature.
 * @param {string} urlpath - API endpoint including query string (e.g., '/v1/webhooks')
 * @param {string|null} body - Request body string, or null for requests without a body
 * @param {string} secret - Base64-encoded API secret
 * @param {string|number|bigint} nonce - Monotonically increasing integer nonce
 * @param {object|null} params - Optional query parameters
 * @returns {string} Base64-encoded signature
 */
function getPaywardSignature(urlpath, body, secret, nonce, params = null) {
  const noncePayload = body === null ? String(nonce) : String(nonce) + body;

  let signPath = urlpath;
  if (params) {
    signPath += '?' + new URLSearchParams(params).toString();
  }

  const sha256Hash = crypto.createHash('sha256').update(noncePayload).digest();
  const message = Buffer.concat([Buffer.from(signPath), sha256Hash]);
  const secretBuffer = Buffer.from(secret, 'base64');
  const hmac = crypto.createHmac('sha512', secretBuffer);
  hmac.update(message);

  return hmac.digest('base64');
}

let lastNonce = 0n;

// Returns nanoseconds since the Unix epoch. Date.now() has only millisecond
// resolution, so if the clock hasn't advanced since the last call, or has
// stepped back, nextNonce() returns the last nonce plus 1.
function nextNonce() {
  const now = BigInt(Date.now()) * 1_000_000n;
  lastNonce = now > lastNonce ? now : lastNonce + 1n;
  return lastNonce.toString();
}

const apiSecret = process.env.PWS_API_SECRET;
const endpoint = '/v1/webhooks';
const nonce = nextNonce();

const signature = getPaywardSignature(endpoint, null, apiSecret, nonce);
console.log(`API-Nonce: ${nonce}`);
console.log(`API-Sign: ${signature}`);
```

## Complete request example

Here's a complete example making an authenticated `GET` request to list your webhooks. It reads your credentials from `PWS_API_KEY` and `PWS_API_SECRET`, and the API base URL from `PWS_BASE_URL`. Set `PWS_BASE_URL` to the sandbox or production base URL, listed in [Environments](https://docs.services.payward.com/tabs/developer-documentation/testing/overview.md#environments).

```python Python theme={null}
import os
import time
import hashlib
import hmac
import base64
import requests

API_KEY = os.environ.get("PWS_API_KEY")
API_SECRET = os.environ.get("PWS_API_SECRET")
BASE_URL = os.environ["PWS_BASE_URL"]


def get_payward_signature(urlpath, body, secret, nonce):
    nonce_payload = str(nonce).encode("utf-8") if body is None else (str(nonce) + body).encode("utf-8")
    message = urlpath.encode("utf-8") + hashlib.sha256(nonce_payload).digest()
    mac = hmac.new(base64.b64decode(secret), message, hashlib.sha512)
    return base64.b64encode(mac.digest()).decode()


last_nonce = 0


# Returns nanoseconds since the Unix epoch. time.time_ns() can repeat a value
# or step back with the system clock, so if it hasn't advanced since the last
# call, next_nonce() returns the last nonce plus 1.
def next_nonce():
    global last_nonce
    last_nonce = max(time.time_ns(), last_nonce + 1)
    return last_nonce


def list_webhooks():
    endpoint = "/v1/webhooks"
    nonce = next_nonce()
    signature = get_payward_signature(endpoint, None, API_SECRET, nonce)

    headers = {
        "API-Key": API_KEY,
        "API-Nonce": str(nonce),
        "API-Sign": signature,
    }

    response = requests.get(BASE_URL + endpoint, headers=headers)
    return response.json()


webhooks = list_webhooks()
print(webhooks)
```

```javascript JavaScript theme={null}
import crypto from 'crypto';

const API_KEY = process.env.PWS_API_KEY;
const API_SECRET = process.env.PWS_API_SECRET;
const BASE_URL = process.env.PWS_BASE_URL;

function getPaywardSignature(urlpath, body, secret, nonce) {
  const noncePayload = body === null ? String(nonce) : String(nonce) + body;
  const sha256Hash = crypto.createHash('sha256').update(noncePayload).digest();
  const message = Buffer.concat([Buffer.from(urlpath), sha256Hash]);

  const secretBuffer = Buffer.from(secret, 'base64');
  const hmac = crypto.createHmac('sha512', secretBuffer);
  hmac.update(message);

  return hmac.digest('base64');
}

let lastNonce = 0n;

// Returns nanoseconds since the Unix epoch. Date.now() has only millisecond
// resolution, so if the clock hasn't advanced since the last call, or has
// stepped back, nextNonce() returns the last nonce plus 1.
function nextNonce() {
  const now = BigInt(Date.now()) * 1_000_000n;
  lastNonce = now > lastNonce ? now : lastNonce + 1n;
  return lastNonce.toString();
}

async function listWebhooks() {
  const endpoint = '/v1/webhooks';
  const nonce = nextNonce();
  const signature = getPaywardSignature(endpoint, null, API_SECRET, nonce);

  const response = await fetch(BASE_URL + endpoint, {
    method: 'GET',
    headers: {
      'API-Key': API_KEY,
      'API-Nonce': nonce,
      'API-Sign': signature,
    },
  });

  return response.json();
}

const webhooks = await listWebhooks();
console.log(webhooks);
```

If you haven't registered any webhooks yet, the response is an empty list:

```json theme={null}
{ "data": [] }
```

An empty list still means your request was authenticated. If the key or signature is wrong, you get `401 Unauthorized` instead.

## Query parameters in signature

When your request includes query parameters, they must be included in the URL path used for signature generation. Use the exact same path string in both the signature and the request URL.

The examples in this section and the next one call account-scoped endpoints. They read the account ID from `PWS_ACCOUNT_ID`. To find it, see [Find your account ID](https://docs.services.payward.com/tabs/developer-documentation/testing/setup.md#find-your-account-id). A user ID won't work in its place. See [Users, accounts, and ownership](https://docs.services.payward.com/tabs/concepts/users-and-accounts.md#user-vs-account).

```python Python theme={null}
import urllib.parse

account_id = os.environ["PWS_ACCOUNT_ID"]
# Pass booleans as lowercase strings: urlencode() would turn True into "True".
params = {"quote_symbol": "EUR", "with_zero_balances": "true"}
query_string = urllib.parse.urlencode(params)
signature_path = f"/v1/accounts/{account_id}/portfolio?{query_string}"
nonce = next_nonce()

signature = get_payward_signature(signature_path, None, API_SECRET, nonce)
```

```javascript JavaScript theme={null}
const accountId = process.env.PWS_ACCOUNT_ID;
const params = { quote_symbol: 'EUR', with_zero_balances: true };
const queryString = new URLSearchParams(params).toString();
const signaturePath = `/v1/accounts/${accountId}/portfolio?${queryString}`;
const nonce = nextNonce();

const signature = getPaywardSignature(signaturePath, null, API_SECRET, nonce);
```

## Request body in signature

For requests that send a body (`POST`, `PUT`), the body bytes used to compute the signature must match the bytes sent on the wire exactly. Differences in whitespace, key ordering, or encoding will produce a signature mismatch.

The recommended pattern is to serialize the body once and reuse the same string for both signing and the HTTP request:

```python Python theme={null}
import json

account_id = os.environ["PWS_ACCOUNT_ID"]
endpoint = f"/v1/accounts/{account_id}/quotes"
body = json.dumps(
    {
        "from": {"symbol": "USD", "type": "fiat", "amount": "100.00"},
        "to": {"symbol": "BTC", "type": "crypto"},
    }
)
nonce = next_nonce()

signature = get_payward_signature(endpoint, body, API_SECRET, nonce)

# Send the same string with data=. Passing json= would serialize the body again.
requests.post(
    BASE_URL + endpoint,
    headers={
        "API-Key": API_KEY,
        "API-Nonce": str(nonce),
        "API-Sign": signature,
        "Content-Type": "application/json",
    },
    data=body,
)
```

```javascript JavaScript theme={null}
const accountId = process.env.PWS_ACCOUNT_ID;
const endpoint = `/v1/accounts/${accountId}/quotes`;
const body = JSON.stringify({
  from: { symbol: 'USD', type: 'fiat', amount: '100.00' },
  to: { symbol: 'BTC', type: 'crypto' },
});
const nonce = nextNonce();

const signature = getPaywardSignature(endpoint, body, API_SECRET, nonce);

await fetch(BASE_URL + endpoint, {
  method: 'POST',
  headers: {
    'API-Key': API_KEY,
    'API-Nonce': nonce,
    'API-Sign': signature,
    'Content-Type': 'application/json',
  },
  body,
});
```

## Troubleshooting

| Error | Cause | Solution |
| - | - | - |
| `Invalid signature` | Signature doesn't match | Verify secret encoding, nonce, path, and body bytes |
| `Missing API-Key` | Header not set | Check the header name is exactly `API-Key` |
| `Invalid nonce` | Nonce was reused | Generate a larger nonce for every request |
