> ## 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.

# Swaps

> The Swaps API offers institutional FX-style trading. Lock a price for ~2 minutes, execute against the locked quote, then poll until the trade reaches a terminal state.

## Prerequisites

* Payward Services API credentials (see the [Authentication guide](/tabs/developer-documentation/get-started/authentication)).
* A verified user with at least one account and sufficient balance in the source asset.
* Examples target `https://api.services.payward.com` and read credentials from the `PWS_API_KEY` and `PWS_API_SECRET` environment variables.

## Workflow

<Steps>
  <Step title="Request a quote">
    Lock a price for \~2 minutes.

    `POST /v1/accounts/{account_id}/quotes`
  </Step>

  <Step title="Execute the quote">
    Commit the locked price before it expires.

    `POST /v1/accounts/{account_id}/quotes/{quote_id}/execute`
  </Step>

  <Step title="Poll for terminal status">
    Wait for the quote to settle, or subscribe to webhooks.

    `GET /v1/accounts/{account_id}/quotes/{quote_id}`
  </Step>
</Steps>

## Quote lifecycle

| Status      | Description                                                                                  |
| ----------- | -------------------------------------------------------------------------------------------- |
| `offered`   | Quote created and locked. May still expire if the market moves too far before you execute.   |
| `executing` | Execution accepted. The trade is being processed asynchronously.                             |
| `executed`  | Terminal. The trade settled.                                                                 |
| `expired`   | Terminal. The TTL elapsed, or the market moved too far for the locked price to remain valid. |
| `failed`    | Terminal. The trade failed to execute.                                                       |

A quote is valid for approximately 120 seconds (2 minutes) after creation. Calling execute on a quote whose `expires_at` has passed returns `410 Gone`.

## Specifying the trade

A quote needs `from`, `to`, and `fee` fields. Set the `amount` on **exactly one** of `from` or `to` to indicate which side of the trade is fixed. The server calculates the other side. Setting both or neither returns `400 Bad Request`.

| Scenario                                    | `from.amount` | `to.amount` |
| ------------------------------------------- | ------------- | ----------- |
| "Sell 0.5 BTC for whatever USD that yields" | `"0.5"`       | omit        |
| "Buy whatever BTC \$5,000 yields"           | omit          | `"5000"`    |

Each side carries a `type` that classifies the asset:

| `type`       | Examples              |
| ------------ | --------------------- |
| `fiat`       | `USD`, `EUR`, `GBP`   |
| `crypto`     | `BTC`, `ETH`, `SOL`   |
| `stablecoin` | `USDC`, `USDT`, `DAI` |
| `xstock`     | `TSLAx`, `AAPLx`      |

`fee.bps` is the partner's fee in basis points (1 bps = 0.01%, so 50 bps is 0.5%).

## Authentication setup

Authenticated endpoints require an HMAC-SHA512 request signature in the `API-Sign` header and a monotonically increasing nonce in the `API-Nonce` header. The helper below derives the signature from the URL path, request body, and nonce. See the [Authentication guide](/tabs/developer-documentation/get-started/authentication) for the full algorithm.

<CodeGroup>
  ```python Python theme={null}
  import os
  import json
  import time
  import uuid
  import hashlib
  import hmac
  import base64
  import urllib.parse
  import requests

  API_KEY = os.environ["PWS_API_KEY"]
  API_SECRET = os.environ["PWS_API_SECRET"]
  BASE_URL = "https://api.services.payward.com"


  def get_payward_signature(urlpath, data, secret, nonce, params=None):
      encoded = (
          str(nonce).encode("utf-8")
          if data is None
          else (str(nonce) + json.dumps(data)).encode("utf-8")
      )

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

      message = sign_path.encode() + hashlib.sha256(encoded).digest()
      mac = hmac.new(base64.b64decode(secret), message, hashlib.sha512)
      return base64.b64encode(mac.digest()).decode()


  def pws_headers(signature, nonce, idempotent=False):
      headers = {
          "API-Key": API_KEY,
          "API-Sign": signature,
          "API-Nonce": str(nonce),
          "Content-Type": "application/json",
      }
      if idempotent:
          headers["Idempotency-Key"] = str(uuid.uuid4())
      return headers
  ```

  ```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 = 'https://api.services.payward.com';

  function getPaywardSignature(urlpath, data, secret, nonce, params = null) {
    const encoded = data === null ? String(nonce) : String(nonce) + JSON.stringify(data);

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

    const sha256Hash = crypto.createHash('sha256').update(encoded).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');
  }

  function pwsHeaders(signature, nonce, { idempotent = false } = {}) {
    const headers = {
      'API-Key': API_KEY,
      'API-Sign': signature,
      'API-Nonce': String(nonce),
      'Content-Type': 'application/json',
    };
    if (idempotent) {
      headers['Idempotency-Key'] = crypto.randomUUID();
    }
    return headers;
  }
  ```
</CodeGroup>

## Step 1: request a quote

Send a `POST` to `/v1/accounts/{account_id}/quotes` with the trade body.

<CodeGroup>
  ```python Python theme={null}
  def create_swap_quote(account_id):
      endpoint = f"/v1/accounts/{account_id}/quotes"
      nonce = time.time_ns()

      body = {
          "from": {"symbol": "BTC", "type": "crypto", "amount": "0.5"},
          "to":   {"symbol": "USD", "type": "fiat"},
          "fee":  {"bps": 50},
      }

      signature = get_payward_signature(endpoint, body, API_SECRET, nonce)
      response = requests.post(
          BASE_URL + endpoint,
          headers=pws_headers(signature, nonce, idempotent=True),
          json=body,
      )
      return response.json()


  account_id = "NL00KRAK0123456789"
  quote = create_swap_quote(account_id)
  print(f"Quote ID:    {quote['data']['id']}")
  print(f"Status:      {quote['data']['status']}")
  print(f"Expires at:  {quote['data']['expires_at']}")
  print(f"You send:    {quote['data']['from']['amount']} {quote['data']['from']['symbol']}")
  print(f"You receive: {quote['data']['to']['amount']} {quote['data']['to']['symbol']}")
  ```

  ```javascript Javascript theme={null}
  async function createSwapQuote(accountId) {
    const endpoint = `/v1/accounts/${accountId}/quotes`;
    const nonce = process.hrtime.bigint().toString();

    const body = {
      from: { symbol: 'BTC', type: 'crypto', amount: '0.5' },
      to: { symbol: 'USD', type: 'fiat' },
      fee: { bps: 50 },
    };

    const signature = getPaywardSignature(endpoint, body, API_SECRET, nonce);
    const response = await fetch(BASE_URL + endpoint, {
      method: 'POST',
      headers: pwsHeaders(signature, nonce, { idempotent: true }),
      body: JSON.stringify(body),
    });

    return response.json();
  }

  const accountId = 'NL00KRAK0123456789';
  const quote = await createSwapQuote(accountId);
  console.log('Quote ID:   ', quote.data.id);
  console.log('Status:     ', quote.data.status);
  console.log('Expires at: ', quote.data.expires_at);
  ```
</CodeGroup>

### Response example

```json theme={null}
{
  "data": {
    "id": "Q-BTC2USD-1",
    "status": "offered",
    "created_at": "2026-04-23T16:00:00Z",
    "expires_at": "2026-04-23T16:00:30Z",
    "from": {
      "symbol": "BTC",
      "type": "crypto",
      "amount": "0.5"
    },
    "to": {
      "symbol": "USD",
      "type": "fiat",
      "amount": "31495.50"
    },
    "fees": {
      "trade": {
        "symbol": "USD",
        "type": "fiat",
        "amount": "150.00"
      }
    },
    "rate": {
      "base": { "symbol": "BTC", "type": "crypto" },
      "quote": { "symbol": "USD", "type": "fiat" },
      "price": "63291.00"
    }
  }
}
```

<Warning>
  Quotes expire after **120 seconds** (2 minutes). Execute the quote promptly or request a new one if it expires. The
  exact expiration time is returned in the `expires_at` field of the quote response.
</Warning>

## Step 2: execute the quote

Send a `POST` to `/v1/accounts/{account_id}/quotes/{quote_id}/execute` with an empty body. The response confirms the quote moved to `executing`; settlement happens asynchronously.

<CodeGroup>
  ```python Python theme={null}
  def execute_swap_quote(account_id, quote_id):
      endpoint = f"/v1/accounts/{account_id}/quotes/{quote_id}/execute"
      nonce = time.time_ns()
      body = {}

      signature = get_payward_signature(endpoint, body, API_SECRET, nonce)
      response = requests.post(
          BASE_URL + endpoint,
          headers=pws_headers(signature, nonce, idempotent=True),
          json=body,
      )
      return response.json()


  execution = execute_swap_quote(account_id, quote["data"]["id"])
  print(f"Status:      {execution['data']['status']}")
  print(f"Executed at: {execution['data']['executed_at']}")
  ```

  ```javascript Javascript theme={null}
  async function executeSwapQuote(accountId, quoteId) {
    const endpoint = `/v1/accounts/${accountId}/quotes/${quoteId}/execute`;
    const nonce = process.hrtime.bigint().toString();
    const body = {};

    const signature = getPaywardSignature(endpoint, body, API_SECRET, nonce);
    const response = await fetch(BASE_URL + endpoint, {
      method: 'POST',
      headers: pwsHeaders(signature, nonce, { idempotent: true }),
      body: JSON.stringify(body),
    });

    return response.json();
  }

  const execution = await executeSwapQuote(accountId, quote.data.id);
  console.log('Status:     ', execution.data.status);
  console.log('Executed at:', execution.data.executed_at);
  ```
