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

# Use signed links

> Fix the amount, asset or payout wallet by signing the link on your server, and keep the signing secret there.

By default the widget opens with just your widget ID, and link parameters are prefills the customer can change. Sign the link on your server when the payment details must not change, for example:

* a fixed-price checkout, where the customer must send exactly the amount you quoted;
* a buy where the stablecoins must go to a wallet you chose;
* a widget that should only open from links you issued.

A signed link's parameters are locked, it expires, and nobody can open your widget with their own values. The signing secret is under **Settings → Ramps → Website widget → Signing secret & security**. Keep it on your server; it never belongs in a browser or mobile app.

Pass the signed URL to the script in place of the options object: `Onstacks.open(signedUrl, callbacks)`.

If your integration depends on signed links, turn off **Allow unsigned links** in the same settings section; otherwise a link with the signature removed would still open with its parameters editable. Widgets set up before unsigned links existed have it off already.

## Signing

1. Sort the parameters by name.
2. Join them as `key=value` with `&`, using the raw values (not URL-encoded).
3. HMAC-SHA256 that string with your signing secret.
4. Add the hex digest as `sig` and build the URL: `https://<host>/w/<widget-id>?<params>&sig=<sig>`.

| Environment | Host | Secret |
| - | - | - |
| Sandbox | `https://ramp-sandbox.onstacks.io` | Sandbox signing secret |
| Live | `https://ramp.onstacks.io` | Live signing secret |

```js Node.js theme={"system"}
import crypto from "node:crypto";

export function widgetLink(host, widgetId, params, secret) {
  const canonical = Object.keys(params).sort().map((k) => `${k}=${params[k]}`).join("&");
  const sig = crypto.createHmac("sha256", secret).update(canonical).digest("hex");
  return `${host}/w/${widgetId}?${new URLSearchParams({ ...params, sig })}`;
}

// A 15-minute link that fixes a 100 USDC sale on Base, paid out in NGN
const url = widgetLink("https://ramp-sandbox.onstacks.io", "your-widget-id", {
  exp: Math.floor(Date.now() / 1000) + 15 * 60,
  type: "offramp",
  asset: "USDC",
  chain: "base",
  currency: "NGN",
  amount: "100",
  ref: "order-123",
}, process.env.ONSTACKS_WIDGET_SECRET);
```

## Parameters

| Parameter | Type | Description |
| - | - | - |
| `exp` | Unix seconds | **Required.** When the link stops working. Use a short window, for example 15 minutes. |
| `type` | `onramp` \| `offramp` | `onramp`: the customer buys stablecoins. `offramp`: the customer sells stablecoins. Default `offramp`. |
| `ref` | string | Your order or session reference. Returned in `onCompleted` and `onFailed`. |
| `amount` | string | Fixes the amount the customer sends. Omit to let them choose. |
| `asset` | string | Fixes the stablecoin, for example `USDC`. |
| `chain` | string | Fixes the network, for example `base`. |
| `currency` | string | Fixes the local currency, for example `NGN`. |
| `wallet` | string | Buy only. The customer's wallet address the stablecoins go to. |
| `redirect` | URL | Where to send the customer when they finish a link opened as a full page. Must be on one of your redirect origins. |
| `origin` | URL | The website embedding the widget. Needed only when you allow more than one website. |

Changing any parameter after signing invalidates the link. An expired, unsigned or altered link shows a not-found page, and the script calls `onError` with `expired` when it can tell before opening.


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