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

# Build Your Signer Endpoint

> Create the server-side endpoint that validates and signs payment requests.

The signer is a **server-side** HTTP endpoint that receives a `SignerRequest` from the Deposit SDK, creates a signed payment link, and returns a `SignerResponse`. This is the core security mechanism. It ensures every payment was authorized by your server.

## Request format

The Deposit SDK sends a `POST` request with this JSON body to your signer URL:

| Field            | Type                      | Description                                                                                                                    |
| ---------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `amount`         | `number`                  | USD amount to deposit. Always > 0.                                                                                             |
| `chainId`        | `number`                  | Destination chain ID (for example, `8453` for Base, `42161` for Arbitrum, or `792703809` for Solana).                          |
| `address`        | `string`                  | Destination wallet address. In the web SDK this can be an EVM address or a Solana address, depending on the destination chain. |
| `token`          | `string`                  | Destination token identifier for that chain. Use an EVM token contract address on EVM chains or an SPL mint address on Solana. |
| `callbackScheme` | `string \| null`          | Always `null` for browser integrations. Reserved for native app deep-link callbacks.                                           |
| `url`            | `string`                  | Base webview URL the SDK will navigate to. Provided for logging/validation only. Your signer does not construct the final URL. |
| `version`        | `string`                  | Protocol version (currently `"v1"`).                                                                                           |
| `reference`      | `string?`                 | Optional merchant order or invoice ID for reconciliation.                                                                      |
| `metadata`       | `Record<string, string>?` | Optional arbitrary key-value pairs for your records.                                                                           |

Example request body:

```json theme={null}
{
  "amount": 50,
  "chainId": 8453,
  "address": "0x1a5FdBc891c5D4E6aD68064Ae45D43146D4F9f3a",
  "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  "callbackScheme": null,
  "url": "https://pay.blink.cash",
  "version": "v1",
  "reference": "order-123",
  "metadata": { "invoiceId": "INV-456" }
}
```

## Implementation steps

Your endpoint must perform these steps in order:

### 1. Validate the request

| Field            | Validation Rule                                                                                                                                   |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `amount`         | `Number.isFinite(amount) && amount > 0`                                                                                                           |
| `chainId`        | `Number.isInteger(chainId) && chainId > 0`                                                                                                        |
| `address`        | Validate based on the requested destination chain. For the web SDK, accept EVM addresses on EVM chains and Solana Base58 addresses on Solana.     |
| `token`          | Validate based on the requested destination chain. For the web SDK, accept EVM contract addresses on EVM chains and SPL mint addresses on Solana. |
| `callbackScheme` | `null` or matches `/^[a-zA-Z][a-zA-Z0-9+\-.]*$/`                                                                                                  |
| `version`        | Non-empty string (default to `"v1"` if missing)                                                                                                   |

Return HTTP 400 with an error message for any validation failure.

<Info>
  Your signer should allowlist only Blink-supported destination `chainId` and `token` combinations. See [Supported Networks and Wallets](/integration/supported-networks-and-wallets) for the current routing catalog.
</Info>

### 2. Verify destination ownership

Before signing, confirm that the authenticated user actually controls the destination `address`. Without this check, a malicious caller could submit someone else's wallet address and direct funds to an account they don't own.

Your signer should look up the user's wallets via your auth provider and verify the requested `address` is among them. Here is an example using [Privy](https://docs.privy.io) as the auth provider:

```javascript theme={null}
const { PrivyClient } = require('@privy-io/server-auth');

const privy = new PrivyClient(process.env.PRIVY_APP_ID, process.env.PRIVY_APP_SECRET);

async function verifyDestinationOwnership(authToken, requestedAddress) {
  const { userId } = await privy.verifyAuthToken(authToken);
  const user = await privy.getUser(userId);

  const userWallets = user.linkedAccounts
    .filter((a) => a.type === 'wallet')
    .map((a) => a.address.toLowerCase());

  if (!userWallets.includes(requestedAddress.toLowerCase())) {
    return false;
  }
  return true;
}
```

In your route handler, call this before proceeding to signing:

```javascript theme={null}
const authToken = req.headers.authorization?.replace('Bearer ', '');
const ownsAddress = await verifyDestinationOwnership(authToken, req.body.address);
if (!ownsAddress) {
  return res.status(403).json({ error: 'Destination address does not belong to the authenticated user.' });
}
```

If you use a different auth provider, the principle is the same: resolve the caller's identity, fetch their linked wallets, and reject the request if the destination address is not among them.