</CodeGroup>

### Response example

```json theme={null}
{
  "data": {
    "id": "Q-BTC2USD-1",
    "status": "executing",
    "executed_at": "2026-04-23T16:00:12Z"
  }
}
```

## Step 3: poll for terminal status

After executing, poll `GET /v1/accounts/{account_id}/quotes/{quote_id}` until `status` reaches a terminal value (`executed`, `expired`, or `failed`).

<CodeGroup>
  ```python Python theme={null}
  def get_swap_quote(account_id, quote_id):
      endpoint = f"/v1/accounts/{account_id}/quotes/{quote_id}"
      nonce = time.time_ns()

      signature = get_payward_signature(endpoint, None, API_SECRET, nonce)
      response = requests.get(BASE_URL + endpoint, headers=pws_headers(signature, nonce))
      return response.json()


  def wait_for_terminal_status(account_id, quote_id, max_attempts=60):
      terminal = {"executed", "expired", "failed"}

      for _ in range(max_attempts):
          quote = get_swap_quote(account_id, quote_id)
          status = quote["data"]["status"]
          print(f"Status: {status}")

          if status in terminal:
              return quote

          time.sleep(1)

      raise RuntimeError("Quote did not reach a terminal state in time")


  final = wait_for_terminal_status(account_id, quote["data"]["id"])
  ```

  ```javascript Javascript theme={null}
  async function getSwapQuote(accountId, quoteId) {
    const endpoint = `/v1/accounts/${accountId}/quotes/${quoteId}`;
    const nonce = process.hrtime.bigint().toString();

    const signature = getPaywardSignature(endpoint, null, API_SECRET, nonce);
    const response = await fetch(BASE_URL + endpoint, {
      method: 'GET',
      headers: pwsHeaders(signature, nonce),
    });

    return response.json();
  }

  async function waitForTerminalStatus(accountId, quoteId, maxAttempts = 60) {
    const terminal = new Set(['executed', 'expired', 'failed']);

    for (let i = 0; i < maxAttempts; i++) {
      const quote = await getSwapQuote(accountId, quoteId);
      const { status } = quote.data;
      console.log('Status:', status);

      if (terminal.has(status)) {
        return quote;
      }

      await new Promise(resolve => setTimeout(resolve, 1000));
    }

    throw new Error('Quote did not reach a terminal state in time');
  }

  const final = await waitForTerminalStatus(accountId, quote.data.id);
  ```
</CodeGroup>

### Response example

```json theme={null}
{
  "data": {
    "id": "Q-BTC2USD-1",
    "status": "executed",
    "created_at": "2026-04-23T16:00:00Z",
    "expires_at": "2026-04-23T16:00:30Z",
    "from": {
      "symbol": "BTC",
      "type": "crypto",
      "amount": "0.5"
    },
    "to": {
      "symbol": "USD",
      "type": "fiat",
      "amount": "31495.50"
    },
    "fees": {
      "trade": {
        "symbol": "USD",
        "type": "fiat",
        "amount": "150.00"
      }
    },
    "rate": {
      "base": { "symbol": "BTC", "type": "crypto" },
      "quote": { "symbol": "USD", "type": "fiat" },
      "price": "63291.00"
    }
  }
}
```

<Tip>
  Instead of polling, subscribe to the `quote.executed` and `quote.execution_failed` webhooks and use `GET` only for
  reconciliation.
</Tip>

## Error handling

| HTTP status       | Code                     | Cause                                                   | Remediation                                       |
| ----------------- | ------------------------ | ------------------------------------------------------- | ------------------------------------------------- |
| `400 Bad Request` | validation error         | Both or neither of `from.amount` / `to.amount` were set | Set exactly one side of the trade                 |
| `409 Conflict`    | `quote_already_executed` | The quote has already been executed                     | Read the quote's terminal state with `GET`        |
| `410 Gone`        | `quote_expired`          | The quote was executed after its `expires_at`           | Request a fresh quote and execute it more quickly |

### Best practices

1. **Treat `executed`, `expired`, and `failed` as terminal.** Don't retry execution after a terminal state — request a new quote.
2. **Capture `Request-Id` from response headers.** It speeds up support investigations.

## API reference

| Endpoint                                              | Method | Description                            |
| ----------------------------------------------------- | ------ | -------------------------------------- |
| `/v1/accounts/{account_id}/quotes`                    | POST   | Create a swap quote (locks price \~2m) |
| `/v1/accounts/{account_id}/quotes/{quote_id}/execute` | POST   | Execute a locked quote                 |
| `/v1/accounts/{account_id}/quotes/{quote_id}`         | GET    | Read the current state of a quote      |
