Skip to main content

Prerequisites

  • Payward Services API credentials (see the Authentication guide).
  • 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

1

Create a price trigger swap

Submit the swap with its trigger condition.POST /v1/accounts/{account_id}/price-trigger-swaps
2

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

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:
FieldDescription
drops_toFire when the price falls to or below this value (e.g. “buy on dip”)
rises_toFire 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:
  • Buy the Dip / Stop the Loss: The swap triggers when the market price drops to or below drops_to. The actual execution price may be lower than the threshold.
  • Join the Rally / Take the Profit: The swap triggers when the market price rises to or above rises_to. The actual execution price may be higher than the threshold.
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.

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 for the full algorithm.
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
Every PWS write endpoint (POST / PUT / DELETE) accepts an Idempotency-Key header containing a UUIDv4. Generate a fresh key per logical attempt — replays of the same key return the original response body and the Idempotent-Replayed: true response header, which keeps retries safe under timeouts and connection drops.

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.
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}")

Response example

{
  "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

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']}")

Response example

{
  "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.
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']}")

Swap statuses

StatusDescription
activeSwap is monitoring the market, waiting for the trigger condition to be met
completedTrigger condition was met and the trade executed
cancelledCancelled at the user’s request before the trigger fired
failedTerminated 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_reasonDescription
user_lockedThe user’s account was locked
expired_payment_methodThe funding payment method expired
asset_unavailableOne of the assets in the pair became unavailable for trading
pair_unavailableThe trading pair became unavailable
retries_exhaustedThe swap exhausted its execution retries
funding_method_deletedThe funding method backing the swap was deleted
otherTerminated 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.
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

Webhook events

Event typeDelivered when
price_trigger_swap.executedTrigger fired and the resulting trade settled successfully
price_trigger_swap.execution_failedTrigger fired but the trade failed to execute (may retry, may move to failed)
price_trigger_swap.cancelledSwap moved to cancelled status

Portfolio transactions

Executed price trigger swaps appear in the List Portfolio Transactions endpoint. Filter by transaction type:
Transaction typeDescription
price_trigger_swapSuccessfully executed price trigger swap
price_trigger_swap_failedPrice trigger swap execution that failed
GET /v1/accounts/{account_id}/portfolio/transactions?types=price_trigger_swap,price_trigger_swap_failed

API reference

EndpointMethodDescription
/v1/accounts/{account_id}/price-trigger-swapsPOSTCreate a price trigger swap
/v1/accounts/{account_id}/price-trigger-swapsGETList price trigger swaps (filterable by status)
/v1/accounts/{account_id}/price-trigger-swaps/{price_trigger_swap_id}GETGet a single price trigger swap
/v1/accounts/{account_id}/price-trigger-swaps/{price_trigger_swap_id}/cancelPOSTCancel an active price trigger swap