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
Request a quote
Lock a price for ~30 seconds.POST /v1/accounts/{account_id}/quotes
Execute the quote
Commit the locked price before it expires.POST /v1/accounts/{account_id}/quotes/{quote_id}/execute
Poll for terminal status
Wait for the quote to settle, or subscribe to webhooks.GET /v1/accounts/{account_id}/quotes/{quote_id}
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 30 seconds 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 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: request a quote
Send a POST to /v1/accounts/{account_id}/quotes with the trade body.
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']}")
Response example
{
"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"
}
}
}
Quotes expire approximately 30 seconds after creation. Execute promptly or request a new quote. The exact deadline
is on the expires_at field of the quote.
Step 2: execute the quote
Send a POST to /v1/accounts/{account_id}/quotes/{quote_id}/execute with an empty body. Use a fresh Idempotency-Key per logical attempt. The response confirms the quote moved to executing; settlement happens asynchronously.
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']}")
Response example
{
"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).
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"])
Response example
{
"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"
}
}
}
Instead of polling, subscribe to the quote.executed and quote.execution_failed webhooks and use GET only for
reconciliation.
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
- Treat
executed, expired, and failed as terminal. Don’t retry execution after a terminal state — request a new quote.
- 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 ~30s) |
/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 |