Transaction Payload Signing
SwapKit can cryptographically sign the transaction payloads returned by the Swap API so integrators can verify that responses originate from SwapKit and have not been tampered with in transit. This page describes the generic ES256 signing flow that works for any swap, on any chain, with any token.
SLIP-0024 is a separate envelope format used specifically to render human-readable confirmation screens on hardware wallets. It is signed with a different scheme (ECDSA over secp256k1 of a binary-encoded payment request) and is not the signature described here. See the separate SLIP-0024 documentation.
Overview
Each API key can be associated with a secp256r1 (P-256 / ES256) key pair.
SwapKit holds the private key; integrators receive the public key when the key pair is created.
When a swap response includes a transaction, SwapKit signs it and returns, on each route's
metaobject:meta.signedTx— the JWS payload component (see below). It is not the raw transaction.meta.signature— the ES256 signature, encoded as a Flattened JWS signature.meta.signedTxString— the exact serialized transaction bytes SwapKit hashed and signed (the signature pre-image). It is the RFC 8785 canonical JSON of an object transaction, or the raw string of a string transaction. Use it to run the binding check without re-serializing thetxyourself.
The signature is a Flattened JWS (RFC 7515): SwapKit serializes the transaction to its canonical form (see Transaction serialization below), computes the SHA-256 digest of that canonical byte string as a lowercase hex string, base64url encodes that hex string as the JWS payload (
meta.signedTx), and signs the JWS Signing Input. The integrator verifiesmeta.signatureagainst the reconstructed JWS Signing Input using the stored public key.
If no key pair is configured for the API key — or the response does not include a transaction — the swap response is returned unsigned and meta.signedTx / meta.signature / meta.signedTxString are omitted. Note that sending disableBuildTx: true suppresses transaction building and therefore signing: if you build your own transaction from our response, there is nothing for SwapKit to sign.
The signature covers a digest of the transaction, not the transaction directly. Verifying the signature is necessary but not sufficient — see Verify the signature for the two checks you must perform.
Activate signing for your API key
Signing is activated by SwapKit on your behalf — there is no self-serve endpoint. To enable it, contact your SwapKit account manager and request a signing key pair for your API key.
Once provisioned, you will receive a public key in PEM format, for example:
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----Notes:
Store the public key securely on your side — this is the only value you need to verify signatures.
SwapKit holds the corresponding private key. It is never exposed to integrators and is stored encrypted at rest using Google Cloud KMS under a per-tenant encryption key.
A key pair cannot be overwritten in place. To rotate, request a rotation through your account manager.
secp256r1(ES256) is the current default. Other key types may be added in the future.
Receive signed swap responses
Once signing is active, a swap response that includes a transaction carries the signature in each route's meta:
None of these fields is the raw transaction. meta.signedTx is the JWS payload — BASE64URL(SHA-256 hex digest of the canonical tx); meta.signature is the JWS signature over the JWS Signing Input eyJhbGciOiJFUzI1NiJ9.<meta.signedTx>; and meta.signedTxString is the exact byte string that was hashed and signed, provided so you can run the binding check by hashing it directly. The exact byte layout of each field is in the Signature specification below.
Transaction serialization
The signature is over a digest of the transaction's canonical serialization, which depends on the transaction shape:
Object transactions (EVM, Cosmos, Tron, Starknet, TON, …) are serialized with RFC 8785 — the JSON Canonicalization Scheme (JCS): object keys sorted by UTF-16 code unit, array order preserved, no insignificant whitespace. Unlike
JSON.stringify, the output byte string is deterministic regardless of object property insertion order, so it can be reproduced byte-for-byte by any platform or language that implements JCS.String transactions (PSBT, base64, CBOR, and other pre-serialized formats) are the exact signable bytes already, so they are used verbatim, with no canonicalization.
Why this matters. SwapKit previously serialized object transactions with
JSON.stringify, which does not guarantee key ordering across languages/platforms and is therefore not safe to reproduce for the binding check. Signing now uses RFC 8785 so the hashed pre-image is deterministic and reproducible byte-for-byte. SwapKit also returns the exact pre-image inmeta.signedTxString, so you never have to re-serialize the transaction yourself.
The exact serializer SwapKit uses is a standard RFC 8785 (JCS) implementation:
In other languages, use a maintained JCS/RFC 8785 library (for example canonicaljson in Python, gibson042/canonicaljson-go in Go, erdtman/java-json-canonicalization in Java) rather than your language's default JSON encoder.
Verify the signature
Verification has two independent checks — both are required:
Signature check — the JWS signature is valid for the JWS Signing Input under your public key. This proves SwapKit produced the signature.
Binding check — the signed digest equals
SHA-256of the canonical serialization of thetxyou are about to broadcast. This proves the signature is bound to the transaction you will send, not some other transaction.
Skipping step 2 leaves you exposed: an attacker could leave a valid signedTx/signature pair untouched while swapping out tx, and a signature-only check would still pass.
Run the binding check against the
txyou will broadcast.meta.signedTxStringis the exact pre-image SwapKit signed, so hashing it directly reproduces the signed digest with no re-serialization. That only proves those bytes are authentic, though — to bind the check to what you actually send, confirm the transaction you broadcast is the onemeta.signedTxStringrepresents. The trust-minimizing way to do that is to canonicalize your owntxand require it to equalmeta.signedTxString; the example below does exactly that.
You do not need a SwapKit-specific SDK. The simplest correct approach is a JWS/JOSE library, because the signature is a Flattened JWS. A raw ES256 verifier also works if you reconstruct the JWS Signing Input and handle the signature encoding (see Recommended libraries).
Signature specification
Envelope
Flattened JWS JSON Serialization (RFC 7515 §7.2.2)
Curve
secp256r1 (also known as P-256 / prime256v1)
Hash
SHA-256
Algorithm
ECDSA (ES256, RFC 7518)
Public key format
PEM, SubjectPublicKeyInfo (as delivered on activation)
Protected header
{"alg":"ES256"} → base64url eyJhbGciOiJFUzI1NiJ9
Signed bytes (JWS Signing Input)
eyJhbGciOiJFUzI1NiJ9.<meta.signedTx> (ASCII)
Canonical tx string
Object tx → RFC 8785 (JCS) JSON; string tx (PSBT, base64, CBOR, …) → used verbatim. Returned as meta.signedTxString.
meta.signedTx (JWS payload)
BASE64URL(SHA-256 hex digest of the canonical tx string)
meta.signedTxString
The exact canonical tx string (signature pre-image). Hash it to reproduce the digest without re-serializing tx.
Signature encoding
JOSE: raw r || s, 64 bytes (concatenated, fixed-width). Not DER.
Signature transport encoding
base64url (meta.signature) — not standard base64, not hex
TypeScript example using jose library
If you cannot implement RFC 8785 canonicalization in your language, you may hash
meta.signedTxStringdirectly for the digest — but you must then broadcast the transaction that string represents (parsemeta.signedTxString), not a separately-heldtx, or the binding no longer covers what you send.
Recommended libraries
The signature is a Flattened JWS with a r || s (non-DER) signature, base64url-encoded. Pick one of two paths:
Path A — JWS/JOSE library (recommended). Hand it the protected header {"alg":"ES256"}, meta.signedTx as the payload, meta.signature, and your public key. It reconstructs the signing input and handles the r || s encoding for you.
Node.js / Browser —
jose:jose.flattenedVerify({ protected, payload, signature }, key). This is what SwapKit uses; see the example above.Go —
go-jose(jose.ParseSigned/JSONWebSignature.Verify)Java —
nimbus-jose-jwt(JWSObject/ECDSAVerifier)Rust —
josekit. (Note: thejsonwebtokencrate only handles compact JWT, not arbitrary Flattened JWS payloads.)
Path B — raw ES256 verifier. If you use a generic ECDSA-P256 verifier instead, you must: (a) build the signing input string eyJhbGciOiJFUzI1NiJ9.<meta.signedTx> and pass its UTF-8 bytes as the data; (b) base64url-decode meta.signature to the raw 64-byte r || s; and (c) match the signature format your verifier expects. The verifier applies SHA-256 to the data itself.
Verifier
Signature format expected
Conversion from base64url r || s
raw r || s via { key, dsaEncoding: "ieee-p1363" }
none (default "der" would reject it)
DER
encode_dss_signature(r, s), then public_key.verify(der, input, ec.ECDSA(hashes.SHA256()))
two big integers
split r/s, ecdsa.Verify(pub, sha256.Sum256(input)[:], r, s) (or DER + VerifyASN1)
Whichever path you choose, the binding check (recomputing the digest from the canonical tx and comparing to meta.signedTx) is the same and still required.
What to do on verification failure
If either check fails:
Do not broadcast the transaction.
Do not retry against a different endpoint or relax the check.
Treat the response as untrusted and surface the error to the caller or log it for investigation.
A failure means the response did not come from SwapKit, was modified in transit, or the integration is using a stale public key after a rotation.
FAQ
Why do I have to recompute the digest if the signature already verifies? Because the signature only proves the digest is authentic. Without comparing the signed digest to the digest of your tx, a tampered transaction with an intact (but unrelated) signedTx/signature pair would pass the signature check.
Does signing work for tokens that aren't in SLIP-0044? Yes. The ES256 signature is over a digest of the raw transaction payload SwapKit returns. It is independent of any token registry — there is no SLIP-0044 lookup involved, and token coverage is not limited by it.
Can I have multiple key pairs per API key? No. One key pair per API key — which also means an API key is provisioned for either ES256 transaction payload signing or SLIP-0024 hardware-wallet signing, never both. To rotate, request a rotation through your SwapKit point of contact.
Last updated