### 3. Generate an idempotency key

Generate a UUID v4 for this payment request. This prevents duplicate transfers if the user retries.

```javascript theme={null}
const { randomUUID } = require('node:crypto');
const idempotencyKey = randomUUID();
```

### 4. Record the signature timestamp

Record the current time as the signature timestamp. Swype enforces a maximum signature age of 15 minutes server-side, so you do not need to manage expiration TTLs yourself.

```javascript theme={null}
const signatureTimestamp = new Date().toISOString();
```

### 5. Build the payload JSON

```javascript theme={null}
const payloadObject = {
  amount: request.amount,
  chainId: request.chainId,
  address: request.address,
  token: request.token,
  idempotencyKey,
  callbackScheme: request.callbackScheme,
  signatureTimestamp,
  version: request.version,
};
```

### 6. Base64url-encode the payload

Convert the JSON string to a base64url-encoded string. Base64url uses `-` instead of `+`, `_` instead of `/`, and no padding.

```javascript theme={null}
const payload = Buffer.from(JSON.stringify(payloadObject), 'utf8').toString('base64url');
```

### 6. Sign the payload

Sign the **encoded payload string** (not the raw JSON) using ECDSA P-256 with SHA-256, then base64url-encode the signature:

```javascript theme={null}
const { createSign } = require('node:crypto');

const signer = createSign('SHA256');
signer.update(payload);
signer.end();
const signature = signer.sign(privateKeyPem).toString('base64url');
```

The input to the signing function is the base64url-encoded payload string (ASCII bytes). The output is the raw DER-encoded ECDSA signature, which is then base64url-encoded.

### 8. Return the response

## Response format

| Field                    | Type     | Description                                              |
| ------------------------ | -------- | -------------------------------------------------------- |
| `merchantId`             | `string` | Your merchant UUID (from Blink registration).            |
| `payload`                | `string` | Base64url-encoded payment payload.                       |
| `signature`              | `string` | Base64url-encoded ECDSA signature of the payload string. |
| `preview`                | `object` | Echo of payment parameters for client-side display.      |
| `preview.amount`         | `number` | Deposit amount.                                          |
| `preview.chainId`        | `number` | Destination chain ID.                                    |
| `preview.address`        | `string` | Destination wallet address.                              |
| `preview.token`          | `string` | Destination token contract address.                      |
| `preview.idempotencyKey` | `string` | The generated idempotency key.                           |

Example response:

```json theme={null}
{
  "merchantId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "payload": "eyJhbW91bnQiOjUwLCJjaGFpbklkIjo4NDUz...",
  "signature": "MEUCIQC3k9F...",
  "preview": {
    "amount": 50,
    "chainId": 8453,
    "address": "0x1a5FdBc891c5D4E6aD68064Ae45D43146D4F9f3a",
    "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    "idempotencyKey": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
  }
}
```

## Complete Node.js implementation

A copy-paste-ready Express.js signer endpoint. Replace `YOUR_MERCHANT_ID` and load your private key from a secure source.

```javascript theme={null}
const express = require('express');
const { randomUUID, createSign } = require('node:crypto');
const fs = require('node:fs');

const app = express();
app.use(express.json());

const MERCHANT_ID = process.env.MERCHANT_ID || 'YOUR_MERCHANT_ID';
const PRIVATE_KEY_PEM = process.env.MERCHANT_PRIVATE_KEY
  || fs.readFileSync('./private.pem', 'utf8');

function encodeBase64Url(value) {
  return Buffer.from(value, 'utf8').toString('base64url');
}

function signPayload(payload, privateKeyPem) {
  const signer = createSign('SHA256');
  signer.update(payload);
  signer.end();
  return signer.sign(privateKeyPem).toString('base64url');
}

function validateRequest(body) {
  const errors = [];
  const { amount, chainId, address, token, callbackScheme } = body;

  if (!Number.isFinite(amount) || amount <= 0) {
    errors.push('amount must be a positive number.');
  }
  if (!Number.isInteger(chainId) || chainId <= 0) {
    errors.push('chainId must be a positive integer.');
  }
  if (typeof address !== 'string' || !/^0x[a-fA-F0-9]{40}$/.test(address)) {
    errors.push('address must be a 0x-prefixed, 40-character hex string.');
  }
  if (typeof token !== 'string' || !/^0x[a-fA-F0-9]{1,40}$/.test(token)) {
    errors.push('token must be a 0x-prefixed hex contract address.');
  }
  if (
    callbackScheme !== null &&
    callbackScheme !== undefined &&
    (typeof callbackScheme !== 'string' ||
      !/^[a-zA-Z][a-zA-Z0-9+\-.]*$/.test(callbackScheme))
  ) {
    errors.push('callbackScheme must be null or a valid URI scheme.');
  }

  return errors;
}

app.post('/api/sign-payment', (req, res) => {
  const errors = validateRequest(req.body);
  if (errors.length > 0) {
    return res.status(400).json({ error: errors.join(' ') });
  }

  const { amount, chainId, address, token, callbackScheme = null, version = 'v1' } = req.body;

  const idempotencyKey = randomUUID();
  const signatureTimestamp = new Date().toISOString();

  const payloadObject = {
    amount, chainId, address, token,
    idempotencyKey, callbackScheme, signatureTimestamp, version,
  };

  const payload = encodeBase64Url(JSON.stringify(payloadObject));
  const signature = signPayload(payload, PRIVATE_KEY_PEM);

  res.setHeader('Cache-Control', 'no-store');
  res.json({
    merchantId: MERCHANT_ID,
    payload,
    signature,
    preview: { amount, chainId, address, token, idempotencyKey },
  });
});

const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
  console.log(`Merchant signer listening on port ${PORT}`);
});
```

