Prerequisites
- You create API keys in Payward Console. Sandbox and production each have their own Console, listed in Environments, and a key works only in the environment where you created it. To get access to the sandbox Console, see Get sandbox access.
- Some examples on this page act on an account. To find your account ID, see Find your account ID.
Authentication parameters
The Payward Services API uses HMAC signing. Every authenticated request must include the following headers:API-KeyHTTP header parameter: your public API key.API-TimestampHTTP header parameter: the time you signed the request, as Unix time in milliseconds.API-SignHTTP header parameter: HMAC-SHA512 signature of the request.
Which version does my key use?
API keys use one of two signing versions: V2 (timestamp), described on this page, and V1 (nonce), the legacy version. Use V2 for every new integration. With V1, each nonce must be larger than the last one the API received for that key, so servers that share a key must coordinate their nonces. With V2, the API accepts any recent timestamp, in any order.- To see a key’s version, open it in API keys in Payward Console. If its details show a Signature version, that’s the version it uses. Otherwise, ask your Payward contact.
- If your key uses V1, keep signing its requests as described in V1 signing (legacy).
- To move to V2, ask your Payward contact to migrate your existing keys. They’ll tell you when to switch your integration to V2 signing. You can also create a new key that uses V2.
Setting the API-Key parameter
The value for the API-Key HTTP header parameter is your public API key.
In Payward Console, open API keys and select Create key. Give the key a name, choose its purpose, set an expiration date if needed, and select only the scopes your integration needs. The examples on this page need two scopes: query-funds for the portfolio requests and modify-trades for creating a quote. Listing webhooks needs no scope. If the form offers a choice of signature version, choose V2 (timestamp).






Setting the API-Timestamp parameter
Set API-Timestamp to the current Unix time in milliseconds, as a decimal string, such as 1700000000000. Read it from the system clock, such as time.time_ns() // 1_000_000 in Python or Date.now() in JavaScript. Timers with an arbitrary origin, such as process.hrtime() in Node.js, don’t return Unix time.
Take a new timestamp for every request, including every retry. See Retries and duplicate requests.
The API accepts a timestamp up to 15 seconds before or after its own time, and rejects the request otherwise. Keep your servers’ clocks synchronized with NTP (Network Time Protocol).
Setting the API-Sign parameter
The value for the API-Sign HTTP header parameter is a base64-encoded HMAC-SHA512 signature of a four-line signing string:
Algorithm steps
- Take the timestamp: use the exact value you send in
API-Timestamp. - Take the method: use the HTTP method in uppercase, such as
GETorPOST. - Take the request target: use the path and query string exactly as you send them, such as
/v1/webhooksor/v1/accounts/{account_id}/portfolio?quote_symbol=EUR. Leave out the scheme, host, port, and fragment. Keep the percent-encoding exactly as sent. - Hash the body: compute the SHA-256 digest of the exact body bytes you send, and write it in uppercase hexadecimal. For any request without a body, hash empty bytes, which gives
E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855. - Build the signing string: join the timestamp, method, request target, and body hash with newline characters (
\n), in that order. Don’t add a trailing newline. - Sign: compute HMAC-SHA512 over the UTF-8 bytes of the signing string. Use your API secret, base64-decoded, as the HMAC key.
- Encode: base64-encode the signature. The result is the
API-Signvalue.
Examples
The following helpers sign requests in Python and JavaScript. They read your credentials fromPWS_API_KEY and PWS_API_SECRET, and the API base URL from PWS_BASE_URL. Set PWS_BASE_URL to the sandbox or production base URL, listed in Environments.
payward_signature(paywardSignaturein JavaScript) computes theAPI-Signvalue from its arguments. It reads no clock and no environment variable, so you can unit-test it with the test vectors.signed_headers(signedHeaders) takes the current time, signs the request, and returns every header to send:API-Key,API-Timestamp, andAPI-Sign. It addsContent-Type: application/jsonwhen the request has a body. Withidempotent=True({ idempotent: true }), it also adds a newIdempotency-Key. Each call creates a new key, so to retry an operation safely, create the key yourself and reuse it, as shown in Retries and duplicate requests.
Test vectors
To check your own signer, reproduce the signatures of these two requests. They use fake key material:GET with a query string and no body
Request:
POST with a body and no query string
Request, with the body on the second line:
Complete request example
Here’s a complete example making an authenticatedGET request to list your webhooks. Add it after the helpers, in the same file. Run the JavaScript version as an ES module, for example saved as list-webhooks.mjs.
401 Unauthorized instead, which the example raises as an error. See Troubleshooting.
Query parameters in signature
When your request includes query parameters, sign the path together with its query string. Build that string once, and use it both to sign and to send the request, so the encoding you sign is the encoding you send. Build the query string withurlencode() in Python or URLSearchParams in JavaScript, and send that exact string. In Python, don’t encode characters such as ~ as %7E by hand: requests decodes them before sending, so the request no longer matches its signature.
The examples in this section and the next one call account-scoped endpoints. They read the account ID from PWS_ACCOUNT_ID. To find it, see Find your account ID. A user ID won’t work in its place. See Users, accounts, and ownership.
params= in requests. The client then encodes the query string itself, and the result can differ from the string you signed.
Request body in signature
For requests that send a body (POST, PUT, PATCH), the body bytes used to compute the signature must match the bytes sent on the wire exactly. Differences in whitespace, key ordering, or encoding produce a signature mismatch.
Serialize the body once, and pass the same string to signed_headers and to the HTTP request:
Content-Type.
Retries and duplicate requests
The API rejects a request that exactly repeats an earlier one, with the same timestamp, method, request target, and body. Because the timestamp is signed, a copy can’t be given a new one, so once the timestamp is too old, the API rejects the copy anyway. To make sure the API doesn’t reject your own requests:- Don’t send two identical requests in the same millisecond. They get the same timestamp and signature, so the API rejects the second one.
- Sign every retry again, with a new timestamp. Don’t resend the headers of an earlier attempt.
- If the endpoint takes an
Idempotency-Keyheader, listed among its parameters in the API reference, send the same key with every retry of an operation. The new signature makes each attempt a new request, and the shared key tells the API that they’re the same operation.
Troubleshooting
Authentication failures return401 Unauthorized with "code": "gateway" in the error body, whatever the cause, so the response doesn’t say which check failed. The exception is a request that sends both API-Timestamp and API-Nonce, which returns 400 Bad Request with the same code. Find the cause from what you see: