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

# Price trigger swaps

> Price trigger swaps are price-triggered trades that execute automatically when an asset reaches a target price. Use them to implement strategies like "Buy the Dip" (buy when price drops to a target), "Join the Rally" (buy when price rises to ride momentum), "Take the Profit" (sell when price rises to a target), and "Stop the Loss" (sell when price drops to limit losses) for your users.

## 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="Create a price trigger swap">
    Submit the swap with its trigger condition.

    `POST /v1/accounts/{account_id}/price-trigger-swaps`
  </Step>

  <Step title="Monitor or cancel">
    Track status, list active swaps, or cancel one that hasn't fired yet.

    * `GET /v1/accounts/{account_id}/price-trigger-swaps`
    * `GET /v1/accounts/{account_id}/price-trigger-swaps/{price_trigger_swap_id}`
    * `POST /v1/accounts/{account_id}/price-trigger-swaps/{price_trigger_swap_id}/cancel`
  </Step>
</Steps>

## Trigger conditions

The `when` block specifies the price to monitor and the threshold that fires the trade. Set the `base` and `quote` `AssetRef` pair, then exactly one of `drops_to` or `rises_to`:

| Field      | Description                                                           |
| ---------- | --------------------------------------------------------------------- |
| `drops_to` | Fire when the price falls to or below this value (e.g. "buy on dip")  |
| `rises_to` | Fire when the price rises to or above this value (e.g. "take profit") |

The threshold is a `base`/`quote` rate as a decimal string. For example, with `base.symbol` = `BTC` and `quote.symbol` = `USD`, `drops_to: "50000.00"` means "fire when 1 BTC trades at 50,000 USD or below".

**Examples:**

* **Buy the Dip:** "Buy BTC when BTC/USD drops to 50,000" — set `base.symbol` to `BTC`, `quote.symbol` to `USD`, and `drops_to` to `"50000.00"`. Pair with a `trade` whose `from` is `USD` and `to` is `BTC`.
* **Stop the Loss:** "Sell BTC when BTC/USD drops to 50,000" — same `when` block, but with `trade.from` set to `BTC` and `trade.to` set to `USD`.
* **Join the Rally:** "Buy BTC when BTC/USD rises to 80,000" — same pair, replace `drops_to` with `rises_to: "80000.00"`. Trade goes USD → BTC.
* **Take the Profit:** "Sell BTC when BTC/USD rises to 80,000" — same `when` block as Join the Rally, but with `trade.from` set to `BTC` and `trade.to` set to `USD`.

## Execution behavior

Price trigger swaps are not guaranteed to execute at exactly the threshold price. The threshold is a **trigger**, not a limit price. The execution price can move beyond the threshold, but only ever in the direction that fired the trigger — it never crosses back to the other side of the threshold:

* **Buy the Dip / Stop the Loss (`drops_to`):** The swap triggers when the market price drops to or below `drops_to`. The execution price may be lower than the threshold, but never higher.
* **Join the Rally / Take the Profit (`rises_to`):** The swap triggers when the market price rises to or above `rises_to`. The execution price may be higher than the threshold, but never lower.

<Note>
  For swaps where the execution price is higher than the market price and the user is spending the client's reserve fiat
  currency, an error will be returned at creation. Because the execution price may be higher than the threshold, the
  final spend amount is unbounded upward — this prevents a situation where a user creates a swap that could spend more
  fiat than the client has approved.
</Note>

## 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: create a price trigger swap

Submit the swap with `trade`, `when`, and an optional `external_reference`. The `external_reference` is a free-form string for client-side correlation — it's echoed back on reads and webhook events but is not interpreted by Payward. The response returns only the new swap's `id`; fetch `GET /v1/accounts/{account_id}/price-trigger-swaps/{price_trigger_swap_id}` to read the full resource.

Set the `amount` on **exactly one** of `trade.from` or `trade.to` to indicate which side of the trade is fixed. Setting both or neither returns `400 Bad Request`.

<CodeGroup>
  ```python Python theme={null}
  def create_price_trigger_swap(account_id):
      endpoint = f"/v1/accounts/{account_id}/price-trigger-swaps"
      nonce = time.time_ns()

      body = {
          "external_reference": "buy-dip-btc-001",
          "trade": {
              "from": {"symbol": "USD", "type": "fiat", "amount": "100.00"},
              "to":   {"symbol": "BTC", "type": "crypto"},
              "fee":  {"bps": 50},
          },
          "when": {
              "base":  {"symbol": "BTC", "type": "crypto"},
              "quote": {"symbol": "USD", "type": "fiat"},
              "drops_to": "50000.00",
          },
      }

      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"
  created = create_price_trigger_swap(account_id)
  swap_id = created["data"]["id"]
  print(f"Swap created: {swap_id}")
  ```

  ```javascript Javascript theme={null}
  async function createPriceTriggerSwap(accountId) {
    const endpoint = `/v1/accounts/${accountId}/price-trigger-swaps`;
    const nonce = process.hrtime.bigint().toString();

    const body = {
      external_reference: 'buy-dip-btc-001',
      trade: {
        from: { symbol: 'USD', type: 'fiat', amount: '100.00' },
        to: { symbol: 'BTC', type: 'crypto' },
        fee: { bps: 50 },
      },
      when: {
        base: { symbol: 'BTC', type: 'crypto' },
        quote: { symbol: 'USD', type: 'fiat' },
        drops_to: '50000.00',
      },
    };

    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 created = await createPriceTriggerSwap(accountId);
  const swapId = created.data.id;
  console.log('Swap created:', swapId);
  ```
</CodeGroup>

### Response example

```json theme={null}
{
  "data": {
    "id": "swap_01J0M7C0Z9F8YX3GQH8E"
  }
}
```

The response also carries a `Location` header pointing at the new resource (e.g. `/v1/accounts/NL00KRAK0123456789/price-trigger-swaps/swap_01J0M7C0Z9F8YX3GQH8E`).

## Step 2: monitor swaps

### Get a single swap

<CodeGroup>
  ```python Python theme={null}
  def get_price_trigger_swap(account_id, swap_id):
      endpoint = f"/v1/accounts/{account_id}/price-trigger-swaps/{swap_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()


  swap = get_price_trigger_swap(account_id, swap_id)
  print(f"Status: {swap['data']['status']}")
  ```

  ```javascript Javascript theme={null}
  async function getPriceTriggerSwap(accountId, swapId) {
    const endpoint = `/v1/accounts/${accountId}/price-trigger-swaps/${swapId}`;
    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();
  }

  const swap = await getPriceTriggerSwap(accountId, swapId);
  console.log('Status:', swap.data.status);
  ```
</CodeGroup>

#### Response example

```json theme={null}
{
  "data": {
    "id": "swap_01J0M7C0Z9F8YX3GQH8E",
    "external_reference": "buy-dip-btc-001",
    "trade": {
      "from": { "symbol": "USD", "type": "fiat", "amount": "100.00" },
      "to": { "symbol": "BTC", "type": "crypto" }
    },
    "when": {
      "base": { "symbol": "BTC", "type": "crypto" },
      "quote": { "symbol": "USD", "type": "fiat" },
      "drops_to": "50000.00"
    },
    "status": "active"
  }
}
```

When `status` is `completed`, the response also includes `trade.fees` and `trade.rate` reflecting what was realised at execution time.

### List swaps

Filter by status using the `statuses` query parameter (repeat for multiple values, or omit to include any status). Use `page_token` and `page_size` (default 20, max 100) for pagination — `next_page_token` is omitted on the final page.

<CodeGroup>
  ```python Python theme={null}
  def list_price_trigger_swaps(account_id, statuses=None, page_token=None, page_size=20):
      endpoint = f"/v1/accounts/{account_id}/price-trigger-swaps"
      nonce = time.time_ns()

      params = []
      if statuses:
          params.extend(("statuses", s) for s in statuses)
      if page_token:
          params.append(("page_token", page_token))
      params.append(("page_size", page_size))

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


  page = list_price_trigger_swaps(account_id, statuses=["active"])
  for s in page["data"]:
      print(f"{s.get('external_reference', s['id'])} — {s['status']}")
  ```

  ```javascript Javascript theme={null}
  async function listPriceTriggerSwaps(accountId, { statuses = [], pageToken, pageSize = 20 } = {}) {
    const endpoint = `/v1/accounts/${accountId}/price-trigger-swaps`;
    const nonce = process.hrtime.bigint().toString();

    const search = new URLSearchParams();
    for (const s of statuses) search.append('statuses', s);
    if (pageToken) search.append('page_token', pageToken);
    search.append('page_size', String(pageSize));

    const signature = getPaywardSignature(endpoint, null, API_SECRET, nonce, Object.fromEntries(search.entries()));
    const response = await fetch(`${BASE_URL}${endpoint}?${search}`, {
      method: 'GET',
      headers: pwsHeaders(signature, nonce),
    });

    return response.json();
  }

  const page = await listPriceTriggerSwaps(accountId, { statuses: ['active'] });
  for (const s of page.data) {
    console.log(`${s.external_reference ?? s.id} — ${s.status}`);
  }
  ```
