> For the complete documentation index, see [llms.txt](https://docs.ur.app/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ur.app/api-reference/signature-and-verify.md).

# Signature and verify

This document defines the signing and verification rules for all parties interacting with the UR ecosystem. It serves as the single source of truth for signature formats and authentication mechanisms.

## Overview

The UR ecosystem uses two primary authentication methods depending on the API being accessed:

* **Partner Authentication (Server-to-Server)**: Used for `UR-OPEN-API` and Webhooks. Authenticates the Partner backend using a registered ECDSA key pair.
* **User Authentication (Wallet-to-Server)**: Used for `UR-API`. Authenticates individual end-users using their wallet's private key.

Both methods utilize **Ethereum Personal Sign (EIP-191)**: `"\x19Ethereum Signed Message:\n{len(messageToSign)}{messageToSign}"`

***

## Part A: Partner authentication (UR-OPEN-API & webhooks)

This method is used when a Partner backend calls UR APIs or when UR sends Webhooks to a Partner.

### 1. HTTP headers

| Header            | Description                                                                                                           | Required |
| ----------------- | --------------------------------------------------------------------------------------------------------------------- | -------- |
| `X-Api-Signature` | Hexadecimal signature with `0x` prefix.                                                                               | Yes      |
| `X-Api-Deadline`  | The Unix timestamp (in seconds) indicating when the request expires. Setting this to 5 minutes from the current time. | Yes      |
| `X-Api-PublicKey` | The Ethereum address of the signer.                                                                                   | Optional |

### 2. Signing logic

#### Partner request (Partner -> UR)

* **Signer**: Partner's registered backend Ethereum address.
* **Message Components**:
  * `requestBody`: The exact raw JSON string in the request body.
  * `deadline`: The value sent in the `X-Api-Deadline` header, a timestamp within the next 5 minutes.
* **Message to Sign**: Standard OpenAPI requests use `messageToSign = "{requestBody} {deadline}"` (note the single space between body and deadline). User-scoped FMA OpenAPI requests use `messageToSign = "{canonicalPayload}urId:{X-Ur-Id}externalUserId:{X-External-User-Id} {deadline}"`; see [API reference: Managed Custody Mode](https://docs.ur.app/api-reference/pages/gg7tFeofpTSL542LLbQS#id-2.2-authentication-partner-auth-eip-191).
* **Bodyless requests** (`GET` / `DELETE` / `HEAD`): there is no request body, so the raw **query string** takes the place of `requestBody`. The message becomes `messageToSign = "{rawQueryString} {deadline}"` (an empty message, meaning no query string, is rejected).
* **Signing code example:** Refers to [this](#signature).

#### UR response / webhook (UR -> Partner)

* **Signer**: UR Server address.
* **Message Components**:
  * `responseBody`: The exact raw JSON string returned in the HTTP body.
* **Message to Sign**: `messageToSign = "{responseBody}"`

### 3. Verification

The receiver must:

* Read the raw body and relevant headers.
* Construct the `messageToSign` as defined above.
* Use EIP-191 recovery to extract the signer's Ethereum address from the `X-Api-Signature`.
* Verify that the recovered address matches the expected/whitelisted address.

***

## Part B: User authentication (UR-API)

This method is used for sensitive user operations (e.g., FX, Transfers) where a direct wallet signature is required.

### 1. HTTP headers

| Header     | Description                                                                                                                         |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `sign`     | The user's wallet signature.                                                                                                        |
| `hash`     | A Keccak256 hash of the business payload or a unique message.                                                                       |
| `deadline` | The Unix timestamp (in seconds) indicating when the request expires. We recommend setting this to 20 minutes from the current time. |
| `tokenId`  | The user's UR Token ID (URID).                                                                                                      |

### 2. Signing logic

* **Construct Base Message**: `baseMessage = hash + deadline` (String concatenation).
* **Generate Intermediate Hash**: `intermediateHash = Keccak256(baseMessage)`.
* **Construct Final Message**: `finalMessage = "I agree to access my profile. " + intermediateHash.hex()`.
* **Sign**: The user signs the `finalMessage` using their wallet (EIP-191).

***

## Key pair

{% hint style="info" %}
If you use the [API sandbox](https://partner.ur.app/api-sandbox), it generates and registers this key pair for you. The steps below are for generating your own key pair instead. See [API signing key](https://docs.ur.app/getting-started/integration-guide#api-signing-key) for both paths.
{% endhint %}

### 1. Generate key pair using Node.js (viem)

```javascript
import { toHex } from "viem";
import {
  english,
  generateMnemonic,
  mnemonicToAccount,
} from "viem/accounts";

// Generate mnemonic and account
const mnemonic = generateMnemonic(english);
const account = mnemonicToAccount(mnemonic);
const privateKeyBytes = account.getHdKey().privateKey;

console.log("Address (Public Key):", account.address);
console.log("Private Key:", privateKeyBytes ? toHex(privateKeyBytes) : "<unavailable>");
```

### 2. Key management recommendations

Private Key Storage: Use key management services like AWS Secrets Manager, HashiCorp Vault Public Key Registration: Synchronize the generated address (publicKey) with the UR system through secure channels

***

## Code examples

### signature

```go
import (
    "fmt"
    "time"
    "strconv"
    "encoding/hex"
    "github.com/ethereum/go-ethereum/crypto"
)

// GenSignature generates API request signature
// Parameters:
//   privateKeyHex: private key hex string (without 0x prefix)
//   msg: business message content (typically the request body JSON string)
//   deadline: signature expiration time (Unix timestamp, seconds)
func GenSignature(privateKeyHex string, msg string, deadline int64) (string, error) {
    // 1. Build deadline string
    deadlineStr := strconv.FormatInt(deadline, 10)
    
    // For concurrent requests within the same second, you can add a very small random offset to the deadline (but still within the allowed window) to ensure signature uniqueness
    messageToSign := fmt.Sprintf("%s %s", msg, deadlineStr)
    
    // 2. Add Ethereum personal signature prefix
    prefix := fmt.Sprintf("\x19Ethereum Signed Message:\n%d", len(messageToSign))
    prefixedMessageBytes := []byte(prefix + messageToSign)
    
    // 3. Calculate message hash
    messageHash := crypto.Keccak256Hash(prefixedMessageBytes)
    
    // 4. Load private key and perform signature
    privateKey, err := crypto.HexToECDSA(privateKeyHex)
    if err != nil {
    return "", fmt.Errorf("failed to load private key: %w", err)
    }
    
    signatureBytes, err := crypto.Sign(messageHash.Bytes(), privateKey)
    if err != nil {
    return "", fmt.Errorf("failed to sign message: %w", err)
    }
    
    // 5. Return hex-encoded signature (including recovery ID)
    return "0x" + hex.EncodeToString(signatureBytes), nil
}
```

### verify

```go
// recoverAddress recovers signer's public key address from signature and original message
func recoverAddress(message, sigHex string) (common.Address, error) {
    // Decode hex signature
    sigBytes, err := hex.DecodeString(strings.TrimPrefix(sigHex, "0x"))
    if err != nil {
        return common.Address{}, fmt.Errorf("invalid hex signature: %w", err)
    }

    if len(sigBytes) != 65 {
        return common.Address{}, fmt.Errorf("signature length must be 65 bytes, got %d", len(sigBytes))
    }

    // Compatible handling of recovery ID (v): some libraries return 27/28, need to convert to 0/1
    if sigBytes[64] >= 27 {
        sigBytes[64] -= 27
    }

    // Rebuild prefixed message hash
    prefix := fmt.Sprintf("\x19Ethereum Signed Message:\n%d", len(message))
    prefixedMsgBytes := []byte(prefix + message)
    hash := crypto.Keccak256Hash(prefixedMsgBytes)

    // Recover public key from signature
    pubKey, err := crypto.SigToPub(hash.Bytes(), sigBytes)
    if err != nil {
        return common.Address{}, fmt.Errorf("could not recover public key: %w", err)
    }

    return crypto.PubkeyToAddress(*pubKey), nil
}

```

### User wallet signature

```javascript
// We only allow the signature to be valid for 20 minutes max.
// Can be less than that if want more security.
const SIGNATURE_DEADLINE_IN_SECONDS = 1200;

// Alternative to Date.now()
// sometimes the device clock is out of sync and can give the wrong timestamp
const serverTimestamp = await fetch("https://api.fiat24.com/timestamp");
const now = serverTimestamp.timestamp;

const deadline = Math.round(now / 1000) + SIGNATURE_DEADLINE_IN_SECONDS;

const hash = "Hello world"; // Could be any text or payload hash

// Calculate Hash of (hash + deadline)
// Example using web3.js
const deadlineHash = web3.utils.sha3(hash + deadline); 
// Example using ethers.js
// const deadlineHash = ethers.keccak256(ethers.toUtf8Bytes(hash + deadline));

// Construct the specific message required by UR
const messageToSign = `I agree to access my profile. ${deadlineHash}`;

// Generate Signature
// Example using web3.js
const sign = await web3.eth.personal.sign(messageToSign, address);
// Example using ethers.js
// const sign = await signer.signMessage(messageToSign);

return { hash, deadline, sign };

```

***

## Important notes

* **Raw Body**: Always use the raw HTTP body bytes. Do not re-serialize JSON, as key ordering or whitespace differences will cause signature mismatches.
* **Deadline Window**: Recommended window is 1-5 minutes. Requests with an expired deadline are rejected. A deadline too far in the future is also rejected, but the maximum forward window is **server-configured** (typically 5 minutes) and is only enforced when that limit is set; it is not a hard-coded 5-minute constant.
* **Replay Protection**: For high-concurrency environments, add a small random offset (1-5 seconds) to the `deadline` to ensure each request generates a unique signature.
* **Case Sensitivity**: Ethereum addresses should be treated as case-insensitive but are typically stored/transmitted in lowercase or checksum format.

## Environment addresses

| Environment | UR Server Signature Address                  |
| ----------- | -------------------------------------------- |
| **Sepolia** | `0x4D2AA3f43De8f8BE746E315D291B804a4aBD3939` |
| **Mainnet** | `0xee28dEaD5F114C8405BE3be1144D59A4110B7F79` |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.ur.app/api-reference/signature-and-verify.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
