> For the complete documentation index, see [llms.txt](https://docs.swapkit.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.swapkit.dev/spotlights/transaction-payload-signing.md).

# 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](/spotlights/slip-0024-transaction-payload-signing.md).

***

#### 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 `meta` object**:
  * `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.
* The signature is a **Flattened JWS (RFC 7515)**: SwapKit computes the `SHA-256` digest of the canonical transaction, base64url-encodes that digest as the JWS payload (`meta.signedTx`), and signs the JWS Signing Input. The integrator verifies `meta.signature` against 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` are omitted.

> **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`:

```json
{
  "routes": [
    {
      "tx": { "...": "..." },
      "meta": {
        "signedTx": "<base64url(SHA-256 hex digest of the canonical tx)>",
        "signature": "<base64url ES256 signature — see Signature specification below>"
      }
    }
  ]
}
```

Neither field is the raw transaction: `meta.signedTx` is the **JWS payload** — `BASE64URL(SHA-256 hex digest of the tx)` — and `meta.signature` is the JWS signature over the **JWS Signing Input** `eyJhbGciOiJFUzI1NiJ9.<meta.signedTx>`. The exact byte layout of each field is in the Signature specification below.

#### Verify the signature

Verification has **two independent checks** — both are required:

1. **Signature check** — the JWS signature is valid for the JWS Signing Input under your public key. This proves SwapKit produced the signature.
2. **Binding check** — `meta.signedTx` equals `BASE64URL(SHA-256-hex(your tx))`. This proves the signature is bound to the transaction you are about to broadcast, 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.

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**

| Field                            | Value                                                                        |
| -------------------------------- | ---------------------------------------------------------------------------- |
| 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)                               |
| `meta.signedTx` (JWS payload)    | `BASE64URL(SHA-256 hex digest of the canonical tx string)`                   |
| 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**

```typescript
import * as jose from "jose";
import * as crypto from "crypto";

// Public key delivered when signing was activated for your API key.
const publicKeyPem = "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...\n-----END PUBLIC KEY-----";

// A single route from a swap response
const route = {
  tx: {
    to: "0x569a904F8478c66fD495d2B4E8e272B6507feDB3",
    from: "0x569a904F8478c66fD495d2B4E8e272B6507feDB3",
    gas: "0x5208",
    gasPrice: "0x1805fa7",
    value: "2000000000000000",
    data: "0x",
  },
  meta: {
    signedTx: "NjFhNGM1ZTJmMWIzZC4uLg....", // base64url(SHA-256 hex digest of the tx)
    signature: "cceWPI6Ak-....",            // base64url ES256 signature (raw r || s)
  },
};

async function verifyRoute(route: {
  tx: unknown;
  meta: { signedTx: string; signature: string };
}): Promise<void> {
  const { signedTx, signature } = route.meta;

  // Import the public key (PEM SubjectPublicKeyInfo).
  const ecPublicKey = await jose.importSPKI(publicKeyPem, "ES256");

  // 1) SIGNATURE CHECK — verify the Flattened JWS.
  //    jose reconstructs the signing input (eyJhbGciOiJFUzI1NiJ9.<signedTx>)
  //    and decodes the raw r || s signature internally.
  //    It throws on an invalid signature, wrong key, or unexpected algorithm.
  const { payload } = await jose.flattenedVerify(
    {
      payload: signedTx,
      signature,
      protected: Buffer.from(JSON.stringify({ alg: "ES256" })).toString("base64url"),
    },
    ecPublicKey,
  );

  // 2) BINDING CHECK — confirm signedTx is the digest of the tx you will broadcast.
  //    Serialize tx exactly as SwapKit does (string as-is, otherwise JSON.stringify).
  const txString = typeof route.tx === "string" ? route.tx : JSON.stringify(route.tx);
  const expectedDigest = crypto.createHash("sha256").update(Buffer.from(txString)).digest("hex");

  // The verified JWS payload is the base64url-decoded signedTx, i.e. the hex digest string.
  const signedDigest = Buffer.from(payload).toString("utf8");
  if (signedDigest !== expectedDigest) {
    throw new Error("Digest mismatch — transaction does not match the signed payload");
  }

  // Both checks passed — the tx is authentic and unmodified. Safe to broadcast.
}

await verifyRoute(route);
```

> **Serialization note for the binding check.** SwapKit computes the digest over `JSON.stringify(tx)` (for object transactions) of the same `tx` it returns, so re-stringifying the `tx` you received reproduces it as long as key order is preserved (`JSON.parse` → `JSON.stringify` preserves insertion order in JS). In languages that do not preserve or canonicalize key order, compute the digest over the exact JSON bytes you received for `tx` rather than over a re-encoded object.

**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`](https://github.com/panva/jose): `jose.flattenedVerify({ protected, payload, signature }, key)`. This is what SwapKit uses; see the example above.
* **Python** — [`jwcrypto`](https://jwcrypto.readthedocs.io/) or [`joserfc`](https://jose.authlib.org/en/) (both actively maintained). Avoid `python-jose` — it is effectively unmaintained and has had CVEs.
* **Go** — [`go-jose`](https://github.com/go-jose/go-jose) (`jose.ParseSigned` / `JSONWebSignature.Verify`)
* **Java** — [`nimbus-jose-jwt`](https://connect2id.com/products/nimbus-jose-jwt) (`JWSObject` / `ECDSAVerifier`)
* **Rust** — [`josekit`](https://docs.rs/josekit/). (Note: the `jsonwebtoken` crate 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`                                                          |
| ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| [Node `crypto.verify`](https://nodejs.org/api/crypto.html#cryptoverifyalgorithm-data-key-signature-callback)                                   | raw `r \|\| s` via `{ key, dsaEncoding: "ieee-p1363" }` | none (default `"der"` would reject it)                                                        |
| [WebCrypto `SubtleCrypto.verify`](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/verify) (`{ name: "ECDSA", hash: "SHA-256" }`) | raw `r \|\| s`                                          | none                                                                                          |
| [Python `cryptography`](https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec/)                                                    | DER                                                     | `encode_dss_signature(r, s)`, then `public_key.verify(der, input, ec.ECDSA(hashes.SHA256()))` |
| [Go `crypto/ecdsa`](https://pkg.go.dev/crypto/ecdsa)                                                                                           | two big integers                                        | split `r`/`s`, `ecdsa.Verify(pub, sha256.Sum256(input)[:], r, s)` (or DER + `VerifyASN1`)     |
| [Java `Signature` `SHA256withECDSA`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/security/Signature.html)                | DER                                                     | convert `r \|\| s` to DER first                                                               |

Whichever path you choose, the binding check (recomputing the digest from `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 `signedTx` 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. To rotate, request a rotation through your SwapKit point of contact.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.swapkit.dev/spotlights/transaction-payload-signing.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