</CodeGroup>

### Swap statuses

| Status      | Description                                                                |
| ----------- | -------------------------------------------------------------------------- |
| `active`    | Swap is monitoring the market, waiting for the trigger condition to be met |
| `completed` | Trigger condition was met and the trade executed                           |
| `cancelled` | Cancelled at the user's request before the trigger fired                   |
| `failed`    | Terminated by Payward before completing, for a non-user reason             |

A `cancelled` swap is always user-initiated and carries no extra reason. When `status` is `failed`, the response includes a `failure_reason` field explaining why it was terminated. The value is one of:

| `failure_reason`         | Description                                                  |
| ------------------------ | ------------------------------------------------------------ |
| `user_locked`            | The user's account was locked                                |
| `expired_payment_method` | The funding payment method expired                           |
| `asset_unavailable`      | One of the assets in the pair became unavailable for trading |
| `pair_unavailable`       | The trading pair became unavailable                          |
| `retries_exhausted`      | The swap exhausted its execution retries                     |
| `funding_method_deleted` | The funding method backing the swap was deleted              |
| `other`                  | Terminated for a reason not listed above                     |

## Step 3: cancel a swap

Cancel an `active` swap that hasn't triggered yet. The body is empty.

Swaps in `completed`, `cancelled`, or `failed` status are terminal and return `409 Conflict` with `code: price_trigger_swap_not_cancellable`.

<CodeGroup>
  ```python Python theme={null}
  def cancel_price_trigger_swap(account_id, swap_id):
      endpoint = f"/v1/accounts/{account_id}/price-trigger-swaps/{swap_id}/cancel"
      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,
      )
      response.raise_for_status()


  cancel_price_trigger_swap(account_id, swap_id)
  swap = get_price_trigger_swap(account_id, swap_id)
  print(f"Status: {swap['data']['status']}")  # cancelled
  ```

  ```javascript Javascript theme={null}
  async function cancelPriceTriggerSwap(accountId, swapId) {
    const endpoint = `/v1/accounts/${accountId}/price-trigger-swaps/${swapId}/cancel`;
    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),
    });

    if (!response.ok) {
      throw new Error(`Cancel failed: ${response.status}`);
    }
  }

  await cancelPriceTriggerSwap(accountId, swapId);
  const after = await getPriceTriggerSwap(accountId, swapId);
  console.log('Status:', after.data.status); // cancelled
  ```
</CodeGroup>

## Webhook events

| Event type                            | Delivered when                                                                  |
| ------------------------------------- | ------------------------------------------------------------------------------- |
| `price_trigger_swap.executed`         | Trigger fired and the resulting trade settled successfully                      |
| `price_trigger_swap.execution_failed` | Trigger fired but the trade failed to execute (may retry, may move to `failed`) |
| `price_trigger_swap.cancelled`        | Swap moved to `cancelled` status                                                |

## Portfolio transactions

Executed price trigger swaps appear in the List Portfolio Transactions endpoint. Filter by transaction type:

| Transaction type            | Description                              |
| --------------------------- | ---------------------------------------- |
| `price_trigger_swap`        | Successfully executed price trigger swap |
| `price_trigger_swap_failed` | Price trigger swap execution that failed |

```
GET /v1/accounts/{account_id}/portfolio/transactions?types=price_trigger_swap,price_trigger_swap_failed
```

## API reference

| Endpoint                                                                       | Method | Description                                     |
| ------------------------------------------------------------------------------ | ------ | ----------------------------------------------- |
| `/v1/accounts/{account_id}/price-trigger-swaps`                                | POST   | Create a price trigger swap                     |
| `/v1/accounts/{account_id}/price-trigger-swaps`                                | GET    | List price trigger swaps (filterable by status) |
| `/v1/accounts/{account_id}/price-trigger-swaps/{price_trigger_swap_id}`        | GET    | Get a single price trigger swap                 |
| `/v1/accounts/{account_id}/price-trigger-swaps/{price_trigger_swap_id}/cancel` | POST   | Cancel an active price trigger swap             |