## Python implementation

For merchants using Python:

```python theme={null}
import json
import uuid
import base64
from datetime import datetime, timezone
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec

def base64url_encode(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode('ascii')

def sign_payment(
    private_key_pem: str,
    merchant_id: str,
    amount: float,
    chain_id: int,
    address: str,
    token: str,
    callback_scheme: str | None = None,
    version: str = "v1",
) -> dict:
    idempotency_key = str(uuid.uuid4())
    signature_timestamp = datetime.now(timezone.utc).isoformat()

    payload_obj = {
        "amount": amount,
        "chainId": chain_id,
        "address": address,
        "token": token,
        "idempotencyKey": idempotency_key,
        "callbackScheme": callback_scheme,
        "signatureTimestamp": signature_timestamp,
        "version": version,
    }

    payload = base64url_encode(json.dumps(payload_obj).encode("utf-8"))

    private_key = serialization.load_pem_private_key(private_key_pem.encode(), password=None)
    signature_bytes = private_key.sign(
        payload.encode("utf-8"),
        ec.ECDSA(hashes.SHA256()),
    )
    signature = base64url_encode(signature_bytes)

    return {
        "merchantId": merchant_id,
        "payload": payload,
        "signature": signature,
        "preview": {
            "amount": amount,
            "chainId": chain_id,
            "address": address,
            "token": token,
            "idempotencyKey": idempotency_key,
        },
    }
```

## Signing algorithm summary

For merchants implementing the signer in any language:

1. Build the payload as a JSON string with fields: `amount`, `chainId`, `address`, `token`, `idempotencyKey`, `callbackScheme`, `signatureTimestamp`, `version`.
2. Base64url-encode the JSON string (UTF-8 bytes to base64url, no padding).
3. Sign the **base64url-encoded string** (not the raw JSON) using **ECDSA with P-256 (prime256v1/secp256r1) and SHA-256**. The input to the sign function is the ASCII bytes of the base64url string.
4. Base64url-encode the raw signature bytes (DER format).
5. Return the encoded payload, encoded signature, your merchant ID, and a preview object.

## Security requirements

<Warning>
  The signer endpoint is a security-critical component. A compromised signer allows unauthorized payment link creation.
</Warning>

* **HTTPS only.** Never serve the signer over plain HTTP in production.
* **Authenticate callers.** The signer should only accept requests from your own frontend. Use session cookies, auth tokens, or CORS restrictions.
* **Verify destination ownership.** Confirm the authenticated user controls the destination `address` before signing. Without this, a malicious user could redirect funds to a wallet they don't own. See [step 2](#2-verify-destination-ownership) above.
* **Server-enforced expiry.** Swype enforces a maximum signature age of 15 minutes. Include `signatureTimestamp` in every payload so Swype can reject stale signatures. You do not need to manage expiration TTLs yourself.
* **Rate limit.** Protect against abuse by rate-limiting signer requests per user/session.
* **Log, but never log the private key.** Log request metadata for debugging, but never log the private key or raw signature in production.
* **CORS.** Configure CORS to only allow your frontend origin(s).
