> For the complete documentation index, see [llms.txt](https://hinkal-team.gitbook.io/hinkal/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://hinkal-team.gitbook.io/hinkal/hinkal-waas/signing-requests.md).

# Signing Requests

Every request to the WaaS API requires an `X-Stamp` header — an Ed25519 signature over the full request payload. This page shows how to build it.

## How it works

{% stepper %}
{% step %}

## Build an action binding

`"<METHOD> <routePath>"`, e.g. `"POST /waas/create-wallet"` or `"GET /waas/public-balance"`.

`routePath` is the server's route pattern — for the one parameterized route, `GET /waas/scheduled-transaction/{scheduleId}`, the binding is the literal `"GET /waas/scheduled-transaction/:scheduleId"`, **not** the concrete `scheduleId` from the URL. Binding the route into the signed message stops a stamp captured for one endpoint from being replayed against another.
{% endstep %}

{% step %}

## Take all request parameters

Body for `POST`, query params for `GET`.
{% endstep %}

{% step %}

## Add a unique `nonce`

Add a unique `nonce` to the parameters.
{% endstep %}

{% step %}

## Serialize

`JSON.stringify([binding, Object.entries(params)])`
{% endstep %}

{% step %}

## Sign the bytes

Sign the UTF-8 bytes with your Ed25519 private key.
{% endstep %}

{% step %}

## Encode and send

Base64URL-encode the resulting JSON stamp and send it as the `X-Stamp` header.
{% endstep %}
{% endstepper %}

## Building the stamp

```js
const nacl = require("tweetnacl");
const { randomUUID } = require("crypto");

function normalizeRoutePath(path) {
  const withSlash = path.startsWith("/") ? path : `/${path}`;
  return withSlash.length > 1 ? withSlash.replace(/\/+$/, "") : withSlash;
}

// Must match the server's route pattern exactly, e.g. "POST /waas/create-wallet"
function buildActionBinding(method, routePath) {
  return `${method.toUpperCase()} ${normalizeRoutePath(routePath)}`;
}

function buildXStamp(binding, params, keypair) {
  const canonical = JSON.stringify([binding, Object.entries(params)]);
  const sigBytes = nacl.sign.detached(Buffer.from(canonical, "utf8"), keypair.secretKey);
  const stamp = {
    publicKey: Buffer.from(keypair.publicKey).toString("hex"),
    signature: Buffer.from(sigBytes).toString("hex"),
  };
  return Buffer.from(JSON.stringify(stamp)).toString("base64url");
}
```

## Making a signed POST request

```js
async function post(baseUrl, path, body, keypair) {
  const bodyWithNonce = { ...body, nonce: randomUUID() };
  const binding = buildActionBinding("POST", path);
  const xStamp = buildXStamp(binding, bodyWithNonce, keypair);

  const res = await fetch(`${baseUrl}${path}`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Stamp": xStamp,
    },
    body: JSON.stringify(bodyWithNonce),
  });

  return res.json();
}
```

For a static route like `/waas/create-wallet`, `path` doubles as the route pattern, so no extra argument is needed. Every WaaS `POST` route is static — only the `scheduled-transaction` `GET` route is parameterized (see below).

## Making a signed GET request

For `GET` requests, the nonce and all parameters go in the query string — the stamp is built over those same query parameters, bound to the route.

```js
async function get(baseUrl, path, routePath, params, keypair) {
  const paramsWithNonce = { ...params, nonce: randomUUID() };
  const binding = buildActionBinding("GET", routePath);
  const xStamp = buildXStamp(binding, paramsWithNonce, keypair);

  const query = new URLSearchParams(paramsWithNonce).toString();
  const res = await fetch(`${baseUrl}${path}?${query}`, {
    headers: { "X-Stamp": xStamp },
  });

  return res.json();
}
```

`routePath` is usually the same as `path` — e.g. `get(baseUrl, "/waas/private-balance", "/waas/private-balance", params, keypair)`.

The exception is `GET /waas/scheduled-transaction/{scheduleId}`: `path` includes the concrete ID (`` `/waas/scheduled-transaction/${scheduleId}` ``), but `routePath` must stay the literal pattern:

```js
await get(
  baseUrl,
  `/waas/scheduled-transaction/${scheduleId}`,
  "/waas/scheduled-transaction/:scheduleId",
  {},
  keypair,
);
```

## Nonce

Every request must include a unique `nonce`. A UUID works well:

```js
const { randomUUID } = require("crypto");
const nonce = randomUUID();
```

Once a nonce has been used, any request reusing it will be rejected with `401 Nonce already used`.
