# Knit API Documentation — full text Generated from https://docs.useknit.io. Every documentation page, concatenated. # Stablecoin payments infrastructure Collect payments to single-use addresses, hold balances in reusable wallets, disburse payouts on-chain, and keep your systems in sync with signed webhooks — through one JSON API. - [Quickstart](https://docs.useknit.io/quickstart) — Go from credentials to your first collection and payout in a few requests. - [Authentication](https://docs.useknit.io/authentication) — OAuth 2.0 client credentials, scopes, and IP allow-listing. - [How money moves](https://docs.useknit.io/architecture) — Where balances live and which calls credit or debit them. - [Webhooks](https://docs.useknit.io/webhooks) — Event payloads, signature verification, and retry behaviour. ## What you can build - **`Collections` (single-use deposit addresses)** — Create a one-time address and hosted payment link for an invoice or checkout. Confirmed proceeds are credited to your API account automatically. See [Collections](https://docs.useknit.io/collections/create-a-collection). - **`API wallets` (reusable addresses)** — Long-lived addresses per network — useful for per-customer ledgers or recurring deposits. Deposits are recorded as wallet transactions. See [Wallets](https://docs.useknit.io/wallets/create-a-wallet). - **`API account` (spendable balance)** — A per-token balance that funds programmatic payouts. Collections and wallet deposits credit it; payouts debit it. See [API Account](https://docs.useknit.io/API-account/create-an-API-account). - **`Payouts` (on-chain disbursements)** — Send USDT or USDC to an external address. Knit checks the balance, holds it, and reports the final state over webhooks. See [Payouts](https://docs.useknit.io/payouts/create-a-single-payout). - **`Blockchain notifications` (address watching)** — Subscribe to incoming or outgoing activity on any address you care about, including addresses Knit does not hold. See [Blockchain Notifications](https://docs.useknit.io/blockchain-notifications). - **`Managed signing` (policy-governed signing)** — Create signing wallets, define spending policies, and submit transactions, typed data, or messages for signature. See [Managed Signing](https://docs.useknit.io/managed-signing). ## Conventions Every endpoint in this reference lives under `/api/v1` and follows the same rules. - **Base URLs.** `https://api-prod.useknit.io` for production and `https://api-dev.useknit.io` for pre-production testing. - **Authentication.** OAuth 2.0 client credentials. Send `Authorization: Bearer ` on every request. See [Authentication](https://docs.useknit.io/authentication). - **JSON only.** Requests and responses are JSON. Send `Content-Type: application/json` on requests with a body. - **camelCase.** Send camelCase field names; responses come back camelCase too. See [Requests & responses](https://docs.useknit.io/conventions#field-casing) for the two places this does not hold. - **Shared envelope.** Every response — success or failure — has the same top-level shape. See [Requests & responses](https://docs.useknit.io/conventions). ```json filename="Response envelope" { "statusCode": 200, "message": "Collections fetched successfully", "data": {}, "success": true } ``` > **Note:** Access tokens are scoped. A request that reaches an endpoint without the required scope is rejected with `401` — it is never silently downgraded. The scope each endpoint needs is listed on its page. ## For AI agents These docs are published in plain text as well as HTML. | URL | Contents | | --- | -------- | | [`/llms.txt`](https://docs.useknit.io/llms.txt) | An index of every page, with descriptions | | [`/llms-full.txt`](https://docs.useknit.io/llms-full.txt) | Every page concatenated into one file | | `.md` | That page's Markdown source | Append `.md` to any documentation URL to get the Markdown — for example [`/authentication.md`](https://docs.useknit.io/authentication.md) or [`/payouts/create-a-single-payout.md`](https://docs.useknit.io/payouts/create-a-single-payout.md). All three are served with permissive CORS, so a browser-based agent can fetch them directly. ## Getting set up ### Onboard your business Create your business in the [Knit dashboard](https://dashboard.useknit.io) and complete KYB. Developer credentials become available once your business is approved. ### Create an OAuth client Go to **Business → Developer → OAuth Clients** and create a client with only the scopes your integration needs. Copy the client ID and secret — the secret is shown once. ### Allow-list your server IPs Add the outbound IP addresses of your servers in the dashboard. Requests from any other address are rejected before they reach the API. ### Configure your webhook endpoint Set your webhook URL and secret under **Business → Developer → Webhooks**. The endpoint must be publicly reachable over HTTP or HTTPS. ### Make your first call Request an access token and call an endpoint. The [Quickstart](https://docs.useknit.io/quickstart) walks through a complete collection and payout. # Quickstart This walks through a complete integration: authenticate, take a payment, and send one back out. Run it against `https://api-dev.useknit.io` first. > **Note:** Before you start you need an approved business, an OAuth client, your server IPs allow-listed, and a webhook URL configured — all set up in the [dashboard](https://dashboard.useknit.io). See [Authentication](https://docs.useknit.io/authentication) for the details. ### Get an access token ```bash filename="1. Authenticate" curl https://api-dev.useknit.io/oauth/token \ -H "Content-Type: application/json" \ -d '{ "grant_type": "client_credentials", "client_id": "", "client_secret": "" }' ``` Store `access_token` and reuse it until `expires_in` is nearly up. ```bash export KNIT_ACCESS_TOKEN="eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIs..." export KNIT_BASE_URL="https://api-dev.useknit.io" ``` ### Check which networks are live Collection and payout availability differ per network and per token, and change over time. Read them rather than hardcoding. ```bash filename="2. List networks" curl "$KNIT_BASE_URL/api/v1/networks" \ -H "Accept: application/json" ``` Use a network whose `payoutStatus` is `ACTIVE` for step 5, and one whose `collectionStatus` is `ACTIVE` for step 4. ### Create your API account The API account holds the balance your integration spends. Create one per token you plan to use. ```bash filename="3. Create the API account" curl -X POST "$KNIT_BASE_URL/api/v1/business-api-services-wallets" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "token": "USDT" }' ``` This creates the account with a zero balance. Fund it from the dashboard by transferring from your business wallet, or let a collection fund it in the next step. ### Collect a payment A collection issues a single-use address and a hosted payment link. It expires 30 minutes after creation. ```bash filename="4. Create a collection" curl -X POST "$KNIT_BASE_URL/api/v1/collections" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "network": "MATIC_MAINNET", "token": "USDT", "tokenAmount": 25, "confirmationThreshold": 10, "merchantCallbackUrl": "https://example.com/webhooks/knit", "merchantRedirectUrl": "https://example.com/thanks" }' ``` Send your customer to `data.paymentLinkUrl`, or show them `data.address` directly. When the deposit confirms, the proceeds credit your API account and a `COLLECTION_SUCCESSFUL` webhook is delivered. ### Send a payout ```bash filename="5. Create a payout" curl -X POST "$KNIT_BASE_URL/api/v1/payouts" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "network": "MATIC_MAINNET", "token": "USDT", "amount": 10, "toAddress": "0x56adfcc254ab3b8142a275c1837bcffaff5aa38b", "merchantReference": "INV-2045" }' ``` `merchantReference` is required and must be unique across your payouts — it is your idempotency handle. Reuse it when retrying a request whose outcome you are unsure of. ### Verify the webhook Every delivery carries an `X-Signature` header: the HMAC-SHA256 of the raw request body, keyed with your webhook secret, hex-encoded. ```js filename="6. Verify a delivery" import crypto from "node:crypto"; // `rawBody` must be the unparsed request body, byte for byte. function isFromKnit(rawBody, signatureHeader, secret) { const expected = crypto .createHmac("sha256", secret) .update(rawBody) .digest("hex"); const a = Buffer.from(expected, "utf8"); const b = Buffer.from(signatureHeader ?? "", "utf8"); return a.length === b.length && crypto.timingSafeEqual(a, b); } ``` Respond `2xx` as soon as the signature checks out, then do your work asynchronously. See [Webhooks](https://docs.useknit.io/webhooks) for retry behaviour and the full event list. ## Next steps - [Collections](https://docs.useknit.io/collections/create-a-collection) — Fields, filters, and the collection lifecycle. - [Payouts](https://docs.useknit.io/payouts/create-a-single-payout) — Validation rules, supported networks, and status tracking. - [Webhooks](https://docs.useknit.io/webhooks) — Signature verification, retries, and every event payload. - [Errors](https://docs.useknit.io/conventions) — Status codes, validation errors, and safe retries. # Authentication Every `/api/v1` endpoint is authenticated with an **OAuth 2.0 client credentials** access token. Send it as a bearer token on each request. ```bash filename="Authenticated request" curl https://api-prod.useknit.io/api/v1/payouts \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" ``` > **Warning:** **API keys are no longer accepted.** The `X-API-KEY` header is not a valid credential on any `/api/v1` endpoint — requests that rely on it are rejected with `401`. If you are still sending one, migrate to OAuth client credentials. ## Environments | Environment | Base URL | | ----------- | -------- | | Production | `https://api-prod.useknit.io` | | Development / sandbox | `https://api-dev.useknit.io` | Credentials are issued per environment and are not interchangeable. ## Create an OAuth client In the Knit dashboard go to **Business → Developer → OAuth Clients** and create a client. Choose only the scopes the integration needs — you can create several clients so that, for example, a reporting service holds read scopes only. Copy the client ID and the plain secret when they are shown. The secret is not retrievable afterwards; if you lose it, rotate the client to issue a new one. > **Note:** Creating and rotating clients is a dashboard action. There is no public API for provisioning credentials. ## Request an access token `POST https://api-prod.useknit.io/oauth/token` Exchange the client ID and secret for a bearer token using the standard `client_credentials` grant. ```bash filename="Request a token" curl https://api-prod.useknit.io/oauth/token \ -H "Content-Type: application/json" \ -d '{ "grant_type": "client_credentials", "client_id": "", "client_secret": "" }' ``` ```json filename="Token response" { "token_type": "Bearer", "expires_in": 7200, "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIs..." } ``` > **Warning:** Read the lifetime from `expires_in` on each response rather than hardcoding it. Cache the token until shortly before it expires and request a new one on demand — do not mint a token per API call. ### Requesting narrower scopes Omit the `scope` parameter and the token is issued with the client's full set of scopes. Pass an explicit space-separated `scope` to mint a token that is narrower than the client — useful for short-lived tokens handed to a subsystem. ```bash filename="Narrower token" curl https://api-prod.useknit.io/oauth/token \ -H "Content-Type: application/json" \ -d '{ "grant_type": "client_credentials", "client_id": "", "client_secret": "", "scope": "collections:read payouts:read" }' ``` A token can only ever narrow the client's grant. Requesting a scope the client does not hold fails with `401 OAuth token contains scopes not granted to this client`. ## Scopes Scopes follow a `resource:action` pattern. | Scope | Grants | | ----- | ------ | | `collections:read` | View and list collections | | `collections:write` | Create, update, and delete collections | | `payouts:read` | View and list payouts | | `payouts:write` | Create payouts | | `wallets:read` | View and list API wallets and their transactions | | `wallets:write` | Create, update, and delete API wallets | | `payout-wallets:read` | View API account balances | | `payout-wallets:write` | Create API accounts | | `managed-signing:read` | View managed signing wallets, policies, requests, and audit events | | `managed-signing:write` | Create and approve managed signing wallets, policies, and requests | | `blockchain-notifications:read` | View address notification subscriptions | | `blockchain-notifications:write` | Create, update, and delete address notification subscriptions | | `custody:read` | View custody owners, wallets, policies, signing requests, and operations | | `custody:write` | Create custody resources and confirm or execute operations | | `*` | Full access to every resource | Two rules apply when a scope is checked: - **Write implies read.** A client holding `payouts:write` can also call `GET /api/v1/payouts`. The reverse is not true. - **`*` satisfies everything.** Grant it only when a client genuinely needs the whole surface. A request that reaches an endpoint without a satisfying scope is rejected: ```json filename="401 — insufficient scope" { "statusCode": 401, "message": "Insufficient permissions. This action requires scope: payouts:write", "data": null, "success": false } ``` ## IP allow-listing Access tokens are only accepted from IP addresses on your business's allow list. Register the outbound IPs of every environment that will call the API — staging included — in the dashboard before going live. ```json filename="401 — IP not allow-listed" { "statusCode": 401, "message": "IP address not whitelisted", "data": null, "success": false } ``` If your servers sit behind a NAT gateway or egress proxy, allow-list the gateway's address rather than the instance addresses. ## Authentication errors | Status | Message | What it means | | ------ | ------- | ------------- | | `401` | `Authentication required. Provide a Bearer token.` | No `Authorization` header was sent | | `401` | `Invalid or expired OAuth token` | The token is malformed, revoked, or past `expires_in` | | `401` | `OAuth client not found or has been revoked` | The client behind the token no longer exists | | `401` | `IP address not whitelisted` | The caller's IP is not on the business allow list | | `401` | `Insufficient permissions. This action requires scope: …` | The token lacks the scope the endpoint requires | ## Credential hygiene - Issue a separate client per application and per environment so a compromised credential can be revoked without a wider outage. - Grant the narrowest scope set that works. Prefer several small clients over one `*` client. - Store secrets in a secrets manager, never in source control or client-side code. These credentials are for server-to-server use only. - Rotate on a schedule and immediately after any suspected exposure. Rotation issues a new secret without changing the client's scopes. # How money moves Knit separates the funds you hold from the funds your integration can spend. Understanding that split is the fastest way to reason about balances, payout failures, and webhook side effects. ## The two balances - **`Business wallet` (treasury)** — Your main balance. Deposits from the dashboard land here, and manual withdrawals are made from here. Programmatic API calls never spend from it. - **`API account` (per-token spendable balance)** — A balance per token (USDT, USDC) that funds API activity. Collections and wallet deposits credit it; payouts debit it. This is the balance your integration actually draws on. Collections, API wallets, and payouts do not carry their own spendable balances. Every movement they represent settles against the API account. ```mermaid flowchart TD payer(["Customer"]) coll["Collection
single-use address"] wal["API wallet
reusable address"] api["API account
per token"] biz["Business wallet
treasury"] out(["External address"]) payer -->|pays| coll payer -->|deposits| wal coll -->|credits| api wal -->|credits| api biz <-->|dashboard transfer| api api -->|POST /payouts| out classDef balance fill:#4c38cb,stroke:#4c38cb,color:#ffffff; classDef route fill:transparent,stroke:#4c38cb,stroke-width:1.5px; classDef outside fill:transparent,stroke:#9aa0aa,stroke-dasharray:4 3; class biz,api balance; class coll,wal route; class payer,out outside; ``` The two filled boxes are the balances. Everything else is a route value passes through. Everything with an arrow into the API account increases what you can pay out. Only the dashboard transfer moves value between the two balances — no API call does. > **Note:** Where a response includes `balanceBefore` and `balanceAfter`, those values describe the API account ledger for that token. Watching them is the most direct way to know how much runway your integration has left. ## The flows ### Fund the API account Move USDT or USDC from your business wallet into the API account from the dashboard. This is deliberately a manual step, so you control the ceiling on what programmatic flows can spend. Create the account first if it does not exist — [`POST /api/v1/business-api-services-wallets`](https://docs.useknit.io/API-account/create-an-API-account) creates the record with a zero balance; it does not move funds. ### Collections credit it Each [collection](https://docs.useknit.io/collections/create-a-collection) issues a single-use address. When the deposit is confirmed on-chain, the proceeds are credited to the API account for that token. No transfer step is needed. ### API wallet deposits credit it Deposits into a reusable [API wallet](https://docs.useknit.io/wallets/create-a-wallet) are recorded as wallet transactions and credited to the API account, so the funds are immediately available for payouts. ### Payouts debit it [`POST /api/v1/payouts`](https://docs.useknit.io/payouts/create-a-single-payout) checks the API account balance for the requested token, holds the amount, and submits the transfer. If the balance is short, the payout is rejected with `400` and nothing is held. ### Return unused balance To move value back to the business wallet, use the transfer action in the dashboard. ## A collection, end to end Where the webhooks land relative to the money moving: ```mermaid sequenceDiagram autonumber participant You participant Knit participant Chain You->>Knit: POST /collections Knit-->>You: 200 · address + link Knit->>You: COLLECTION_CREATED Note over You: show link to customer Chain-->>Knit: deposit seen Note over Knit,Chain: waits for confirmationThreshold Knit->>You: COLLECTION_CONFIRMED Knit->>Knit: credit API account Knit->>You: COLLECTION_SUCCESSFUL Note over You: fulfil the order here ``` ## A payout, end to end ```mermaid sequenceDiagram autonumber participant You participant Knit participant Chain You->>Knit: POST /payouts alt balance is short Knit-->>You: 400 · Insufficient balance Note over Knit: nothing is held else balance is sufficient Knit-->>You: 201 · PENDING Knit->>Knit: hold amount Knit->>Chain: submit transfer Note over Knit: PROCESSING Chain-->>Knit: confirmed Note over Knit: COMPLETED Knit->>You: PAYOUT_SUCCESSFUL end ``` Only `COMPLETED` produces a webhook. A payout that ends `FAILED` is discovered by reading it back — see [Get payout status](https://docs.useknit.io/payouts/get-payout-status). ## Request lifecycle Every authenticated request goes through the same three checks before it reaches an endpoint: ```mermaid flowchart LR req["Request"] --> tok{"Valid
bearer token?"} tok -->|no| e1["401"] tok -->|yes| ip{"IP on the
allow list?"} ip -->|no| e2["401"] ip -->|yes| sc{"Token has the
required scope?"} sc -->|no| e3["401"] sc -->|yes| ok["Endpoint runs"] classDef bad fill:transparent,stroke:#b3261e,color:#b3261e; classDef good fill:#4c38cb,stroke:#4c38cb,color:#fff; class e1,e2,e3 bad; class ok good; ``` 1. **Token.** The bearer token is validated and resolved to an OAuth client and the business it belongs to. 2. **Network.** The caller's IP must be on the business allow list. 3. **Scope.** The token must satisfy the scope the route requires, for example `payouts:write` for `POST /api/v1/payouts`. All three failures are `401`, with a message naming the check that failed. See [Authentication](https://docs.useknit.io/authentication) for the exact strings. ## Observability - **Webhooks.** Every significant state change is delivered to your endpoint, signed with your webhook secret, and retried on failure. See [Webhooks](https://docs.useknit.io/webhooks). - **Dashboard.** Request volume, success and error rates, collection and payout history, and webhook delivery attempts are all visible in the dashboard. - **Reconciliation.** Every collection and payout carries a stable `id`, and payouts additionally carry your own `merchantReference`, so you can reconcile against your ledger without relying on webhook ordering. ## Response format All endpoints return the same envelope, so you can parse success and failure identically. See [Requests & responses](https://docs.useknit.io/conventions) for status codes and validation details. ```json filename="Envelope" { "statusCode": 200, "message": "Payout created successfully", "data": {}, "success": true } ``` # Requests & responses Every Knit endpoint follows the same rules for how you send data and what comes back. Read this once and the rest of the reference is mechanical. ## Field casing **Send camelCase.** It matches what the API returns, so one convention covers both directions. ```json filename="Send this" { "tokenAmount": 2500, "confirmationThreshold": 10, "merchantCallbackUrl": "https://example.com/webhooks/knit" } ``` Top-level fields are mapped to their internal names before validation, so snake_case is also accepted — `token_amount` works exactly like `tokenAmount`. Both forms are supported, but pick one and stay consistent; camelCase is the form documented on every endpoint page. > **Warning:** There is one exception, and it fails **silently**. The query parameters on [List wallet transactions](https://docs.useknit.io/wallets/get-single-wallet-transactions) are snake_case only: `per_page`, `date_from`, `date_to`. Sending `perPage` or `dateFrom` there is not an error — the parameter is simply ignored and you get unfiltered, default-paginated results. ### Nested objects The camelCase mapping applies to top-level fields. Nested objects are converted only where an endpoint says so: | Where | Behaviour | | ----- | --------- | | `rules` on [managed signing policies](https://docs.useknit.io/managed-signing/create-policy) | Converted recursively — send camelCase throughout (`maxTransferAmount`, `rateLimits.windowSec`) | | `payload` on [signing requests](https://docs.useknit.io/managed-signing/create-signing-request) | Converted recursively — send camelCase (`txOverrides`, `typedData`) | | `payload.typedData.types` and `payload.typedData.message` | **Passed through untouched.** EIP-712 requires exact key names, so send them exactly as your signing counterparty specified | | `metadata` on [notification subscriptions](https://docs.useknit.io/blockchain-notifications/create-subscription) | **Stored verbatim.** Your keys come back byte-for-byte on every event | ## Response envelope Every response — success or failure — shares the same top-level shape. ```json filename="Success" { "statusCode": 200, "message": "Collections fetched successfully", "data": [], "success": true } ``` - **`statusCode` (integer)** — Mirrors the HTTP status code. - **`message` (string)** — A human-readable summary. Useful in logs; do not branch on its exact text. - **`data` (object | array | null)** — The payload. `null` on errors and on endpoints that return nothing. - **`success` (boolean)** — `true` only for 2xx responses. - **`pagination` (object)** — Present only on paginated endpoints. See [Pagination](#pagination). > **Warning:** The status field is named `statusCode`, not `status`. A `status` key appearing inside `data` is a resource's own state — a payout's `PENDING`, say — which is a different thing entirely. Response keys are always camelCase, including nested ones. The one place this does **not** hold is webhook payloads, which are delivered by a different path — see [Webhooks](https://docs.useknit.io/webhooks) for the per-event casing. ## Status codes | Status | When you'll see it | | ------ | ------------------ | | `200` | The request succeeded | | `201` | A resource was created | | `400` | **Validation failed**, or the request was well-formed but could not be fulfilled — insufficient balance, an unsupported network, a downstream failure | | `401` | Missing, invalid, or expired token; IP not allow-listed; token lacks the required scope | | `403` | Authenticated, but the resource belongs to another business | | `404` | The resource does not exist, or is not visible to your business | | `409` | The resource already exists — for example a second API account for the same token | | `422` | Query-parameter validation failed on a list endpoint | | `500` | An unexpected error on our side. Retry with backoff; if it persists, contact support | ## Validation errors Validation failures come back in **two different shapes** depending on whether the endpoint validates a request body or query parameters. Branch on the HTTP status, and read `errors` — it is present on both. ### Request bodies — `400` Almost every endpoint. The body is trimmed to `message` and `errors`; the usual `statusCode`, `data`, and `success` keys are **not** present. ```json filename="400 — body validation failed" { "message": "Callback URL must be a valid URL", "errors": { "merchantCallbackUrl": ["Callback URL must be a valid URL"], "merchantReference": ["The merchant reference has already been taken."] } } ``` - **`message` (string)** — The first error message, for logging. - **`errors` (object)** — Field name to array of messages. Keys are **camelCase**, matching what you sent — so you can map them straight back onto your form fields. > **Warning:** A duplicate `merchantReference` on [Create a payout](https://docs.useknit.io/payouts/create-a-single-payout) surfaces here, as a `400` with an `errors.merchantReference` entry — not as a `409`. That is the response that tells you a retry was correctly rejected as a duplicate. ### Query parameters — `422` Only [List collections](https://docs.useknit.io/collections/get-all-collections) and [List wallet transactions](https://docs.useknit.io/wallets/get-single-wallet-transactions). These return the full envelope with `errors` alongside it. ```json filename="422 — query validation failed" { "statusCode": 422, "message": "The given data was invalid.", "data": null, "errors": { "perPage": ["The per page field must not be greater than 100."] }, "success": false } ``` ### Handling both ```js filename="One handler for both shapes" async function call(path, init) { const res = await fetch(`https://api-prod.useknit.io${path}`, init); const body = await res.json(); if (res.status === 400 || res.status === 422) { // `errors` is present on both shapes; the envelope is not. throw new ValidationError(body.message, body.errors ?? {}); } if (!res.ok) throw new ApiError(body.message, res.status); return body.data; } ``` ## Pagination Endpoints that paginate accept `page` and `per_page` and return a `pagination` object next to `data`. - **`page` (integer, default `1`)** — The page to fetch. Minimum `1`. - **`per_page` (integer, default `15`)** — Items per page. Minimum `1`, maximum `100`. ```json filename="Paginated response" { "statusCode": 200, "message": "Wallet transactions fetched successfully", "data": [], "pagination": { "totalItems": 128, "page": 2, "perPage": 25, "currentPage": 2, "lastPage": 6 }, "success": true } ``` > **Note:** Note the asymmetry: you send `per_page`, and the response echoes it back as `perPage`. Request parameters on this endpoint are snake_case; response keys are always camelCase. Not every list endpoint paginates. Where `pagination` is absent, the endpoint returned the full result set — each endpoint page states which applies. ## Retrying safely - **Reads** (`GET`) are safe to retry unconditionally. - **Payouts** are protected by `merchantReference`, which must be unique. Reuse the same reference when retrying a request whose outcome you are unsure of — a duplicate is rejected with `400` instead of sending twice. Only generate a new reference for a genuinely new payout. - **Managed signing requests** accept an `idempotencyKey` in the body; replaying the same key returns the original request rather than creating another. - **`5xx` and network timeouts** should be retried with exponential backoff. A timeout does not mean the request was rejected — re-read the resource before assuming it was not created. # List supported networks Returns every network Knit supports, the tokens available on each, and whether collections and payouts are currently enabled for them. `GET https://api-prod.useknit.io/api/v1/networks` - Auth: `None — public endpoint` > **Warning:** Availability changes. A chain can be paused for payouts while still accepting collections, and individual tokens can be toggled independently of their network. Read this endpoint at startup or on a schedule rather than hardcoding a list. ## Request No authentication is required. ```bash filename="cURL" curl "https://api-prod.useknit.io/api/v1/networks" \ -H "Accept: application/json" ``` ## Response ```json filename="200 OK" { "statusCode": 200, "message": "Networks fetched successfully", "data": [ { "id": 2, "name": "Polygon", "identifier": "MATIC_MAINNET", "logo": "https://demo.useknit.io/coins/polygon.svg", "collectionStatus": "ACTIVE", "payoutStatus": "ACTIVE", "tokens": [ { "id": 1, "name": "USDT", "symbol": "USDT", "logo": "https://demo.useknit.io/coins/usdt.svg", "address": "0xc2132d05d31c914a87c6611c10748aeb04b58e8f", "collectionStatus": "ACTIVE", "payoutStatus": "ACTIVE" }, { "id": 2, "name": "USDC", "symbol": "USDC", "logo": "https://demo.useknit.io/coins/usdc.svg", "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", "collectionStatus": "ACTIVE", "payoutStatus": "ACTIVE" } ] }, { "id": 1, "name": "Ethereum (ERC 20)", "identifier": "ETHEREUM_MAINNET", "logo": "https://demo.useknit.io/coins/ethereum.svg", "collectionStatus": "ACTIVE", "payoutStatus": "INACTIVE", "tokens": [ { "id": 1, "name": "USDT", "symbol": "USDT", "logo": "https://demo.useknit.io/coins/usdt.svg", "address": "0xdac17f958d2ee523a2206206994597c13d831ec7", "collectionStatus": "ACTIVE", "payoutStatus": "INACTIVE" } ] } ], "success": true } ``` ## Fields - **`identifier` (string)** — The value to send as `network` in every other request — for example `MATIC_MAINNET`. Use this, not `name`. - **`name` (string)** — A display label for your UI. - **`collectionStatus` (string)** — `ACTIVE` or `INACTIVE`. Whether [collections](https://docs.useknit.io/collections/create-a-collection) can be created on this network. - **`payoutStatus` (string)** — `ACTIVE` or `INACTIVE`. Whether [payouts](https://docs.useknit.io/payouts/create-a-single-payout) can be sent on this network. - **`tokens` (object[])** — The tokens on this network, each with its own `collectionStatus` and `payoutStatus`. A token can be inactive on an otherwise active network — so check both levels. - **`tokens[].address` (string)** — The token's contract address on that chain. > **Note:** Use this endpoint to drive the network and token pickers in your product. It keeps your UI in step with what the API will actually accept, and avoids showing customers an option that will fail at submission. ## Errors | Status | Cause | | ------ | ----- | | `400` | The network list could not be retrieved. Retry with backoff | # Create a collection Creates a **single-use** deposit address and a hosted payment link for one payment. When the deposit is confirmed on-chain, the proceeds are credited to your API account. `POST https://api-prod.useknit.io/api/v1/collections` - Required scope: `collections:write` - Auth: `Bearer token` > **Warning:** A collection address expires **30 minutes** after creation and accepts one payment. Create it when your customer is ready to pay, not when the invoice is drafted — and create a fresh one if the window lapses. ## Body - **`network` (string, required)** — The chain the payment will arrive on. Must be a network whose `collectionStatus` is `ACTIVE` in [`GET /api/v1/networks`](https://docs.useknit.io/networks/get-supported-networks). - **`token` (string, required)** — The token being collected, for example `USDT` or `USDC`. Must be active for collections on the chosen network. - **`tokenAmount` (number, required)** — The amount to collect, in token units. Must be greater than zero. - **`confirmationThreshold` (integer, required)** — How many on-chain confirmations to wait for before the collection is treated as confirmed. Must be greater than zero. Higher values trade settlement speed for finality — pick a value appropriate to the network and the size of the payment. - **`merchantCallbackUrl` (string, required)** — Where this collection's webhooks are delivered. Must be a publicly reachable `http://` or `https://` URL — private, loopback, and internal addresses are rejected. - **`merchantRedirectUrl` (string, optional)** — Where the hosted payment page sends the customer after payment. Must be a valid `http://` or `https://` URL if supplied. > **Note:** `merchantCallbackUrl` is a delivery target, not a label. Collection webhooks go to this URL; only if it is somehow absent do they fall back to the webhook URL configured on your business. ## Request ```bash filename="cURL" curl -X POST "https://api-prod.useknit.io/api/v1/collections" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "network": "MATIC_MAINNET", "token": "USDT", "tokenAmount": 2500, "confirmationThreshold": 10, "merchantCallbackUrl": "https://example.com/webhooks/knit", "merchantRedirectUrl": "https://example.com/thanks" }' ``` ## Response Returns `200` with the created collection. ```json filename="200 OK" { "statusCode": 200, "message": "Collection created successfully", "data": { "id": "9cddd3ad-c2a8-463c-a703-158b900beea8", "network": "MATIC_MAINNET", "token": "USDT", "address": "0x3761f3504104f4faa8959963a5d8dce89989d45b", "status": "PENDING", "tokenAmountRequested": 2500, "tokenAmount": 2500, "tokenToUsd": 1, "feeInUsd": 0, "feeByToken": 0, "confirmationThreshold": 10, "merchantCallbackUrl": "https://example.com/webhooks/knit", "merchantRedirectUrl": "https://example.com/thanks", "paymentLinkUrl": "https://checkout.collection.useknit.io/9cddd3ad-c2a8-463c-a703-158b900beea8", "expiresAt": "2024-08-27T14:02:10.000000Z", "createdAt": "2024-08-27T13:32:10.000000Z", "updatedAt": "2024-08-27T13:32:10.000000Z" }, "success": true } ``` - **`address` (string)** — The single-use deposit address. Show it to the customer, or embed it in a QR code. - **`paymentLinkUrl` (string)** — A hosted checkout page for this collection. The simplest integration is to redirect the customer here. - **`expiresAt` (string)** — ISO-8601 timestamp, 30 minutes after creation. - **`status` (string)** — Starts at `PENDING`. See the lifecycle below. ## Lifecycle | Status | Meaning | | ------ | ------- | | `PENDING` | Address issued, waiting for a deposit | | `SUCCESSFUL` | Deposit received and credited to your API account | | `SUCCESSFUL_PARTIAL` | Less than the requested amount arrived; the received amount was credited | | `FAILED` | The payment could not be completed | Each transition emits a webhook: 1. `COLLECTION_CREATED` — fired immediately on creation. 2. `COLLECTION_CONFIRMED` — fired once your `confirmationThreshold` is reached. 3. `COLLECTION_SUCCESSFUL` — fired when the funds are credited. 4. `COLLECTION_FAILED` — fired if the payment cannot be completed. See [Webhooks](https://docs.useknit.io/webhooks) for payloads and signature verification. ## Errors | Status | Cause | | ------ | ----- | | `401` | Missing or invalid token, IP not allow-listed, or missing `collections:write` | | `400` | A required field is missing, `tokenAmount` or `confirmationThreshold` is not greater than zero, or a URL is invalid or not publicly reachable | | `500` | The collection could not be created. Retry; if it persists, contact support | # Delete a collection Removes a collection from your business. Use it to retire a payment link that was created in error or is no longer needed. `DELETE https://api-prod.useknit.io/api/v1/collections/{id}` - Required scope: `collections:write` - Auth: `Bearer token` > **Warning:** Deleting a collection does not reverse anything on-chain. Do not delete a collection whose address may still receive a deposit — let it expire instead. ## Path parameters - **`id` (string, required)** — The collection's `id`. ## Request ```bash filename="cURL" curl -X DELETE "https://api-prod.useknit.io/api/v1/collections/9cddd3ad-c2a8-463c-a703-158b900beea8" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ## Response ```json filename="200 OK" { "statusCode": 200, "message": "Collection deleted successfully", "data": null, "success": true } ``` ## Errors | Status | Cause | | ------ | ----- | | `401` | Missing or invalid token, IP not allow-listed, or missing `collections:write` | | `404` | No collection with that ID belongs to your business | # List collections Returns your business's collections, newest first. `GET https://api-prod.useknit.io/api/v1/collections` - Required scope: `collections:read` - Auth: `Bearer token` ## Query parameters All filters are optional and combine with AND. - **`status` (string, optional)** — Exact match on collection status, for example `PENDING` or `SUCCESSFUL`. - **`network` (string, optional)** — Exact match on network identifier, for example `MATIC_MAINNET`. - **`token` (string, optional)** — Exact match on token symbol, for example `USDT`. - **`minAmount` (number, optional)** — Only collections whose `tokenAmount` is greater than or equal to this. - **`maxAmount` (number, optional)** — Only collections whose `tokenAmount` is less than or equal to this. - **`startDate` (date, optional)** — Only collections created at or after this date. - **`endDate` (date, optional)** — Only collections created at or before this date. > **Note:** This endpoint returns the full filtered result set — there is no `pagination` object in the response. Narrow with `startDate` / `endDate` rather than fetching everything on a schedule. ## Request ```bash filename="cURL" curl -G "https://api-prod.useknit.io/api/v1/collections" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" \ --data-urlencode "status=SUCCESSFUL" \ --data-urlencode "token=USDT" \ --data-urlencode "startDate=2024-08-01" ``` ## Response ```json filename="200 OK" { "statusCode": 200, "message": "Collections fetched successfully", "data": [ { "id": "9cddd3ad-c2a8-463c-a703-158b900beea8", "network": "MATIC_MAINNET", "token": "USDT", "address": "0x3761f3504104f4faa8959963a5d8dce89989d45b", "tokenAmount": "2500.000000000000000000", "tokenAmountRequested": "2500.000000000000000000", "tokenAmountReceived": null, "tokenToUsd": "1.00000000", "feeInUsd": "0.00000000", "feeByToken": "0.000000000000000000", "fiatExchangeRateToUsd": null, "fiatAmountInUsd": null, "fiatAmount": null, "fiatAmountCurrency": null, "status": "PENDING", "transactionHash": null, "transactionBlockNumber": null, "numberOfConfirmations": null, "confirmationThreshold": 10, "merchantRedirectUrl": null, "merchantCallbackUrl": "https://example.com/webhooks/knit", "paymentLinkUrl": "https://checkout.collection.useknit.io/9cddd3ad-c2a8-463c-a703-158b900beea8", "expiresAt": "2024-08-27T14:02:10.000000Z", "createdAt": "2024-08-27T13:32:10.000000Z", "updatedAt": "2024-08-27T13:32:10.000000Z" } ], "success": true } ``` > **Note:** Decimal fields come back as strings to preserve full precision. Parse them with a decimal library rather than a float. ## Errors | Status | Cause | | ------ | ----- | | `401` | Missing or invalid token, IP not allow-listed, or missing `collections:read` | | `422` | A filter value has the wrong type, for example a non-date `startDate`. Query-parameter failures on this endpoint return the full envelope with `errors` — see [Requests & responses](https://docs.useknit.io/conventions#validation-errors) | # Retrieve a collection Fetches one collection belonging to your business. `GET https://api-prod.useknit.io/api/v1/collections/{id}` - Required scope: `collections:read` - Auth: `Bearer token` ## Path parameters - **`id` (string, required)** — The collection's `id`, as returned when it was created. ## Request ```bash filename="cURL" curl "https://api-prod.useknit.io/api/v1/collections/9cddd3ad-c2a8-463c-a703-158b900beea8" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ## Response ```json filename="200 OK" { "statusCode": 200, "message": "Collection fetched successfully", "data": { "id": "9cddd3ad-c2a8-463c-a703-158b900beea8", "network": "MATIC_MAINNET", "token": "USDT", "address": "0x3761f3504104f4faa8959963a5d8dce89989d45b", "tokenAmount": "2500.000000000000000000", "tokenAmountRequested": "2500.000000000000000000", "tokenAmountReceived": "2500.000000000000000000", "tokenToUsd": "1.00000000", "feeInUsd": "0.00000000", "feeByToken": "0.000000000000000000", "fiatExchangeRateToUsd": null, "fiatAmountInUsd": null, "fiatAmount": null, "fiatAmountCurrency": null, "status": "SUCCESSFUL", "transactionHash": "0xb480ed44a275f042e482a78d9c5b54fcf612441c", "transactionBlockNumber": 61234567, "numberOfConfirmations": 12, "confirmationThreshold": 10, "merchantRedirectUrl": "https://example.com/thanks", "merchantCallbackUrl": "https://example.com/webhooks/knit", "paymentLinkUrl": "https://checkout.collection.useknit.io/9cddd3ad-c2a8-463c-a703-158b900beea8", "expiresAt": "2024-08-27T14:02:10.000000Z", "createdAt": "2024-08-27T13:32:10.000000Z", "updatedAt": "2024-08-27T13:35:44.000000Z" }, "success": true } ``` > **Note:** Prefer [webhooks](https://docs.useknit.io/webhooks) over polling this endpoint. If you do poll — for example to reconcile after downtime — back off rather than looping tightly. ## Errors | Status | Cause | | ------ | ----- | | `401` | Missing or invalid token, IP not allow-listed, or missing `collections:read` | | `404` | No collection with that ID belongs to your business | # Update a collection Amends a collection that has not yet been paid. Send only the fields you want to change. `PUT https://api-prod.useknit.io/api/v1/collections/{id}` - Required scope: `collections:write` - Auth: `Bearer token` > **Warning:** Updating does not extend the 30-minute expiry and does not issue a new address. Once a payment has been detected, create a new collection instead of amending this one. ## Path parameters - **`id` (string, required)** — The collection's `id`. ## Body Every field is optional; omitted fields are left unchanged. - **`network` (string, optional)** — A network that is active for collections. - **`token` (string, optional)** — A token that is active for collections on that network. - **`tokenAmount` (number, optional)** — Must be greater than zero. - **`confirmationThreshold` (integer, optional)** — Must be greater than zero. - **`merchantCallbackUrl` (string, optional)** — Must be a publicly reachable `http://` or `https://` URL. - **`merchantRedirectUrl` (string, optional)** — Must be a valid `http://` or `https://` URL, or `null` to clear it. ## Request ```bash filename="cURL" curl -X PUT "https://api-prod.useknit.io/api/v1/collections/9cddd3ad-c2a8-463c-a703-158b900beea8" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "tokenAmount": 3000, "merchantRedirectUrl": "https://example.com/checkout/complete" }' ``` ## Response ```json filename="200 OK" { "statusCode": 200, "message": "Collection updated successfully", "data": { "id": "9cddd3ad-c2a8-463c-a703-158b900beea8", "network": "MATIC_MAINNET", "token": "USDT", "tokenAmount": "3000.000000000000000000", "status": "PENDING", "merchantRedirectUrl": "https://example.com/checkout/complete", "updatedAt": "2024-08-27T13:41:02.000000Z" }, "success": true } ``` ## Errors | Status | Cause | | ------ | ----- | | `401` | Missing or invalid token, IP not allow-listed, or missing `collections:write` | | `404` | No collection with that ID belongs to your business | | `400` | A supplied field failed validation | # Create a wallet Provisions a reusable address you can hand out repeatedly — for a customer balance, a recurring subscriber, or any long-lived deposit destination. Unlike a [collection](https://docs.useknit.io/collections/create-a-collection), an API wallet does not expire and accepts any number of deposits. `POST https://api-prod.useknit.io/api/v1/wallets` - Required scope: `wallets:write` - Auth: `Bearer token` ## Supported networks | Network | Identifier | | ------- | ---------- | | Polygon | `MATIC_MAINNET` | | Polygon Amoy (testnet) | `MATIC_AMOY` | | BNB Smart Chain | `BSC_MAINNET` | | Ethereum | `ETHEREUM_MAINNET` | | Bitcoin | `BITCOIN_MAINNET` | | Tron | `TRON_MAINNET` | ## Body Provide either `network` or `networks` — not both. - **`network` (string, optional)** — A single network identifier. Required unless `networks` is supplied. - **`networks` (string[], optional)** — One or more network identifiers, for an address usable across compatible chains. Required unless `network` is supplied; must contain at least one entry. > **Warning:** Sending `network` and `networks` in the same request is a validation error. Pick one form. ## Request ```bash filename="Single network" curl -X POST "https://api-prod.useknit.io/api/v1/wallets" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "network": "MATIC_MAINNET" }' ``` ```bash filename="Multiple networks" curl -X POST "https://api-prod.useknit.io/api/v1/wallets" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "networks": ["MATIC_MAINNET", "BSC_MAINNET", "ETHEREUM_MAINNET"] }' ``` ## Response Returns `201` with the new wallet. ```json filename="201 Created" { "statusCode": 201, "message": "Wallet created successfully", "data": { "id": "d95143d2-c076-49da-b121-b3beed054bf3", "businessId": "a2667393-2c6c-43c9-a288-6167e2b2b591", "network": "MATIC_MAINNET", "networkId": "MATIC_MAINNET", "networks": ["MATIC_MAINNET"], "address": "0x1adb0a39bde00fa0957e519584cb5c51fefcb37f", "isActive": true, "createdAt": "2024-02-26T13:59:19.000000Z", "updatedAt": "2024-02-26T13:59:19.000000Z" }, "success": true } ``` - **`id` (string)** — Use this in every subsequent wallet request. - **`address` (string)** — The deposit address. Safe to display and reuse. - **`network` (string)** — The wallet's primary network. When you supplied `networks`, this is the first entry. - **`networks` (string[])** — Every network the address is usable on. ## What happens on a deposit Deposits into an API wallet are recorded as [wallet transactions](https://docs.useknit.io/wallets/get-single-wallet-transactions) and credited to your API account, so the funds are immediately available to [payouts](https://docs.useknit.io/payouts/create-a-single-payout). A `WALLET_FUNDING_SUCCESSFUL` webhook is delivered for each confirmed deposit. ## Errors | Status | Cause | | ------ | ----- | | `401` | Missing or invalid token, IP not allow-listed, or missing `wallets:write` | | `400` | Neither `network` nor `networks` was supplied, both were supplied, or a network identifier is not supported | # Delete a wallet Removes an API wallet from your business. `DELETE https://api-prod.useknit.io/api/v1/wallets/{id}` - Required scope: `wallets:write` - Auth: `Bearer token` > **Warning:** Deleting a wallet does not close anything on-chain, and it does not reverse funds already credited. Stop handing the address out to customers before you delete it — deposits sent to a deleted wallet's address may not be attributable. ## Path parameters - **`id` (string, required)** — The wallet's `id`. ## Request ```bash filename="cURL" curl -X DELETE "https://api-prod.useknit.io/api/v1/wallets/d95143d2-c076-49da-b121-b3beed054bf3" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ## Response ```json filename="200 OK" { "statusCode": 200, "message": "Wallet deleted successfully", "data": null, "success": true } ``` ## Errors | Status | Cause | | ------ | ----- | | `401` | Missing or invalid token, IP not allow-listed, or missing `wallets:write` | | `404` | No wallet with that ID belongs to your business | # List wallets Returns all API wallets your business has created, across every network. `GET https://api-prod.useknit.io/api/v1/wallets` - Required scope: `wallets:read` - Auth: `Bearer token` ## Request ```bash filename="cURL" curl "https://api-prod.useknit.io/api/v1/wallets" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ## Response ```json filename="200 OK" { "statusCode": 200, "message": "Wallets fetched successfully", "data": [ { "id": "d95143d2-c076-49da-b121-b3beed054bf3", "businessId": "a2667393-2c6c-43c9-a288-6167e2b2b591", "network": "MATIC_MAINNET", "networkId": "MATIC_MAINNET", "networks": ["MATIC_MAINNET"], "address": "0x1adb0a39bde00fa0957e519584cb5c51fefcb37f", "isActive": true, "createdAt": "2024-02-26T13:59:19.000000Z", "updatedAt": "2024-02-26T13:59:19.000000Z" } ], "success": true } ``` > **Note:** This endpoint returns every wallet in one response. Cache the mapping between your customers and wallet IDs on your side rather than calling this on each request. ## Errors | Status | Cause | | ------ | ----- | | `401` | Missing or invalid token, IP not allow-listed, or missing `wallets:read` | # Retrieve a wallet Fetches one API wallet belonging to your business. `GET https://api-prod.useknit.io/api/v1/wallets/{id}` - Required scope: `wallets:read` - Auth: `Bearer token` ## Path parameters - **`id` (string, required)** — The wallet's `id`, as returned when it was created. ## Request ```bash filename="cURL" curl "https://api-prod.useknit.io/api/v1/wallets/d95143d2-c076-49da-b121-b3beed054bf3" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ## Response ```json filename="200 OK" { "statusCode": 200, "message": "Wallet fetched successfully", "data": { "id": "d95143d2-c076-49da-b121-b3beed054bf3", "businessId": "a2667393-2c6c-43c9-a288-6167e2b2b591", "network": "MATIC_MAINNET", "networkId": "MATIC_MAINNET", "networks": ["MATIC_MAINNET"], "address": "0x1adb0a39bde00fa0957e519584cb5c51fefcb37f", "isActive": true, "createdAt": "2024-02-26T13:59:19.000000Z", "updatedAt": "2024-02-26T13:59:19.000000Z" }, "success": true } ``` > **Note:** This returns the wallet record, not an on-chain balance. For the money that has moved through it, use [wallet transactions](https://docs.useknit.io/wallets/get-single-wallet-transactions). ## Errors | Status | Cause | | ------ | ----- | | `401` | Missing or invalid token, IP not allow-listed, or missing `wallets:read` | | `404` | No wallet with that ID belongs to your business | # List wallet transactions Returns the transactions recorded against one API wallet, with filtering and pagination. `GET https://api-prod.useknit.io/api/v1/wallets/{id}/transactions` - Required scope: `wallets:read` - Auth: `Bearer token` ## Path parameters - **`id` (string, required)** — The wallet's `id`. ## Query parameters > **Warning:** These parameters are **snake_case**, unlike the camelCase used everywhere else in the API. Sending `perPage`, `dateFrom`, or `dateTo` is not an error — the parameter is silently ignored and you get unfiltered results with the default page size. Spell them exactly as below. - **`page` (integer, optional, default `1`)** — The page to fetch. Minimum `1`. - **`per_page` (integer, optional, default `15`)** — Items per page. Minimum `1`, maximum `100`. - **`network` (string, optional)** — Filter to one network identifier. - **`token` (string, optional)** — Filter to one token symbol. - **`status` (string, optional)** — Filter to one transaction status, for example `SUCCESSFUL`. - **`date_from` (date, optional)** — Only transactions on or after this date. - **`date_to` (date, optional)** — Only transactions on or before this date. Must not be earlier than `date_from`. The response still uses camelCase, so you send `per_page` and read `perPage` back out of the `pagination` object. ## Request ```bash filename="cURL" curl -G "https://api-prod.useknit.io/api/v1/wallets/d95143d2-c076-49da-b121-b3beed054bf3/transactions" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" \ --data-urlencode "status=SUCCESSFUL" \ --data-urlencode "per_page=25" \ --data-urlencode "page=1" ``` ## Response This endpoint paginates, so the envelope carries a `pagination` object alongside `data`. ```json filename="200 OK" { "statusCode": 200, "message": "Wallet transactions fetched successfully", "data": [ { "id": "75229581-5709-4b0a-886d-12ea1328e6a7", "walletId": "d95143d2-c076-49da-b121-b3beed054bf3", "network": "MATIC_MAINNET", "token": "USDT", "fromAddress": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "toAddress": "0x1adb0a39bde00fa0957e519584cb5c51fefcb37f", "value": "1.00000000", "numberOfConfirmations": 12, "status": "SUCCESSFUL", "transactionHash": "0xa8ba7949431a95f71430ae6ce30aa5c2e488e6206996", "createdAt": "2024-02-23T18:20:40.000000Z", "updatedAt": "2024-02-23T18:20:40.000000Z" } ], "pagination": { "totalItems": 128, "page": 1, "perPage": 25, "currentPage": 1, "lastPage": 6 }, "success": true } ``` ## Errors | Status | Cause | | ------ | ----- | | `401` | Missing or invalid token, IP not allow-listed, or missing `wallets:read` | | `404` | No wallet with that ID belongs to your business | | `422` | `per_page` is above 100, or `date_to` is earlier than `date_from`. Query-parameter failures on this endpoint return the full envelope with `errors` — see [Requests & responses](https://docs.useknit.io/conventions#validation-errors) | # Update a wallet Amends an existing API wallet. Send only the fields you want to change. `PUT https://api-prod.useknit.io/api/v1/wallets/{id}` - Required scope: `wallets:write` - Auth: `Bearer token` > **Warning:** Updating a wallet does not re-provision its on-chain address. The `address` returned when the wallet was created stays the same. ## Path parameters - **`id` (string, required)** — The wallet's `id`. ## Body - **`network` (string, optional)** — The wallet's primary network identifier. - **`type` (string, optional)** — The wallet's type classification. ## Request ```bash filename="cURL" curl -X PUT "https://api-prod.useknit.io/api/v1/wallets/d95143d2-c076-49da-b121-b3beed054bf3" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "network": "MATIC_MAINNET" }' ``` ## Response ```json filename="200 OK" { "statusCode": 200, "message": "Wallet updated successfully", "data": { "id": "d95143d2-c076-49da-b121-b3beed054bf3", "businessId": "a2667393-2c6c-43c9-a288-6167e2b2b591", "network": "MATIC_MAINNET", "networkId": "MATIC_MAINNET", "networks": ["MATIC_MAINNET"], "address": "0x1adb0a39bde00fa0957e519584cb5c51fefcb37f", "isActive": true, "createdAt": "2024-02-26T13:59:19.000000Z", "updatedAt": "2024-03-04T09:12:41.000000Z" }, "success": true } ``` ## Errors | Status | Cause | | ------ | ----- | | `401` | Missing or invalid token, IP not allow-listed, or missing `wallets:write` | | `404` | No wallet with that ID belongs to your business | | `400` | A supplied field failed validation | # Create an API account Creates the per-token balance your integration spends from. You need one API account per token you plan to pay out in. `POST https://api-prod.useknit.io/api/v1/business-api-services-wallets` - Required scope: `payout-wallets:write` - Auth: `Bearer token` > **Warning:** This creates the account with a **zero balance** — it does not move any money. Fund it from the dashboard by transferring from your business wallet, or let [collections](https://docs.useknit.io/collections/create-a-collection) and [wallet deposits](https://docs.useknit.io/wallets/create-a-wallet) credit it. Payouts fail with `400` until there is enough balance. ## Body - **`token` (string, required)** — The token this account holds. One of `USDT` or `USDC`. ## Request ```bash filename="cURL" curl -X POST "https://api-prod.useknit.io/api/v1/business-api-services-wallets" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "token": "USDT" }' ``` ## Response Returns `201` with the new account. ```json filename="201 Created" { "statusCode": 201, "message": "Payout wallet created successfully", "data": { "id": "c7ddb254-5da1-4136-b19e-733a00a31c70", "businessId": "a2667393-2c6c-43c9-a288-6167e2b2b591", "token": "USDT", "amount": "0", "createdAt": "2024-02-22T14:16:23.000000Z", "updatedAt": "2024-02-22T14:16:23.000000Z" }, "success": true } ``` ## Errors | Status | Cause | | ------ | ----- | | `401` | Missing or invalid token, IP not allow-listed, or missing `payout-wallets:write` | | `409` | An API account already exists for that token — fetch it with [List API accounts](https://docs.useknit.io/API-account/get-all-API-accounts) instead | | `400` | `token` is missing or is not `USDT` or `USDC` | > **Note:** Creating accounts is idempotent in practice: a `409` means the account you wanted already exists, so it is safe to treat as success and read the existing account. # List API accounts Returns every API account your business holds, one per token, with its current balance. This is the balance [payouts](https://docs.useknit.io/payouts/create-a-single-payout) draw on. `GET https://api-prod.useknit.io/api/v1/business-api-services-wallets` - Required scope: `payout-wallets:read` - Auth: `Bearer token` ## Request ```bash filename="cURL" curl "https://api-prod.useknit.io/api/v1/business-api-services-wallets" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ## Response ```json filename="200 OK" { "statusCode": 200, "message": "Payout wallets fetched successfully", "data": [ { "id": "c7ddb254-5da1-4136-b19e-733a00a31c70", "businessId": "a2667393-2c6c-43c9-a288-6167e2b2b591", "token": "USDT", "amount": "99.897000000000000000", "createdAt": "2024-02-22T14:16:23.000000Z", "updatedAt": "2024-02-22T14:43:21.000000Z" } ], "success": true } ``` - **`amount` (string)** — The spendable balance in token units, as a decimal string. Parse it with a decimal library — the precision does not survive a float. > **Note:** Poll this before a batch of payouts, or alert on it falling below your expected daily volume. It is the single number that tells you whether programmatic disbursement will keep working. ## Errors | Status | Cause | | ------ | ----- | | `401` | Missing or invalid token, IP not allow-listed, or missing `payout-wallets:read` | # Retrieve an API account Fetches one API account belonging to your business. `GET https://api-prod.useknit.io/api/v1/business-api-services-wallets/{id}` - Required scope: `payout-wallets:read` - Auth: `Bearer token` ## Path parameters - **`id` (string, required)** — The API account's `id`. ## Request ```bash filename="cURL" curl "https://api-prod.useknit.io/api/v1/business-api-services-wallets/c7ddb254-5da1-4136-b19e-733a00a31c70" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ## Response ```json filename="200 OK" { "statusCode": 200, "message": "Payout Wallet fetched successfully", "data": { "id": "c7ddb254-5da1-4136-b19e-733a00a31c70", "businessId": "a2667393-2c6c-43c9-a288-6167e2b2b591", "token": "USDT", "amount": "99.950000000000000000", "createdAt": "2024-02-22T14:16:23.000000Z", "updatedAt": "2024-02-22T14:35:13.000000Z" }, "success": true } ``` ## Errors | Status | Cause | | ------ | ----- | | `401` | Missing or invalid token, IP not allow-listed, or missing `payout-wallets:read` | | `404` | No API account with that ID belongs to your business | # Create a payout Sends stablecoin from your API account to an external address. Knit checks the balance, holds the amount, and submits the transfer. `POST https://api-prod.useknit.io/api/v1/payouts` - Required scope: `payouts:write` - Auth: `Bearer token` ## Before you call 1. **The API account must exist and be funded.** [Create it](https://docs.useknit.io/API-account/create-an-API-account) for the token you are paying out, then fund it from the dashboard or let collections credit it. A short balance is rejected with `400`, and nothing is held. 2. **Your server IP must be allow-listed.** Requests from other addresses are rejected before reaching this endpoint. 3. **The destination address must be valid for the network.** Addresses are checked per chain; a malformed or wrong-chain address fails with `400`. ## Supported networks | Network | Identifier | | ------- | ---------- | | Polygon | `MATIC_MAINNET` | | Tron | `TRON_MAINNET` | | BNB Smart Chain | `BSC_MAINNET` | | Solana | `SOL_MAINNET` | > **Warning:** Payout availability is narrower than collection availability — a network that accepts collections may not support payouts. Check `payoutStatus` in [`GET /api/v1/networks`](https://docs.useknit.io/networks/get-supported-networks) before you build against a chain. ## Body - **`network` (string, required)** — One of the payout networks above. - **`token` (string, required)** — `USDT` or `USDC`. - **`amount` (number, required)** — The amount to send, in token units — not fiat. Minimum `0.01`. - **`toAddress` (string, required)** — The destination address. Validated against the format and checksum rules of the chosen network. - **`merchantReference` (string, required)** — Your own reference for this payout. Must be unique across your payouts — this is what makes retries safe. > **Warning:** `merchantReference` is **required**, not optional. Generate it from something stable on your side — an invoice ID, a ledger entry ID — so that replaying a request whose outcome you are unsure of is rejected as a duplicate rather than sending the money twice. ## Request ```bash filename="cURL" curl -X POST "https://api-prod.useknit.io/api/v1/payouts" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "network": "MATIC_MAINNET", "token": "USDT", "amount": 25.5, "toAddress": "0x56adfcc254ab3b8142a275c1837bcffaff5aa38b", "merchantReference": "INV-2045" }' ``` ## Response Returns `201`. The payout starts at `PENDING` — it has not settled on-chain yet. ```json filename="201 Created" { "statusCode": 201, "message": "Payout created successfully", "data": { "id": "8e5697e3-8265-455b-984a-0eb40e10b0f9", "businessId": "b7b93a40-4e18-4e97-9c5f-8a7a4fd0df92", "network": "MATIC_MAINNET", "token": "USDT", "amount": "25.500000000000000000", "toAddress": "0x56adfcc254ab3b8142a275c1837bcffaff5aa38b", "merchantReference": "INV-2045", "transactionHash": null, "status": "PENDING", "info": null, "createdAt": "2024-08-27T13:44:20.000000Z", "updatedAt": "2024-08-27T13:44:20.000000Z" }, "success": true } ``` ## Status lifecycle | Status | Meaning | | ------ | ------- | | `PENDING` | Accepted and queued. The balance has been held | | `PROCESSING` | Submitted for settlement; awaiting on-chain confirmation | | `COMPLETED` | Confirmed on-chain. `transactionHash` is populated and a `PAYOUT_SUCCESSFUL` webhook is delivered | | `FAILED` | Could not be settled. Check `info` for the reason | > **Note:** A failed payout is not retried automatically at the same `merchantReference`. To try again, submit a new payout with a **new** `merchantReference` once you have resolved the cause. Track completion with the [`PAYOUT_SUCCESSFUL` webhook](https://docs.useknit.io/webhooks/payout-successful) rather than polling. If you need to check state on demand — reconciling after an outage, say — use [Retrieve a payout](https://docs.useknit.io/payouts/get-single-payout) or [Get payout status](https://docs.useknit.io/payouts/get-payout-status). ## Errors | Status | Cause | | ------ | ----- | | `400` | A field failed validation: `amount` below `0.01`, an unsupported `network`, an invalid `toAddress`, or a `merchantReference` you have already used | | `400` | Insufficient balance in the API account for that token, or the payout could not be accepted | | `401` | Missing or invalid token, IP not allow-listed, or missing `payouts:write` | | `404` | `Wallet not found for the specified token` — no API account exists for that token yet | Both cases are `400`, but the bodies differ. A validation failure carries an `errors` object naming the offending fields: ```json filename="400 — validation failed" { "message": "The merchant reference has already been taken.", "errors": { "merchantReference": ["The merchant reference has already been taken."] } } ``` A rejected-but-valid request carries the standard envelope and no `errors`: ```json filename="400 — insufficient balance" { "statusCode": 400, "message": "Insufficient balance", "data": null, "success": false } ``` Branch on the presence of `errors` to tell them apart — see [Requests & responses](https://docs.useknit.io/conventions#validation-errors). # List payouts Returns the payouts belonging to your business. `GET https://api-prod.useknit.io/api/v1/payouts` - Required scope: `payouts:read` - Auth: `Bearer token` ## Request ```bash filename="cURL" curl "https://api-prod.useknit.io/api/v1/payouts" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ## Response ```json filename="200 OK" { "statusCode": 200, "message": "Payouts retrieved successfully", "data": [ { "id": "8e5697e3-8265-455b-984a-0eb40e10b0f9", "businessId": "b7b93a40-4e18-4e97-9c5f-8a7a4fd0df92", "network": "MATIC_MAINNET", "token": "USDT", "amount": "25.500000000000000000", "toAddress": "0x56adfcc254ab3b8142a275c1837bcffaff5aa38b", "merchantReference": "INV-2045", "transactionHash": "0xb0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f70819", "status": "COMPLETED", "info": null, "createdAt": "2024-08-27T13:44:20.000000Z", "updatedAt": "2024-08-27T13:47:03.000000Z" } ], "success": true } ``` > **Note:** Reconcile on `merchantReference` — it is the value you control and the one that stays stable across your own retries. ## Errors | Status | Cause | | ------ | ----- | | `401` | Missing or invalid token, IP not allow-listed, or missing `payouts:read` | # Get payout status Returns a live settlement status for one payout. Use it when you need the current state right now — reconciling after downtime, or investigating a payout that has been `PROCESSING` longer than you expect. `GET https://api-prod.useknit.io/api/v1/payouts/{id}/status` - Required scope: `payouts:read` - Auth: `Bearer token` > **Warning:** This is an on-demand check, not a polling endpoint. For normal operation rely on the [`PAYOUT_SUCCESSFUL` webhook](https://docs.useknit.io/webhooks/payout-successful); if you must poll, back off between attempts. ## Path parameters - **`id` (string, required)** — The payout's `id`. ## Request ```bash filename="cURL" curl "https://api-prod.useknit.io/api/v1/payouts/8e5697e3-8265-455b-984a-0eb40e10b0f9/status" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ## Response ```json filename="200 OK" { "statusCode": 200, "message": "Payout status retrieved successfully", "data": { "status": "COMPLETED", "transactionHash": "0xb0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f70819" }, "success": true } ``` ## Errors | Status | Cause | | ------ | ----- | | `400` | The status could not be retrieved. Retry with backoff; the stored record from [Retrieve a payout](https://docs.useknit.io/payouts/get-single-payout) remains available | | `401` | Missing or invalid token, IP not allow-listed, or missing `payouts:read` | | `403` | The payout exists but belongs to another business | | `404` | No payout with that ID | # Retrieve a payout Fetches the payout record Knit holds for one payout. `GET https://api-prod.useknit.io/api/v1/payouts/{id}` - Required scope: `payouts:read` - Auth: `Bearer token` ## Path parameters - **`id` (string, required)** — The payout's `id`, as returned when it was created. ## Request ```bash filename="cURL" curl "https://api-prod.useknit.io/api/v1/payouts/8e5697e3-8265-455b-984a-0eb40e10b0f9" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ## Response ```json filename="200 OK" { "statusCode": 200, "message": "Payout retrieved successfully", "data": { "id": "8e5697e3-8265-455b-984a-0eb40e10b0f9", "businessId": "b7b93a40-4e18-4e97-9c5f-8a7a4fd0df92", "network": "MATIC_MAINNET", "token": "USDT", "amount": "25.500000000000000000", "toAddress": "0x56adfcc254ab3b8142a275c1837bcffaff5aa38b", "merchantReference": "INV-2045", "transactionHash": "0xb0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f70819", "status": "COMPLETED", "info": null, "createdAt": "2024-08-27T13:44:20.000000Z", "updatedAt": "2024-08-27T13:47:03.000000Z" }, "success": true } ``` - **`status` (string)** — `PENDING`, `PROCESSING`, `COMPLETED`, or `FAILED`. - **`transactionHash` (string | null)** — Populated once the transfer is on-chain. `null` before then. - **`info` (string | null)** — Detail about a failure. `null` on healthy payouts. > **Note:** This returns Knit's stored record. For a live check against the settlement layer, use [Get payout status](https://docs.useknit.io/payouts/get-payout-status). ## Errors | Status | Cause | | ------ | ----- | | `401` | Missing or invalid token, IP not allow-listed, or missing `payouts:read` | | `403` | The payout exists but belongs to another business | | `404` | No payout with that ID | # Blockchain notifications Subscribe to activity on an address and receive a [`BLOCKCHAIN_TRANSACTION_DETECTED`](https://docs.useknit.io/webhooks/blockchain-transaction-detected) webhook whenever it moves stablecoin. The address does not have to be one Knit issued — you can watch a treasury address, an exchange deposit address, or a customer's self-custodied wallet. - [Create a subscription](https://docs.useknit.io/blockchain-notifications/create-subscription) — Start watching an address. - [List subscriptions](https://docs.useknit.io/blockchain-notifications/list-subscriptions) — See everything you are watching. - [Update a subscription](https://docs.useknit.io/blockchain-notifications/update-subscription) — Change networks, tokens, direction, or pause it. - [Delete a subscription](https://docs.useknit.io/blockchain-notifications/delete-subscription) — Stop watching an address. ## Address types Every subscription has a `type` that determines which address format is accepted and which networks it can watch. | Type | Address format | Networks | | ---- | -------------- | -------- | | `EVM` | `0x` followed by 40 hex characters | `ETH_MAINNET`, `MATIC_MAINNET`, `MATIC_AMOY`, `BSC_MAINNET`, `BASE_MAINNET` | | `TRON` | `T` followed by 33 base58 characters | `TRON_MAINNET`, `TRON_TESTNET` | | `SOLANA` | 32–44 base58 characters | `SOL_MAINNET`, `SOL_DEVNET` | > **Note:** `ETHEREUM_MAINNET` and `SOLANA_MAINNET` are accepted as aliases and stored as `ETH_MAINNET` and `SOL_MAINNET`. Read the network back from the response rather than assuming the value you sent is echoed verbatim. If the address format does not match the `type`, or a network does not belong to that type, the request fails with `400`. ## Defaults Omitting `networks` or `tokens` applies a sensible default for the type. | Type | Default networks | Default tokens | | ---- | ---------------- | -------------- | | `EVM` | `ETH_MAINNET`, `MATIC_MAINNET`, `BSC_MAINNET`, `BASE_MAINNET` | `USDC`, `USDT` | | `TRON` | `TRON_MAINNET` | `USDT` | | `SOLANA` | `SOL_MAINNET` | `USDC`, `USDT` | Supported tokens are `USDC`, `USDT`, and `PYUSD`. ## Delivery Events go to the subscription's `webhookUrl`. If you omit it, your business's configured webhook URL is used instead — and if neither is set, the request fails with `400`. The URL must be publicly reachable over HTTP or HTTPS; private, loopback, and internal addresses are rejected. Payloads are signed exactly like every other Knit webhook — see [Webhooks](https://docs.useknit.io/webhooks) for verification and retries. > **Warning:** A subscription watches an address, not a balance. Knit does not custody these funds and cannot move them — notifications are observational only. # Create a subscription Starts watching an address. From then on, matching transfers produce a [`BLOCKCHAIN_TRANSACTION_DETECTED`](https://docs.useknit.io/webhooks/blockchain-transaction-detected) webhook. `POST https://api-prod.useknit.io/api/v1/blockchain-notifications/subscriptions` - Required scope: `blockchain-notifications:write` - Auth: `Bearer token` ## Body - **`address` (string, required)** — The address to watch. Its format must match `type` — see [address types](https://docs.useknit.io/blockchain-notifications). - **`type` (string, required)** — `EVM`, `TRON`, or `SOLANA`. - **`networks` (string[], optional)** — Networks to watch. Every entry must belong to the chosen `type`. Defaults to the full mainnet set for that type. - **`tokens` (string[], optional)** — Tokens to watch: `USDC`, `USDT`, or `PYUSD`. Defaults to `USDC` and `USDT` (`USDT` alone for `TRON`). - **`direction` (string, optional, default `BOTH`)** — `INCOMING`, `OUTGOING`, or `BOTH`. - **`webhookUrl` (string, optional)** — Where to deliver events for this subscription. Must be a publicly reachable `http://` or `https://` URL. Falls back to your business's webhook URL. - **`metadata` (object, optional)** — Arbitrary JSON of your own. It is stored with the subscription and echoed back on every event — useful for carrying your customer ID. ## Request ```bash filename="cURL" curl -X POST "https://api-prod.useknit.io/api/v1/blockchain-notifications/subscriptions" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "type": "EVM", "networks": ["MATIC_MAINNET", "BASE_MAINNET"], "tokens": ["USDC", "USDT"], "direction": "INCOMING", "webhookUrl": "https://example.com/webhooks/knit", "metadata": { "customerId": "cus_8121" } }' ``` ## Response ```json filename="201 Created" { "statusCode": 201, "message": "Address notification subscription created", "data": { "id": "3f6f9d1a-64b2-4c7f-9a1e-7a2f0c8c7e11", "businessId": "b7b93a40-4e18-4e97-9c5f-8a7a4fd0df92", "address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "type": "EVM", "networks": ["MATIC_MAINNET", "BASE_MAINNET"], "tokens": ["USDC", "USDT"], "direction": "INCOMING", "webhookUrl": "https://example.com/webhooks/knit", "metadata": { "customerId": "cus_8121" }, "isActive": true, "createdAt": "2024-10-02T11:21:33.000000Z", "updatedAt": "2024-10-02T11:21:33.000000Z" }, "success": true } ``` > **Note:** Keep the returned `id`. It arrives on every event as `subscriptionId`, which is how you route an incoming notification back to the thing you were watching for. ## Errors | Status | Cause | | ------ | ----- | | `400` | `address` or `type` is missing, `type` is not one of the three values, or a network or token is unsupported — carries an `errors` object | | `400` | The address format does not match `type`, a network does not belong to `type`, or no webhook URL is available and none is configured on your business | | `401` | Missing or invalid token, IP not allow-listed, or missing `blockchain-notifications:write` | # Delete a subscription Stops delivery for a subscription. `DELETE https://api-prod.useknit.io/api/v1/blockchain-notifications/subscriptions/{id}` - Required scope: `blockchain-notifications:write` - Auth: `Bearer token` > **Note:** The subscription record is retained with `isActive: false` rather than being erased, so it still appears in [List subscriptions](https://docs.useknit.io/blockchain-notifications/list-subscriptions). If you may want to resume later, prefer [setting `isActive` to `false`](https://docs.useknit.io/blockchain-notifications/update-subscription) — that is reversible. ## Path parameters - **`id` (string, required)** — The subscription's `id`. ## Request ```bash filename="cURL" curl -X DELETE "https://api-prod.useknit.io/api/v1/blockchain-notifications/subscriptions/3f6f9d1a-64b2-4c7f-9a1e-7a2f0c8c7e11" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ## Response ```json filename="200 OK" { "statusCode": 200, "message": "Address notification subscription deleted", "data": null, "success": true } ``` ## Errors | Status | Cause | | ------ | ----- | | `401` | Missing or invalid token, IP not allow-listed, or missing `blockchain-notifications:write` | | `404` | No subscription with that ID belongs to your business | # Retrieve a subscription Fetches one subscription belonging to your business. `GET https://api-prod.useknit.io/api/v1/blockchain-notifications/subscriptions/{id}` - Required scope: `blockchain-notifications:read` - Auth: `Bearer token` ## Path parameters - **`id` (string, required)** — The subscription's `id`. ## Request ```bash filename="cURL" curl "https://api-prod.useknit.io/api/v1/blockchain-notifications/subscriptions/3f6f9d1a-64b2-4c7f-9a1e-7a2f0c8c7e11" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ## Response ```json filename="200 OK" { "statusCode": 200, "message": "Address notification subscription fetched", "data": { "id": "3f6f9d1a-64b2-4c7f-9a1e-7a2f0c8c7e11", "businessId": "b7b93a40-4e18-4e97-9c5f-8a7a4fd0df92", "address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "type": "EVM", "networks": ["MATIC_MAINNET", "BASE_MAINNET"], "tokens": ["USDC", "USDT"], "direction": "INCOMING", "webhookUrl": "https://example.com/webhooks/knit", "metadata": { "customerId": "cus_8121" }, "isActive": true, "createdAt": "2024-10-02T11:21:33.000000Z", "updatedAt": "2024-10-02T11:21:33.000000Z" }, "success": true } ``` ## Errors | Status | Cause | | ------ | ----- | | `401` | Missing or invalid token, IP not allow-listed, or missing `blockchain-notifications:read` | | `404` | No subscription with that ID belongs to your business | # List subscriptions Returns your business's subscriptions, newest first. `GET https://api-prod.useknit.io/api/v1/blockchain-notifications/subscriptions` - Required scope: `blockchain-notifications:read` - Auth: `Bearer token` ## Request ```bash filename="cURL" curl "https://api-prod.useknit.io/api/v1/blockchain-notifications/subscriptions" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ## Response ```json filename="200 OK" { "statusCode": 200, "message": "Address notification subscriptions fetched", "data": [ { "id": "3f6f9d1a-64b2-4c7f-9a1e-7a2f0c8c7e11", "businessId": "b7b93a40-4e18-4e97-9c5f-8a7a4fd0df92", "address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "type": "EVM", "networks": ["MATIC_MAINNET", "BASE_MAINNET"], "tokens": ["USDC", "USDT"], "direction": "INCOMING", "webhookUrl": "https://example.com/webhooks/knit", "metadata": { "customerId": "cus_8121" }, "isActive": true, "createdAt": "2024-10-02T11:21:33.000000Z", "updatedAt": "2024-10-02T11:21:33.000000Z" } ], "success": true } ``` > **Note:** Deleted subscriptions remain in this list with `isActive: false`. Filter on that flag if you only want the ones still delivering events. ## Errors | Status | Cause | | ------ | ----- | | `401` | Missing or invalid token, IP not allow-listed, or missing `blockchain-notifications:read` | # Update a subscription Amends a subscription. Send only the fields you want to change. `PATCH https://api-prod.useknit.io/api/v1/blockchain-notifications/subscriptions/{id}` - Required scope: `blockchain-notifications:write` - Auth: `Bearer token` > **Note:** `address` and `type` are fixed at creation. To watch a different address, create a new subscription. ## Path parameters - **`id` (string, required)** — The subscription's `id`. ## Body - **`networks` (string[], optional)** — Replaces the watched networks. Every entry must belong to the subscription's existing `type`. Must contain at least one entry. - **`tokens` (string[], optional)** — Replaces the watched tokens: `USDC`, `USDT`, or `PYUSD`. Must contain at least one entry. - **`direction` (string, optional)** — `INCOMING`, `OUTGOING`, or `BOTH`. - **`webhookUrl` (string, optional)** — A publicly reachable `http://` or `https://` URL. - **`metadata` (object, optional)** — Replaces the stored metadata. Send `null` to clear it. - **`isActive` (boolean, optional)** — Set to `false` to pause delivery without deleting, and back to `true` to resume. > **Warning:** `networks` and `tokens` replace the stored arrays rather than merging into them. Send the complete list you want, not just the additions. ## Request ```bash filename="cURL" curl -X PATCH "https://api-prod.useknit.io/api/v1/blockchain-notifications/subscriptions/3f6f9d1a-64b2-4c7f-9a1e-7a2f0c8c7e11" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "direction": "BOTH", "networks": ["MATIC_MAINNET", "BASE_MAINNET", "BSC_MAINNET"] }' ``` ## Response ```json filename="200 OK" { "statusCode": 200, "message": "Address notification subscription updated", "data": { "id": "3f6f9d1a-64b2-4c7f-9a1e-7a2f0c8c7e11", "businessId": "b7b93a40-4e18-4e97-9c5f-8a7a4fd0df92", "address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "type": "EVM", "networks": ["MATIC_MAINNET", "BASE_MAINNET", "BSC_MAINNET"], "tokens": ["USDC", "USDT"], "direction": "BOTH", "webhookUrl": "https://example.com/webhooks/knit", "metadata": { "customerId": "cus_8121" }, "isActive": true, "createdAt": "2024-10-02T11:21:33.000000Z", "updatedAt": "2024-10-04T08:02:19.000000Z" }, "success": true } ``` ## Errors | Status | Cause | | ------ | ----- | | `400` | A supplied field failed validation, or `networks` / `tokens` was sent empty — carries an `errors` object | | `400` | A network does not belong to the subscription's `type`, or `webhookUrl` is not a public HTTP(S) URL | | `401` | Missing or invalid token, IP not allow-listed, or missing `blockchain-notifications:write` | | `404` | No subscription with that ID belongs to your business | # Managed Signing Managed Signing gives you signing wallets whose keys Knit holds, governed by policies you define. You submit a transaction, a piece of EIP-712 typed data, or a message; the policy is evaluated; and — if it passes — the payload is signed and, optionally, broadcast. - [Create a wallet](https://docs.useknit.io/managed-signing/create-wallet) — Provision a signing wallet on one or more networks. - [Create a policy](https://docs.useknit.io/managed-signing/create-policy) — Constrain what the wallet is allowed to sign. - [Create a signing request](https://docs.useknit.io/managed-signing/create-signing-request) — Submit a transaction, typed data, or a message. - [Circle Payment Network](https://docs.useknit.io/managed-signing/cpn) — An end-to-end CPN integration guide. ## Base path All endpoints live under: ``` https://api-prod.useknit.io/api/v1/managed-signing ``` ## Authentication Managed Signing uses the same OAuth 2.0 client credentials as the rest of the API — send `Authorization: Bearer `. | Operation | Required scope | | --------- | -------------- | | `GET` — wallets, policies, requests, audit | `managed-signing:read` | | `POST` / `PATCH` — create, update, approve | `managed-signing:write` | Requests must also originate from an IP on your business allow list. See [Authentication](https://docs.useknit.io/authentication) for tokens, scopes, and allow-listing. > **Warning:** The `X-API-KEY` header is no longer a valid credential anywhere on the Knit API, Managed Signing included. Use a bearer token. ## Conventions - **Use the IDs this API returns.** Every `id` in a path parameter or a body filter such as `walletId` must be an ID you received from a Managed Signing response. - **camelCase in and out.** Request bodies and response payloads both use camelCase — except inside `payload.typedData`, where `types` and `message` must match the EIP-712 schema exactly and are passed through untouched. - **Shared envelope.** Responses use the standard `statusCode` / `message` / `data` / `success` envelope. See [Requests & responses](https://docs.useknit.io/conventions). ## Supported networks | Network | Identifier | | ------- | ---------- | | Ethereum | `ETHEREUM_MAINNET` (alias `ETH_MAINNET`) | | Polygon | `MATIC_MAINNET` | | BNB Smart Chain | `BSC_MAINNET` | | Base | `BASE_MAINNET` | | Polygon Amoy — sandbox only | `MATIC_AMOY` | ## Endpoints **Wallets** | Method | Path | Purpose | | ------ | ---- | ------- | | `POST` | `/wallets` | [Create a wallet](https://docs.useknit.io/managed-signing/create-wallet) | | `GET` | `/wallets` | [List wallets](https://docs.useknit.io/managed-signing/list-wallets) | | `GET` | `/wallets/{walletId}` | [Retrieve a wallet](https://docs.useknit.io/managed-signing/get-wallet) | | `GET` | `/wallets/{walletId}/balance` | [Get a token balance](https://docs.useknit.io/managed-signing/get-wallet-balance) | | `GET` | `/wallets/{walletId}/assets` | [List balances across networks](https://docs.useknit.io/managed-signing/get-wallet-assets) | **Policies** | Method | Path | Purpose | | ------ | ---- | ------- | | `POST` | `/policies` | [Create a policy](https://docs.useknit.io/managed-signing/create-policy) | | `PATCH` | `/policies/{policyId}` | [Update a policy](https://docs.useknit.io/managed-signing/update-policy) | | `GET` | `/policies` | [List policies](https://docs.useknit.io/managed-signing/list-policies) | | `GET` | `/policies/{policyId}` | [Retrieve a policy](https://docs.useknit.io/managed-signing/get-policy) | **Signing requests** | Method | Path | Purpose | | ------ | ---- | ------- | | `POST` | `/requests` | [Create a signing request](https://docs.useknit.io/managed-signing/create-signing-request) | | `POST` | `/requests/{requestId}/approve` | [Approve a request](https://docs.useknit.io/managed-signing/approve-signing-request) | | `GET` | `/requests` | [List requests](https://docs.useknit.io/managed-signing/list-signing-requests) | | `GET` | `/requests/{requestId}` | [Retrieve a request](https://docs.useknit.io/managed-signing/get-signing-request) | **Audit** | Method | Path | Purpose | | ------ | ---- | ------- | | `GET` | `/audit` | [List audit events](https://docs.useknit.io/managed-signing/list-audit-events) | ## Common errors | Status | Cause | | ------ | ----- | | `400` | Invalid request body, unsupported network, or the wallet is not active on the requested network | | `401` | Missing or invalid token, IP not allow-listed, or the token lacks the required `managed-signing` scope | | `404` | The wallet, policy, or request does not exist under your business | | `400` | A field failed validation | | `500` | The signing request could not be processed. Retry with backoff | # Approve Signing Request `POST https://api-prod.useknit.io/api/v1/managed-signing/requests/{requestId}/approve` - Required scope: `managed-signing:write` - Auth: `Bearer token` This endpoint approves a pending signing request. ## Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `requestId` | string | Yes | The local request ID | ## Request ```bash curl -X POST "https://api-prod.useknit.io/api/v1/managed-signing/requests//approve" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ## Response ```json filename="Response" { "statusCode": 200, "message": "Signing request approved", "data": { "id": "", "businessId": "", "walletId": "", "network": "MATIC_MAINNET", "type": "evm_tx", "kind": "erc20_approve", "status": "SIGNED", "payload": { "token": "0xToken", "spender": "0xSpender", "amount": "10" }, "signature": "0x...", "broadcast": false, "createdAt": "2026-01-20T18:30:40.912Z", "updatedAt": "2026-01-20T18:35:22.456Z" }, "success": true } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Local request ID | | `businessId` | string | Your business ID | | `walletId` | string | Local wallet ID | | `network` | string | Target network | | `type` | string | Request type | | `kind` | string | Transaction kind | | `status` | string | Request status (updated to `SIGNED`) | | `payload` | object | Request payload | | `signature` | string | Generated signature | | `broadcast` | boolean | Broadcast flag | | `createdAt` | string | Creation timestamp | | `updatedAt` | string | Last update timestamp | ## Notes - Only requests with status `AWAITING_APPROVAL` can be approved. - After approval, the status changes to `SIGNED`. - If `broadcast` was set to `true` during creation, the transaction will be broadcast after signing. # Circle Payment Network (CPN) Integration This guide covers how to integrate Knit's Managed Signing API with Circle's Payment Network (CPN) for seamless payment processing. ## What is CPN? Circle Payment Network (CPN) is Circle's payment infrastructure that enables fast, low-cost USDC transfers. CPN V2 uses EIP-712 signatures (Permit2) for gasless payment authorization, while CPN V1 uses traditional on-chain transactions. ## Why Use Knit for CPN? - **Secure Key Management**: Your signing keys are securely managed by Knit - **Policy Controls**: Define granular rules for what can be signed - **Simplified Integration**: Single API for wallet creation, policy management, and signing - **Audit Trail**: Complete visibility into all signing operations ## CPN Versions | Version | Signing Method | Gas Required | Recommended | |---------|---------------|--------------|-------------| | CPN V2 | EIP-712 (Permit2) | No (gasless) | Yes | | CPN V1 | Raw Transaction | Yes | Legacy only | ## Integration Flow ``` 1. Create Wallet → POST /managed-signing/wallets 2. Create Policy → POST /managed-signing/policies 3. Permit2 Approval → POST /managed-signing/requests (one-time, on-chain) 4. Per-Payment Sign → POST /managed-signing/requests (per payment, off-chain) ``` ## Quick Links - [Onboarding Checklist](https://docs.useknit.io/managed-signing/cpn/onboarding-checklist) - Complete setup guide - [Create Wallet](https://docs.useknit.io/managed-signing/cpn/create-wallet) - Step 1: Create a CPN-enabled wallet - [Create Policy](https://docs.useknit.io/managed-signing/cpn/create-policy) - Step 2: Configure CPN policy rules - [Permit2 Approval](https://docs.useknit.io/managed-signing/cpn/permit2-approval) - Step 3: One-time on-chain approval - [Per-Payment Signature](https://docs.useknit.io/managed-signing/cpn/per-payment-signature) - Step 4: Sign each payment - [Troubleshooting](https://docs.useknit.io/managed-signing/cpn/troubleshooting) - Common issues and solutions ## Supported Networks CPN is typically used on: - `MATIC_MAINNET` (Polygon) - `ETHEREUM_MAINNET` ## Prerequisites Before integrating: 1. Create an OAuth client and obtain an access token — see [Authentication](https://docs.useknit.io/authentication) 2. Allow-list your server IP addresses in the dashboard 3. Get the USDC token address for your target network 4. Get the Permit2 contract address for your target network # CPN V1: Raw Transaction Signing (Optional) `POST https://api-prod.useknit.io/api/v1/managed-signing/requests` - Required scope: `managed-signing:write` - Auth: `Bearer token` CPN V1 uses traditional on-chain transactions instead of EIP-712 signatures. **Only use this if you specifically need CPN V1 support.** ## When to Use CPN V1 - Legacy integrations that don't support Permit2 - Specific Circle requirements for your use case For new integrations, **CPN V2 is recommended** as it's gasless and more efficient. ## Prerequisites 1. Update your policy to allow raw transactions: ```json { "name": "cpn-v1", "rules": { "chains": ["MATIC_MAINNET"], "allowRawTx": true } } ``` ## Body ```json { "walletId": "", "network": "MATIC_MAINNET", "type": "evm_tx", "kind": "raw", "payload": { "tx": { "to": "0x...", "data": "0x...", "value": "0x0", "gas": "0x...", "maxFeePerGas": "0x...", "maxPriorityFeePerGas": "0x...", "nonce": 123 } }, "broadcast": false } ``` | Field | Type | Description | |-------|------|-------------| | `walletId` | string | Your local wallet ID | | `network` | string | Target network | | `type` | string | Must be `evm_tx` | | `kind` | string | Must be `raw` for raw transaction signing | | `payload.tx` | object | Raw transaction object | | `broadcast` | boolean | Whether to broadcast (usually `false` for CPN V1) | ## Transaction Fields | Field | Type | Description | |-------|------|-------------| | `to` | string | Recipient contract address | | `data` | string | Encoded transaction data | | `value` | string | ETH value in hex (usually `0x0`) | | `gas` | string | Gas limit in hex | | `maxFeePerGas` | string | Max fee per gas in hex (EIP-1559) | | `maxPriorityFeePerGas` | string | Priority fee in hex (EIP-1559) | | `nonce` | number | Transaction nonce | ## Request ```bash curl "https://api-prod.useknit.io/api/v1/managed-signing/requests" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "walletId": "", "network": "MATIC_MAINNET", "type": "evm_tx", "kind": "raw", "payload": { "tx": { "to": "0x...", "data": "0x...", "value": "0x0", "gas": "0x5208", "maxFeePerGas": "0x59682f00", "maxPriorityFeePerGas": "0x59682f00", "nonce": 1 } }, "broadcast": false }' ``` ## Response ```json filename="Response" { "statusCode": 201, "message": "Signing request created", "data": { "id": "", "businessId": "", "walletId": "", "network": "MATIC_MAINNET", "type": "evm_tx", "kind": "raw", "status": "SIGNED", "payload": { "tx": { "..." } }, "signedTx": "0x...", "broadcast": false, "createdAt": "2026-01-20T18:30:40.912Z", "updatedAt": "2026-01-20T18:30:40.912Z" }, "success": true } ``` ## Important Notes - Raw transaction signing requires **all fields** to be provided (gas, fees, nonce) - If RPCs are configured, the service can auto-fill gas estimation - The `signedTx` in the response is the signed raw transaction to submit to Circle - CPN V1 transactions require gas fees paid by the wallet ## Security Considerations Enabling `allowRawTx: true` in your policy allows signing arbitrary transactions. Consider: - Only enable if you specifically need CPN V1 - Restrict `chains` to only the networks you need - Monitor audit logs for unexpected raw transaction requests # Step 2: Create CPN Policy `POST https://api-prod.useknit.io/api/v1/managed-signing/policies` - Required scope: `managed-signing:write` - Auth: `Bearer token` Create a policy that allows CPN V2 signature flows while restricting signing to only approved tokens and contracts. ## Body ```json { "name": "cpn-v2", "rules": { "chains": ["MATIC_MAINNET"], "tokens": [""], "spenders": [""], "typedData": { "allow": true }, "denyUnlimitedApprovals": false, "allowRawTx": false } } ``` ## Policy Rules Explained | Field | Type | Description | |-------|------|-------------| | `chains` | array | Allowed blockchain networks | | `tokens` | array | Token contract addresses that can be approved | | `spenders` | array | Contract addresses allowed as spenders (Permit2) | | `typedData.allow` | boolean | **Must be `true`** for CPN V2 EIP-712 signatures | | `denyUnlimitedApprovals` | boolean | Set to `false` to allow standard Permit2 approvals | | `allowRawTx` | boolean | Set to `false` unless you need CPN V1 | ## Security Recommendations - **Restrict `tokens`**: Only include USDC addresses you intend to use - **Restrict `spenders`**: Only include the official Permit2 contract address - **Keep `allowRawTx: false`**: Only enable for CPN V1 legacy support ## Request ```bash curl "https://api-prod.useknit.io/api/v1/managed-signing/policies" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "name": "cpn-v2", "rules": { "chains": ["MATIC_MAINNET"], "tokens": ["0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359"], "spenders": ["0x000000000022D473030F116dDEE9F6B43aC78BA3"], "typedData": { "allow": true }, "denyUnlimitedApprovals": false, "allowRawTx": false } }' ``` ## Response ```json filename="Response" { "statusCode": 201, "message": "Policy created", "data": { "id": "", "businessId": "", "name": "cpn-v2", "rules": { "chains": ["MATIC_MAINNET"], "tokens": ["0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359"], "spenders": ["0x000000000022D473030F116dDEE9F6B43aC78BA3"], "typedData": { "allow": true }, "denyUnlimitedApprovals": false, "allowRawTx": false }, "createdAt": "2026-01-20T18:30:40.912Z", "updatedAt": "2026-01-20T18:30:40.912Z" }, "success": true } ``` ## Next Step Proceed to [Permit2 Approval](https://docs.useknit.io/managed-signing/cpn/permit2-approval) to set up the one-time on-chain approval. # Step 1: Create CPN Wallet `POST https://api-prod.useknit.io/api/v1/managed-signing/wallets` - Required scope: `managed-signing:write` - Auth: `Bearer token` Create a managed signing wallet for CPN integration. This wallet will hold your USDC and sign payment authorizations. ## Body ```json { "type": "EVM", "networks": ["MATIC_MAINNET"], "merchantCallbackUrl": "https://your-domain.com/cpn-callback" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `type` | string | Yes | Must be `EVM` for CPN | | `networks` | array | Yes | Include `MATIC_MAINNET` for Polygon CPN | | `merchantCallbackUrl` | string | No | URL for receiving signing callbacks | ## Request ```bash curl "https://api-prod.useknit.io/api/v1/managed-signing/wallets" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "type": "EVM", "networks": ["MATIC_MAINNET"], "merchantCallbackUrl": "https://your-domain.com/cpn-callback" }' ``` ## Response ```json filename="Response" { "statusCode": 201, "message": "Wallet created", "data": { "id": "", "businessId": "", "type": "EVM", "networks": ["MATIC_MAINNET"], "network": "MATIC_MAINNET", "address": "0x...", "hdPath": "m/44'/60'/0'/0/0", "keyId": "", "isActive": true, "merchantCallbackUrl": "https://your-domain.com/cpn-callback", "createdAt": "2026-01-20T18:30:40.912Z", "updatedAt": "2026-01-20T18:30:40.912Z" }, "success": true } ``` ## Important - Save the `id` (local wallet ID) - you'll need it for all subsequent API calls - Save the `address` - this is your wallet's blockchain address for receiving USDC - Fund this wallet with MATIC for gas (needed for the one-time Permit2 approval) ## Next Step Proceed to [Create Policy](https://docs.useknit.io/managed-signing/cpn/create-policy) to configure CPN-specific signing rules. # CPN Onboarding Checklist This checklist maps Circle CPN V2 requirements to Knit's Managed Signing API. ## Overview | Step | Action | Endpoint | Frequency | |------|--------|----------|-----------| | 1 | Create Wallet | `POST /managed-signing/wallets` | Once | | 2 | Create Policy | `POST /managed-signing/policies` | Once | | 3 | Permit2 Approval | `POST /managed-signing/requests` | Once per token | | 4 | Per-Payment Signature | `POST /managed-signing/requests` | Per payment | --- ## Step 1: Create a Managed Signing Wallet Create a wallet that will hold funds and sign CPN transactions. **Production:** ```json { "type": "EVM", "networks": ["MATIC_MAINNET"] } ``` **Testing (Sandbox/Dev):** ```json { "type": "EVM", "networks": ["MATIC_AMOY"] } ``` See: [Create Wallet](https://docs.useknit.io/managed-signing/cpn/create-wallet) --- ## Step 2: Create a CPN Policy Configure policy rules that allow CPN V2 signature flows. **Production:** ```json { "name": "cpn-v2", "rules": { "chains": ["MATIC_MAINNET"], "tokens": [""], "spenders": [""], "typedData": { "allow": true }, "denyUnlimitedApprovals": false, "allowRawTx": false } } ``` **Testing (Sandbox/Dev):** ```json { "name": "cpn-v2-test", "rules": { "chains": ["MATIC_AMOY"], "tokens": [""], "spenders": [""], "typedData": { "allow": true }, "denyUnlimitedApprovals": false, "allowRawTx": false } } ``` See: [Create Policy](https://docs.useknit.io/managed-signing/cpn/create-policy) --- ## Step 3: One-Time Permit2 Approval Approve the Permit2 contract to spend USDC on behalf of your wallet. ```json { "walletId": "", "network": "MATIC_MAINNET", "type": "evm_tx", "kind": "erc20_approve", "payload": { "token": "", "spender": "", "amount": "" }, "broadcast": true } ``` **For testing, use `MATIC_AMOY` network with testnet token/contract addresses.** See: [Permit2 Approval](https://docs.useknit.io/managed-signing/cpn/permit2-approval) --- ## Step 4: Per-Payment Signature For each payment, Circle provides `messageToBeSigned` (EIP-712 typed data). Sign it and return the signature. ```json { "walletId": "", "network": "MATIC_MAINNET", "type": "eip712", "payload": { "typedData": { "...Circle's messageToBeSigned..." } } } ``` **For testing, use `MATIC_AMOY` network.** See: [Per-Payment Signature](https://docs.useknit.io/managed-signing/cpn/per-payment-signature) --- ## (Optional) Step 5: CPN V1 Raw Transaction Only needed for legacy CPN V1 integration. See: [CPN V1 Raw TX](https://docs.useknit.io/managed-signing/cpn/cpn-v1-raw-tx) --- ## Key Addresses **Production Networks** | Network | USDC Token | Permit2 Contract | |---------|------------|------------------| | Polygon (MATIC_MAINNET) | Contact Circle | Contact Circle | | Ethereum (ETHEREUM_MAINNET) | Contact Circle | Contact Circle | **Testnet Networks (Sandbox/Dev)** | Network | USDC Token | Permit2 Contract | |---------|------------|------------------| | Polygon Amoy (MATIC_AMOY) | Contact Circle | Contact Circle | Contact Circle or refer to their documentation for the exact contract addresses for your integration. **Note:** Most merchants test CPN integration using `MATIC_AMOY` testnet before moving to production. # Step 4: Per-Payment Signature (CPN V2) `POST https://api-prod.useknit.io/api/v1/managed-signing/requests` - Required scope: `managed-signing:write` - Auth: `Bearer token` For each CPN V2 payment, Circle provides a `messageToBeSigned` (EIP-712 typed data). You sign it using this endpoint and return the signature to Circle. ## How CPN V2 Works ``` 1. Initiate payment with Circle → Circle returns `messageToBeSigned` 2. Sign with Knit API → Knit returns `signature` 3. Submit to Circle → Circle processes the payment ``` ## Body ```json { "walletId": "", "network": "MATIC_MAINNET", "type": "eip712", "payload": { "typedData": { "primaryType": "PermitWitnessTransferFrom", "domain": { "name": "Permit2", "chainId": 137, "verifyingContract": "0x000000000022D473030F116dDEE9F6B43aC78BA3" }, "types": { "PermitWitnessTransferFrom": [ { "name": "permitted", "type": "TokenPermissions" }, { "name": "spender", "type": "address" }, { "name": "nonce", "type": "uint256" }, { "name": "deadline", "type": "uint256" }, { "name": "witness", "type": "..." } ], "TokenPermissions": [ { "name": "token", "type": "address" }, { "name": "amount", "type": "uint256" } ] }, "message": { "permitted": { "token": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", "amount": "1000000" }, "spender": "0x...", "nonce": "...", "deadline": "...", "witness": "..." } } } } ``` | Field | Type | Description | |-------|------|-------------| | `walletId` | string | Your local wallet ID | | `network` | string | Target network | | `type` | string | Must be `eip712` for typed data signing | | `payload.typedData` | object | Circle's `messageToBeSigned` (EIP-712 format) | ## Important The `typedData` object should be exactly as provided by Circle's API. Do **not** modify or camelCase the keys in `types` or `message` - they must match the EIP-712 schema exactly. ## Request ```bash curl "https://api-prod.useknit.io/api/v1/managed-signing/requests" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "walletId": "", "network": "MATIC_MAINNET", "type": "eip712", "payload": { "typedData": { "...Circle messageToBeSigned..." } } }' ``` ## Response ```json filename="Response" { "statusCode": 201, "message": "Signing request created", "data": { "id": "", "businessId": "", "walletId": "", "network": "MATIC_MAINNET", "type": "eip712", "status": "SIGNED", "payload": { "typedData": { "..." } }, "signature": "0x...", "createdAt": "2026-01-20T18:30:40.912Z", "updatedAt": "2026-01-20T18:30:40.912Z" }, "success": true } ``` ## Using the Signature Take the `signature` from the response and include it in Circle's payment submission step: ```json { "signature": "0x..." } ``` ## Gasless Signing Unlike the Permit2 approval step, EIP-712 signing is **completely gasless**. The signature is generated off-chain and no blockchain transaction is required. ## Error Handling | Error | Cause | Solution | |-------|-------|----------| | `POLICY_DENIED` | typedData not allowed by policy | Ensure `typedData.allow: true` in your policy | | `INVALID_TYPED_DATA` | Malformed EIP-712 data | Verify the typedData matches Circle's format exactly | ## Integration Example ```javascript // 1. Initiate payment with Circle const circleResponse = await circle.initiatePayment({ amount: "10.00", currency: "USD" }); // 2. Sign with Knit const knitResponse = await fetch('https://api-prod.useknit.io/api/v1/managed-signing/requests', { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ walletId: walletId, network: 'MATIC_MAINNET', type: 'eip712', payload: { typedData: circleResponse.messageToBeSigned } }) }); const { signature } = (await knitResponse.json()).data; // 3. Submit to Circle await circle.submitPayment({ paymentId: circleResponse.paymentId, signature: signature }); ``` # Step 3: Permit2 Approval (One-Time) `POST https://api-prod.useknit.io/api/v1/managed-signing/requests` - Required scope: `managed-signing:write` - Auth: `Bearer token` This is a **one-time on-chain transaction** that approves the Permit2 contract to spend USDC on behalf of your wallet. After this approval, all subsequent CPN V2 payments use gasless EIP-712 signatures. ## Prerequisites - Wallet must have MATIC (or native token) for gas fees - USDC token address for your network - Permit2 contract address for your network ## Body ```json { "walletId": "", "network": "MATIC_MAINNET", "type": "evm_tx", "kind": "erc20_approve", "payload": { "token": "", "spender": "", "amount": "" }, "broadcast": true } ``` | Field | Type | Description | |-------|------|-------------| | `walletId` | string | Your local wallet ID from Step 1 | | `network` | string | Target network (e.g., `MATIC_MAINNET`) | | `type` | string | Must be `evm_tx` for on-chain transaction | | `kind` | string | Must be `erc20_approve` | | `payload.token` | string | USDC contract address | | `payload.spender` | string | Permit2 contract address | | `payload.amount` | string | Approval amount (use max for unlimited) | | `broadcast` | boolean | Set to `true` to broadcast on-chain | ## Approval Amount - For unlimited approval: use a large number like `115792089237316195423570985008687907853269984665640564039457584007913129639935` (max uint256) - For limited approval: specify the exact amount in token units ## Request ```bash curl "https://api-prod.useknit.io/api/v1/managed-signing/requests" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "walletId": "", "network": "MATIC_MAINNET", "type": "evm_tx", "kind": "erc20_approve", "payload": { "token": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", "spender": "0x000000000022D473030F116dDEE9F6B43aC78BA3", "amount": "115792089237316195423570985008687907853269984665640564039457584007913129639935" }, "broadcast": true }' ``` ## Response ```json filename="Response" { "statusCode": 201, "message": "Signing request created", "data": { "id": "", "businessId": "", "walletId": "", "network": "MATIC_MAINNET", "type": "evm_tx", "kind": "erc20_approve", "status": "SIGNED", "payload": { "token": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", "spender": "0x000000000022D473030F116dDEE9F6B43aC78BA3", "amount": "115792089237316195423570985008687907853269984665640564039457584007913129639935" }, "txHash": "0x...", "broadcast": true, "createdAt": "2026-01-20T18:30:40.912Z", "updatedAt": "2026-01-20T18:30:40.912Z" }, "success": true } ``` ## Important Notes - This transaction requires gas paid by the wallet - The service will auto-fill gas/fees/nonce if an RPC is configured for the network - If `broadcast` is `false`, you'll receive the signed transaction to broadcast yourself - This approval only needs to be done **once per token** ## Next Step Proceed to [Per-Payment Signature](https://docs.useknit.io/managed-signing/cpn/per-payment-signature) to sign individual payments. # CPN Troubleshooting Common issues and solutions when integrating CPN with Knit's Managed Signing API. --- ## "Failed to sign transaction" from Managed Signing **Cause**: Transaction is missing required fields (fees, gas, or nonce). **Solution**: - Ensure the transaction includes EIP-1559 fields (`maxFeePerGas`, `maxPriorityFeePerGas`) or legacy fields (`gasPrice`) - Include `gas` limit - Include `nonce` - Verify RPCs are configured for the target network to enable auto-fill --- ## "POLICY_DENIED" Error **Cause**: The signing request violates your policy rules. **Solutions**: | Issue | Fix | |-------|-----| | EIP-712 signing blocked | Set `typedData.allow: true` in your policy | | Token not allowed | Add the token address to `rules.tokens` | | Spender not allowed | Add the Permit2 address to `rules.spenders` | | Raw TX blocked | Set `allowRawTx: true` (for CPN V1 only) | | Wrong network | Add the network to `rules.chains` | --- ## "Invalid typed data" Error **Cause**: The EIP-712 typed data format is incorrect. **Solution**: - Use Circle's `messageToBeSigned` exactly as provided - Do **not** modify or camelCase keys in `types` or `message` - Ensure the JSON is valid (no comments, trailing commas) --- ## Permit2 Approval Transaction Fails **Cause**: Insufficient gas or incorrect parameters. **Solutions**: - Ensure wallet has enough native token (MATIC/ETH) for gas - Verify the USDC token address is correct for your network - Verify the Permit2 contract address is correct - Check that the approval amount is valid --- ## Signature Rejected by Circle **Cause**: Signature doesn't match expected format or data. **Solutions**: - Verify you're using the exact `messageToBeSigned` from Circle - Ensure the signing wallet address matches what Circle expects - Check that the wallet has completed the Permit2 approval - Verify the network matches (e.g., both on Polygon) --- ## Request Body Validation Errors **Cause**: Invalid JSON or missing required fields. **Solutions**: - JSON request bodies must not include comments - All required fields must be present - Use correct data types (strings for addresses, numbers for nonce) - Ensure `walletId` is the local ID from Knit, not an external ID --- ## Network Mismatch **Cause**: Using wrong network identifier. **Supported Production Networks**: - `MATIC_MAINNET` (Polygon) - `ETHEREUM_MAINNET` - `BSC_MAINNET` - `BASE_MAINNET` **Supported Testnet Networks (Sandbox/Dev only)**: - `MATIC_AMOY` (Polygon Amoy testnet - commonly used for CPN testing) Do **not** use `ETH_MAINNET` - use `ETHEREUM_MAINNET` instead. --- ## Auto-Fill Not Working **Cause**: RPC not configured for the target network. **Solution**: - Contact Knit support to verify RPC configuration - Alternatively, provide `txOverrides` with manual gas values - For `broadcast: false`, you may need to estimate gas externally --- ## Common Contract Addresses Contact Circle for the official addresses for your integration. These are for reference only: | Network | Asset | Type | Address | |---------|-------|------|---------| | Polygon | USDC | Token | Verify with Circle | | Polygon | Permit2 | Contract | Verify with Circle | | Ethereum | USDC | Token | Verify with Circle | | Ethereum | Permit2 | Contract | Verify with Circle | --- ## Getting Help If you continue to experience issues: 1. Check the audit logs: `GET /api/v1/managed-signing/audit?requestId=` 2. Verify your policy configuration: `GET /api/v1/managed-signing/policies/` 3. Contact Knit support with: - Request ID - Error message - Request payload (sanitized) - Network and wallet address # Create Policy `POST https://api-prod.useknit.io/api/v1/managed-signing/policies` - Required scope: `managed-signing:write` - Auth: `Bearer token` This endpoint creates a new policy for managing signing request rules. ## Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | Yes | Policy name | | `rules` | object | Yes | Policy rules configuration | ## Basic Example ```json { "name": "default", "rules": { "chains": ["ETHEREUM_MAINNET"], "maxApprovalAmount": "1000000", "denyUnlimitedApprovals": true } } ``` ## Full Policy Rules Schema ```json { "name": "comprehensive-policy", "rules": { "chains": ["ETHEREUM_MAINNET", "MATIC_MAINNET"], "tokens": ["0xAllowedToken1", "0xAllowedToken2"], "spenders": ["0xAllowedSpender1"], "maxApprovalAmount": "1000000000000000000", "denyUnlimitedApprovals": true, "requireApprovalAbove": "500000000000000000", "maxTransferAmount": "1000000000000000000", "rateLimits": { "windowSec": 3600, "maxCount": 10, "maxValue": "5000000000000000000" }, "allowRawTx": false, "allowedContracts": ["0xContract1", "0xContract2"], "typedData": { "allow": true, "allowedPrimaryTypes": ["Permit", "Order"], "allowedDomains": [ { "name": "Uniswap", "chainId": 1, "verifyingContract": "0xContractAddress" } ] }, "message": { "allow": true, "maxBytes": 1024, "allowedPrefixes": ["Sign this message"] } } } ``` ## Policy Rules Reference **Transaction Controls** | Field | Type | Description | |-------|------|-------------| | `chains` | string[] | Allowed blockchain networks | | `tokens` | string[] | Allowed token contract addresses (for approvals and transfers) | | `spenders` | string[] | Allowed spender addresses for approvals | | `allowedContracts` | string[] | Contracts allowed for raw transactions | | `allowRawTx` | boolean | Allow raw transaction signing | **Approval & Transfer Limits** | Field | Type | Description | |-------|------|-------------| | `maxApprovalAmount` | string | Maximum approval amount (in base units) | | `denyUnlimitedApprovals` | boolean | Block unlimited (max uint256) approvals | | `maxTransferAmount` | string | Maximum transfer amount per transaction (in base units) | | `requireApprovalAbove` | string | Require manual approval above this amount | **Rate Limiting** | Field | Type | Description | |-------|------|-------------| | `rateLimits.windowSec` | number | Time window for rate limiting (seconds) | | `rateLimits.maxCount` | number | Max requests allowed in time window | | `rateLimits.maxValue` | string | Max total value in time window (wei) | **EIP-712 Typed Data Controls** | Field | Type | Description | |-------|------|-------------| | `typedData.allow` | boolean | Allow EIP-712 signing | | `typedData.allowedPrimaryTypes` | string[] | Allowed EIP-712 primary types | | `typedData.allowedDomains` | object[] | Allowed EIP-712 domains | | `typedData.allowedDomains[].name` | string | Domain name | | `typedData.allowedDomains[].chainId` | number | Chain ID | | `typedData.allowedDomains[].verifyingContract` | string | Contract address | **EIP-191 Message Controls** | Field | Type | Description | |-------|------|-------------| | `message.allow` | boolean | Allow EIP-191 message signing | | `message.maxBytes` | number | Max message size in bytes | | `message.allowedPrefixes` | string[] | Required message prefixes | ## Request ```bash curl "https://api-prod.useknit.io/api/v1/managed-signing/policies" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "name": "default", "rules": { "chains": ["ETHEREUM_MAINNET"], "maxApprovalAmount": "1000000", "denyUnlimitedApprovals": true } }' ``` ## Response ```json filename="Response" { "statusCode": 201, "message": "Policy created", "data": { "id": "", "businessId": "", "name": "default", "rules": { "chains": ["ETHEREUM_MAINNET"], "maxApprovalAmount": "1000000", "denyUnlimitedApprovals": true }, "createdAt": "2026-01-20T18:30:40.912Z", "updatedAt": "2026-01-20T18:30:40.912Z" }, "success": true } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Local policy ID (use this in subsequent requests) | | `businessId` | string | Your business ID | | `name` | string | Policy name | | `rules` | object | Policy rules configuration | | `createdAt` | string | Creation timestamp | | `updatedAt` | string | Last update timestamp | # Create Signing Request `POST https://api-prod.useknit.io/api/v1/managed-signing/requests` - Required scope: `managed-signing:write` - Auth: `Bearer token` This endpoint creates a new signing request. Supports EVM transactions, EIP-712 typed data signing, and EIP-191 message signing. ## Signing Request Types | Type | Description | |------|-------------| | `evm_tx` | EVM transaction signing (e.g., ERC20 approvals, ERC20 transfers, raw transactions) | | `eip712` | EIP-712 typed data signing (structured data with domain separator) | | `eip191` | EIP-191 message signing (personal sign messages) | ## Supported Networks **Production** - `ETHEREUM_MAINNET` - `MATIC_MAINNET` - `BSC_MAINNET` - `BASE_MAINNET` **Testnet (Sandbox/Dev only)** - `MATIC_AMOY` - Polygon Amoy testnet ## Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `walletId` | string | Yes | Local wallet ID | | `network` | string | Yes | Target network | | `type` | string | Yes | Request type (`evm_tx`, `eip712`, or `eip191`) | | `kind` | string | Conditional | Transaction kind (required for `evm_tx`: `erc20_approve`, `erc20_transfer`, or `raw`) | | `payload` | object | Yes | Request payload | | `broadcast` | boolean | No | Whether to broadcast the transaction | | `idempotencyKey` | string | No | Idempotency key for request deduplication | --- ## EVM Transaction (ERC20 Approve) ```json { "walletId": "", "network": "MATIC_MAINNET", "type": "evm_tx", "kind": "erc20_approve", "payload": { "token": "0xToken", "spender": "0xSpender", "amount": "10" }, "broadcast": false, "idempotencyKey": "optional-idem-key" } ``` **Payload Fields for ERC20 Approve** | Field | Type | Description | |-------|------|-------------| | `token` | string | Token contract address | | `spender` | string | Spender address | | `amount` | string | Approval amount (in base units) | --- ## EVM Transaction (ERC20 Transfer) ```json { "walletId": "", "network": "ETH_MAINNET", "type": "evm_tx", "kind": "erc20_transfer", "broadcast": true, "payload": { "token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "to": "0x1111111111111111111111111111111111111111", "amount": "1000000" } } ``` **Payload Fields for ERC20 Transfer** | Field | Type | Description | |-------|------|-------------| | `token` | string | ERC20 token contract address | | `to` | string | Recipient address | | `amount` | string | Transfer amount in base units (e.g., `1000000` for 1.0 USDC) | | `txOverrides` | object | (Optional) Gas overrides (e.g., `gasLimit`) | **Note:** The `broadcast` field is commonly set to `true` for transfers so the signed transaction is submitted on-chain. When `broadcast: true`, the response includes a `txHash`. When `broadcast: false`, the response includes `rawSignedTx` for manual submission. Transfer requests are subject to policy rules including `tokens`, `rateLimits`, `requireApprovalAbove`, and `maxTransferAmount`. --- ## EIP-712 Typed Data ```json { "walletId": "", "network": "ETHEREUM_MAINNET", "type": "eip712", "payload": { "typedData": { "primaryType": "Permit", "domain": { "name": "USD Coin", "version": "2", "chainId": 1, "verifyingContract": "0xToken" }, "types": { "Permit": [ { "name": "owner", "type": "address" } ] }, "message": { "owner": "0xOwner", "spender": "0xSpender" } } } } ``` **Important:** `typedData.types` and `typedData.message` keys must match the EIP-712 schema exactly and should **not** be camelCased. **Payload Fields for EIP-712** | Field | Type | Description | |-------|------|-------------| | `typedData` | object | EIP-712 typed data structure | | `typedData.primaryType` | string | Primary type name | | `typedData.domain` | object | Domain separator fields | | `typedData.types` | object | Type definitions | | `typedData.message` | object | Message to sign | --- ## EIP-191 Message Signing ```json { "walletId": "", "network": "ETHEREUM_MAINNET", "type": "eip191", "payload": { "message": "Sign this message to verify ownership" } } ``` **Payload Fields for EIP-191** | Field | Type | Description | |-------|------|-------------| | `message` | string | The message to sign | EIP-191 is commonly used for: - Wallet ownership verification - Off-chain authentication - Simple message signing without structured data --- ## Sample Request (EVM TX) ```bash curl "https://api-prod.useknit.io/api/v1/managed-signing/requests" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "walletId": "", "network": "MATIC_MAINNET", "type": "evm_tx", "kind": "erc20_approve", "payload": { "token": "0xToken", "spender": "0xSpender", "amount": "10" }, "broadcast": false }' ``` ## Response ```json filename="Response" { "statusCode": 201, "message": "Signing request created", "data": { "id": "", "businessId": "", "walletId": "", "network": "MATIC_MAINNET", "type": "evm_tx", "kind": "erc20_approve", "status": "AWAITING_APPROVAL", "payload": { "token": "0xToken", "spender": "0xSpender", "amount": "10" }, "broadcast": false, "createdAt": "2026-01-20T18:30:40.912Z", "updatedAt": "2026-01-20T18:30:40.912Z" }, "success": true } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Local request ID (use this in subsequent requests) | | `businessId` | string | Your business ID | | `walletId` | string | Local wallet ID | | `network` | string | Target network | | `type` | string | Request type | | `kind` | string | Transaction kind | | `status` | string | Request status | | `payload` | object | Request payload | | `broadcast` | boolean | Broadcast flag | | `signature` | string | The cryptographic signature (when signed) | | `rawSignedTx` | string | Full signed transaction (if `broadcast: false`) | | `txHash` | string | Transaction hash (if `broadcast: true`) | | `decisionTrace` | object | Policy evaluation details | | `errorMessage` | string | Error details (if status is `FAILED` or `POLICY_DENIED`) | | `createdAt` | string | Creation timestamp | | `updatedAt` | string | Last update timestamp | ## Request Statuses | Status | Description | |--------|-------------| | `AWAITING_APPROVAL` | Request is pending manual approval (exceeds policy threshold) | | `SIGNED` | Request was signed successfully | | `POLICY_DENIED` | Request was denied by policy rules | | `FAILED` | Request failed (see `errorMessage` for details) | # Create Managed Signing Wallet `POST https://api-prod.useknit.io/api/v1/managed-signing/wallets` - Required scope: `managed-signing:write` - Auth: `Bearer token` This endpoint creates a new managed signing wallet for a specified blockchain network type. ## Supported Networks | Network | Identifier | | ------- | ---------- | | Ethereum | `ETHEREUM_MAINNET` (alias `ETH_MAINNET`) | | Polygon | `MATIC_MAINNET` | | BNB Smart Chain | `BSC_MAINNET` | | Base | `BASE_MAINNET` | | Polygon Amoy — sandbox only | `MATIC_AMOY` | ## Body - **`type` (string, required)** — The wallet type. Currently `EVM` is the only accepted value. - **`networks` (string[], required)** — The networks the wallet should be usable on. Must contain at least one entry, and every entry must be a supported network. - **`merchantCallbackUrl` (string, required)** — Where callbacks for this wallet are delivered. Must be a publicly reachable `http://` or `https://` URL — private, loopback, and internal addresses are rejected. > **Warning:** `merchantCallbackUrl` is **required**. A wallet cannot be created without a reachable callback URL. ```json filename="Request body" { "type": "EVM", "networks": ["ETHEREUM_MAINNET"], "merchantCallbackUrl": "https://example.com/callback" } ``` ## Request ```bash curl "https://api-prod.useknit.io/api/v1/managed-signing/wallets" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "type": "EVM", "networks": ["ETHEREUM_MAINNET"], "merchantCallbackUrl": "https://example.com/callback" }' ``` ## Response ```json filename="Response" { "statusCode": 201, "message": "Wallet created", "data": { "id": "", "businessId": "", "type": "EVM", "networks": ["ETHEREUM_MAINNET"], "network": "ETHEREUM_MAINNET", "address": "0x...", "hdPath": "m/44'/60'/0'/0/0", "keyId": "", "isActive": true, "merchantCallbackUrl": "https://example.com/callback", "createdAt": "2026-01-20T18:30:40.912Z", "updatedAt": "2026-01-20T18:30:40.912Z" }, "success": true } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Local wallet ID (use this in subsequent requests) | | `businessId` | string | Your business ID | | `type` | string | Wallet type | | `networks` | array | Supported networks | | `network` | string | Primary network | | `address` | string | Wallet address | | `hdPath` | string | HD derivation path | | `keyId` | string | Key identifier | | `isActive` | boolean | Wallet active status | | `merchantCallbackUrl` | string | Callback URL | | `createdAt` | string | Creation timestamp | | `updatedAt` | string | Last update timestamp | # Get Policy `GET https://api-prod.useknit.io/api/v1/managed-signing/policies/{policyId}` - Required scope: `managed-signing:read` - Auth: `Bearer token` This endpoint retrieves a specific policy by its local ID. ## Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `policyId` | string | Yes | The local policy ID | ## Request ```bash curl "https://api-prod.useknit.io/api/v1/managed-signing/policies/" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ## Response ```json filename="Response" { "statusCode": 200, "message": "Policy retrieved", "data": { "id": "", "businessId": "", "name": "default", "rules": { "chains": ["ETHEREUM_MAINNET"], "maxApprovalAmount": "1000000", "denyUnlimitedApprovals": true }, "createdAt": "2026-01-20T18:30:40.912Z", "updatedAt": "2026-01-20T18:30:40.912Z" }, "success": true } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Local policy ID | | `businessId` | string | Your business ID | | `name` | string | Policy name | | `rules` | object | Policy rules configuration | | `createdAt` | string | Creation timestamp | | `updatedAt` | string | Last update timestamp | # Get Signing Request `GET https://api-prod.useknit.io/api/v1/managed-signing/requests/{requestId}` - Required scope: `managed-signing:read` - Auth: `Bearer token` This endpoint retrieves a specific signing request by its local ID. ## Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `requestId` | string | Yes | The local request ID | ## Request ```bash curl "https://api-prod.useknit.io/api/v1/managed-signing/requests/" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ## Response ```json filename="Response" { "statusCode": 200, "message": "Signing request retrieved", "data": { "id": "", "businessId": "", "walletId": "", "network": "MATIC_MAINNET", "type": "evm_tx", "kind": "erc20_approve", "status": "SIGNED", "payload": { "token": "0xToken", "spender": "0xSpender", "amount": "10" }, "signature": "0x...", "broadcast": false, "createdAt": "2026-01-20T18:30:40.912Z", "updatedAt": "2026-01-20T18:35:22.456Z" }, "success": true } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Local request ID | | `businessId` | string | Your business ID | | `walletId` | string | Local wallet ID | | `network` | string | Target network | | `type` | string | Request type | | `kind` | string | Transaction kind | | `status` | string | Request status | | `payload` | object | Request payload | | `signature` | string | Generated signature (if signed) | | `broadcast` | boolean | Broadcast flag | | `createdAt` | string | Creation timestamp | | `updatedAt` | string | Last update timestamp | ## Request Statuses | Status | Description | |--------|-------------| | `AWAITING_APPROVAL` | Request is pending approval | | `SIGNED` | Request has been signed | | `POLICY_DENIED` | Request was denied by policy | | `FAILED` | Request failed | # Get Managed Signing Wallet `GET https://api-prod.useknit.io/api/v1/managed-signing/wallets/{walletId}` - Required scope: `managed-signing:read` - Auth: `Bearer token` This endpoint retrieves a specific managed signing wallet by its local ID. ## Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `walletId` | string | Yes | The local wallet ID returned by create/list endpoints | ## Request ```bash curl "https://api-prod.useknit.io/api/v1/managed-signing/wallets/" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ## Response ```json filename="Response" { "statusCode": 200, "message": "Wallet retrieved", "data": { "id": "", "businessId": "", "type": "EVM", "networks": ["ETHEREUM_MAINNET"], "network": "ETHEREUM_MAINNET", "address": "0x...", "hdPath": "m/44'/60'/0'/0/0", "keyId": "", "isActive": true, "merchantCallbackUrl": "https://example.com/callback", "createdAt": "2026-01-20T18:30:40.912Z", "updatedAt": "2026-01-20T18:30:40.912Z" }, "success": true } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Local wallet ID | | `businessId` | string | Your business ID | | `type` | string | Wallet type | | `networks` | array | Supported networks | | `network` | string | Primary network | | `address` | string | Wallet address | | `hdPath` | string | HD derivation path | | `keyId` | string | Key identifier | | `isActive` | boolean | Wallet active status | | `merchantCallbackUrl` | string | Callback URL | | `createdAt` | string | Creation timestamp | | `updatedAt` | string | Last update timestamp | # Get Wallet Assets `GET https://api-prod.useknit.io/api/v1/managed-signing/wallets/{walletId}/assets` - Required scope: `managed-signing:read` - Auth: `Bearer token` This endpoint fetches balances for all supported tokens across all networks the wallet is active on. This is a live query to the blockchain — no balance data is stored locally. ## Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `walletId` | string | Yes | The local wallet ID returned by create/list endpoints | ## Request ```bash curl "https://api-prod.useknit.io/api/v1/managed-signing/wallets//assets" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ## Response ```json filename="Response" { "statusCode": 200, "message": "Managed signing wallet assets fetched", "data": { "walletId": "", "walletAddress": "0x...", "walletType": "EVM", "networks": [ { "network": "ETH_MAINNET", "tokens": [ { "tokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "tokenSymbol": "USDC", "decimals": 6, "balanceRaw": "1200000", "balanceFormatted": "1.2" } ] }, { "network": "MATIC_MAINNET", "tokens": [ { "tokenAddress": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", "tokenSymbol": "USDC", "decimals": 6, "balanceRaw": "3000000", "balanceFormatted": "3.0" } ] } ] }, "success": true } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `walletId` | string | Wallet identifier | | `walletAddress` | string | On-chain wallet address | | `walletType` | string | Wallet type (e.g., `EVM`) | | `networks` | array | Array of network objects | | `networks[].network` | string | Network identifier | | `networks[].tokens` | array | Token balances on this network | | `networks[].tokens[].tokenAddress` | string | ERC20 token contract address | | `networks[].tokens[].tokenSymbol` | string | Token symbol | | `networks[].tokens[].decimals` | number | Token decimal places | | `networks[].tokens[].balanceRaw` | string | Raw balance in base units | | `networks[].tokens[].balanceFormatted` | string | Human-readable balance | # Get Wallet Balance `GET https://api-prod.useknit.io/api/v1/managed-signing/wallets/{walletId}/balance` - Required scope: `managed-signing:read` - Auth: `Bearer token` This endpoint fetches the balance of a single token for an EVM wallet. This is a live query to the blockchain — no balance data is stored locally. ## Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `walletId` | string | Yes | The local wallet ID returned by create/list endpoints | ## Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `network` | string | Yes | Target network (e.g., `ETH_MAINNET`, `MATIC_MAINNET`) | | `contractAddress` | string | No | ERC20 token contract address | | `tokenSymbol` | string | No | Token symbol (e.g., `USDC`, `USDT`) | **Note:** If both `contractAddress` and `tokenSymbol` are provided, `contractAddress` takes precedence. If only `tokenSymbol` is provided, the token is resolved from the supported tokens for that network. ## Request ```bash curl "https://api-prod.useknit.io/api/v1/managed-signing/wallets//balance?network=ETH_MAINNET&tokenSymbol=USDC" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ## Response ```json filename="Response" { "statusCode": 200, "message": "Wallet balance fetched", "data": { "tokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "tokenSymbol": "USDC", "decimals": 6, "balanceRaw": "1200000", "balanceFormatted": "1.2" }, "success": true } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `tokenAddress` | string | ERC20 token contract address | | `tokenSymbol` | string | Token symbol | | `decimals` | number | Token decimal places | | `balanceRaw` | string | Raw balance in base units (e.g., wei) | | `balanceFormatted` | string | Human-readable balance | ## Common Errors | Status | Message | Description | |--------|---------|-------------| | 400 | `network is required` | Missing required `network` query parameter | | 400 | `Unsupported network ...` | Network not in supported list | | 400 | `Wallet is not active on network ...` | Wallet not configured for the requested network | | 400 | `Provide contractAddress, or provide tokenSymbol...` | Missing token identifier | | 404 | `Wallet not found` | Invalid or non-existent wallet ID | # List Audit Events `GET https://api-prod.useknit.io/api/v1/managed-signing/audit` - Required scope: `managed-signing:read` - Auth: `Bearer token` This endpoint retrieves audit events for signing requests. Audit events are fetched from Knit Core but mapped by local request ID. ## Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `requestId` | string | No | Filter by local request ID | ## Request ```bash curl "https://api-prod.useknit.io/api/v1/managed-signing/audit?requestId=" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ## Response ```json filename="Response" { "statusCode": 200, "message": "Audit events retrieved", "data": [ { "id": "", "requestId": "", "action": "SIGNING_REQUEST_CREATED", "actor": "api_key", "timestamp": "2026-01-20T18:30:40.912Z", "metadata": { "walletId": "", "network": "MATIC_MAINNET", "type": "evm_tx" } }, { "id": "", "requestId": "", "action": "SIGNING_REQUEST_APPROVED", "actor": "api_key", "timestamp": "2026-01-20T18:35:22.456Z", "metadata": { "walletId": "", "status": "SIGNED" } } ], "success": true } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Audit event ID | | `requestId` | string | Local request ID | | `action` | string | Action performed | | `actor` | string | Who performed the action | | `timestamp` | string | When the action occurred | | `metadata` | object | Additional event metadata | ## Audit Actions | Action | Description | |--------|-------------| | `SIGNING_REQUEST_CREATED` | A signing request was created | | `SIGNING_REQUEST_APPROVED` | A signing request was approved | | `SIGNING_REQUEST_DENIED` | A signing request was denied by policy | | `SIGNING_REQUEST_FAILED` | A signing request failed | ## Notes - Audit events provide a complete history of actions taken on signing requests. - Use the `requestId` query parameter to filter events for a specific signing request. - All request IDs in audit events are local IDs, not Knit Core IDs. # List Policies `GET https://api-prod.useknit.io/api/v1/managed-signing/policies` - Required scope: `managed-signing:read` - Auth: `Bearer token` This endpoint retrieves all policies for your business. ## Request ```bash curl "https://api-prod.useknit.io/api/v1/managed-signing/policies" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ## Response ```json filename="Response" { "statusCode": 200, "message": "Policies retrieved", "data": [ { "id": "", "businessId": "", "name": "default", "rules": { "chains": ["ETHEREUM_MAINNET"], "maxApprovalAmount": "1000000", "denyUnlimitedApprovals": true }, "createdAt": "2026-01-20T18:30:40.912Z", "updatedAt": "2026-01-20T18:30:40.912Z" } ], "success": true } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Local policy ID | | `businessId` | string | Your business ID | | `name` | string | Policy name | | `rules` | object | Policy rules configuration | | `createdAt` | string | Creation timestamp | | `updatedAt` | string | Last update timestamp | # List Signing Requests `GET https://api-prod.useknit.io/api/v1/managed-signing/requests` - Required scope: `managed-signing:read` - Auth: `Bearer token` This endpoint retrieves all signing requests for your business with optional filtering. ## Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `walletId` | string | No | Filter by local wallet ID | | `status` | string | No | Filter by status | ## Available Status Values - `AWAITING_APPROVAL` - `SIGNED` - `POLICY_DENIED` - `FAILED` ## Request ```bash curl "https://api-prod.useknit.io/api/v1/managed-signing/requests?walletId=&status=SIGNED" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ## Response ```json filename="Response" { "statusCode": 200, "message": "Signing requests retrieved", "data": [ { "id": "", "businessId": "", "walletId": "", "network": "MATIC_MAINNET", "type": "evm_tx", "kind": "erc20_approve", "status": "SIGNED", "payload": { "token": "0xToken", "spender": "0xSpender", "amount": "10" }, "signature": "0x...", "broadcast": false, "createdAt": "2026-01-20T18:30:40.912Z", "updatedAt": "2026-01-20T18:35:22.456Z" } ], "success": true } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Local request ID | | `businessId` | string | Your business ID | | `walletId` | string | Local wallet ID | | `network` | string | Target network | | `type` | string | Request type | | `kind` | string | Transaction kind | | `status` | string | Request status | | `payload` | object | Request payload | | `signature` | string | Generated signature (if signed) | | `broadcast` | boolean | Broadcast flag | | `createdAt` | string | Creation timestamp | | `updatedAt` | string | Last update timestamp | # List Managed Signing Wallets `GET https://api-prod.useknit.io/api/v1/managed-signing/wallets` - Required scope: `managed-signing:read` - Auth: `Bearer token` This endpoint retrieves all managed signing wallets for your business. ## Request ```bash curl "https://api-prod.useknit.io/api/v1/managed-signing/wallets" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" ``` ## Response ```json filename="Response" { "statusCode": 200, "message": "Wallets retrieved", "data": [ { "id": "", "businessId": "", "type": "EVM", "networks": ["ETHEREUM_MAINNET"], "network": "ETHEREUM_MAINNET", "address": "0x...", "hdPath": "m/44'/60'/0'/0/0", "keyId": "", "isActive": true, "merchantCallbackUrl": "https://example.com/callback", "createdAt": "2026-01-20T18:30:40.912Z", "updatedAt": "2026-01-20T18:30:40.912Z" } ], "success": true } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Local wallet ID | | `businessId` | string | Your business ID | | `type` | string | Wallet type | | `networks` | array | Supported networks | | `network` | string | Primary network | | `address` | string | Wallet address | | `hdPath` | string | HD derivation path | | `keyId` | string | Key identifier | | `isActive` | boolean | Wallet active status | | `merchantCallbackUrl` | string | Callback URL | | `createdAt` | string | Creation timestamp | | `updatedAt` | string | Last update timestamp | # Update Policy `PATCH https://api-prod.useknit.io/api/v1/managed-signing/policies/{policyId}` - Required scope: `managed-signing:write` - Auth: `Bearer token` This endpoint updates an existing policy by its local ID. ## Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `policyId` | string | Yes | The local policy ID | ## Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | No | Policy name | | `rules` | object | No | Policy rules configuration | | `rules.chains` | array | No | Allowed blockchain networks | | `rules.maxApprovalAmount` | string | No | Maximum approval amount | | `rules.maxTransferAmount` | string | No | Maximum transfer amount per transaction (in base units) | | `rules.denyUnlimitedApprovals` | boolean | No | Whether to deny unlimited approvals | ```json { "rules": { "maxApprovalAmount": "2000000", "denyUnlimitedApprovals": false } } ``` ## Request ```bash curl -X PATCH "https://api-prod.useknit.io/api/v1/managed-signing/policies/" \ -H "Authorization: Bearer $KNIT_ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "rules": { "maxApprovalAmount": "2000000", "denyUnlimitedApprovals": false } }' ``` ## Response ```json filename="Response" { "statusCode": 200, "message": "Policy updated", "data": { "id": "", "businessId": "", "name": "default", "rules": { "chains": ["ETHEREUM_MAINNET"], "maxApprovalAmount": "2000000", "denyUnlimitedApprovals": false }, "createdAt": "2026-01-20T18:30:40.912Z", "updatedAt": "2026-01-20T19:15:22.456Z" }, "success": true } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Local policy ID | | `businessId` | string | Your business ID | | `name` | string | Policy name | | `rules` | object | Policy rules configuration | | `createdAt` | string | Creation timestamp | | `updatedAt` | string | Last update timestamp | # Webhooks Knit delivers a signed `POST` to your endpoint whenever something you care about changes state. Webhooks are the intended way to track collections and payouts — polling is a fallback for reconciliation, not a primary integration. ## Configure your endpoint Set your webhook URL and secret in the dashboard under **Business → Developer → Webhooks**. There is no public API for changing them. Your endpoint must be: - **Publicly reachable** over `http://` or `https://`. Private, loopback, and internal addresses are rejected. - **Fast.** Deliveries time out after 10 seconds. Verify the signature, enqueue the work, and return `2xx` immediately. - **Idempotent.** The same event can arrive more than once. > **Note:** [Collections](https://docs.useknit.io/collections/create-a-collection) and [blockchain notification subscriptions](https://docs.useknit.io/blockchain-notifications) can each carry their own `merchantCallbackUrl` / `webhookUrl`, which takes precedence over the business-level URL for that resource's events. Everything else goes to the business-level URL. ## Verifying the signature Every delivery carries an `X-Signature` header: the **HMAC-SHA256 of the raw request body**, keyed with your webhook secret, hex-encoded. | Header | Value | | ------ | ----- | | `Content-Type` | `application/json` | | `Accept` | `application/json` | | `X-Signature` | Hex-encoded HMAC-SHA256 of the raw body | ```js filename="Node.js" import crypto from "node:crypto"; // `rawBody` must be the unparsed body. Re-serialising the parsed JSON will // produce a different byte sequence and the signature will not match. function verify(rawBody, signatureHeader, secret) { const expected = crypto .createHmac("sha256", secret) .update(rawBody) .digest("hex"); const a = Buffer.from(expected, "utf8"); const b = Buffer.from(signatureHeader ?? "", "utf8"); return a.length === b.length && crypto.timingSafeEqual(a, b); } ``` ```python filename="Python" import hashlib import hmac def verify(raw_body: bytes, signature_header: str, secret: str) -> bool: expected = hmac.new( secret.encode(), raw_body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, signature_header or "") ``` > **Warning:** Capture the raw body before any JSON middleware parses it. Signing a re-serialised object is the single most common cause of "the signature never matches". Always compare with a constant-time function, and reject the request if the header is missing. ## Retries A delivery counts as failed if your endpoint returns a non-2xx status, times out, or is unreachable. Knit then retries up to **3 times** with a fixed backoff: | Attempt | Sent | | ------- | ---- | | Initial | Immediately | | Retry 1 | 1 minute after the failure | | Retry 2 | 5 minutes after that | | Retry 3 | 30 minutes after that | After the third retry, delivery stops. The attempt history — status code, response body, and failure reason — is visible in the dashboard, where you can also trigger a manual resend. > **Warning:** The full automatic retry window is roughly **36 minutes**, not hours. If your endpoint is down longer than that, reconcile by reading the affected resources back from the API rather than waiting for a redelivery. Near-identical events for the same business within a five-minute window are collapsed into a single delivery, so a duplicated upstream signal does not become two webhooks. That is a safety net, not a guarantee — keep your handler idempotent. ## Payload shape Every payload carries an `eventType`. Two things vary by event, so check the table before you write your parser: - **Envelope.** Most events are wrapped in a top-level `body` object. Two are not. - **Key casing.** Some payloads use snake_case, others camelCase. | Event | Envelope | Key casing | | ----- | -------- | ---------- | | [`COLLECTION_CREATED`](https://docs.useknit.io/webhooks/collection-created) | `body` wrapper | snake_case | | [`COLLECTION_CONFIRMED`](https://docs.useknit.io/webhooks/collection-confirmed) | `body` wrapper | snake_case | | [`COLLECTION_SUCCESSFUL`](https://docs.useknit.io/webhooks/collection-successful) | `body` wrapper | snake_case | | [`COLLECTION_FAILED`](https://docs.useknit.io/webhooks/collection-failed) | `body` wrapper | snake_case | | [`PAYOUT_SUCCESSFUL`](https://docs.useknit.io/webhooks/payout-successful) | `body` wrapper | camelCase | | [`WALLET_FUNDING_SUCCESSFUL`](https://docs.useknit.io/webhooks/wallet-funding-successful) | top level | snake_case | | [`BLOCKCHAIN_TRANSACTION_DETECTED`](https://docs.useknit.io/webhooks/blockchain-transaction-detected) | top level | camelCase | > **Warning:** Webhook payloads do **not** follow the camelCase convention that API responses do — the transformation applied to API responses is not applied to outbound webhooks. Read each event's page for its exact keys rather than assuming they match the equivalent `GET` response. A dispatcher that handles both shapes: ```js filename="Handling both envelopes" app.post("/webhooks/knit", (req, res) => { if (!verify(req.rawBody, req.get("X-Signature"), process.env.KNIT_WEBHOOK_SECRET)) { return res.sendStatus(401); } // Acknowledge first — the delivery times out after 10 seconds. res.sendStatus(200); const payload = req.body.body ?? req.body; enqueue(payload.eventType, payload); }); ``` ## Events - [Collection created](https://docs.useknit.io/webhooks/collection-created) — `COLLECTION_CREATED` — a collection was created and its address issued. - [Collection confirmed](https://docs.useknit.io/webhooks/collection-confirmed) — `COLLECTION_CONFIRMED` — the deposit reached your confirmation threshold. - [Collection successful](https://docs.useknit.io/webhooks/collection-successful) — `COLLECTION_SUCCESSFUL` — funds were credited to your API account. - [Collection failed](https://docs.useknit.io/webhooks/collection-failed) — `COLLECTION_FAILED` — the collection could not be completed. - [Payout successful](https://docs.useknit.io/webhooks/payout-successful) — `PAYOUT_SUCCESSFUL` — a payout settled on-chain. - [Wallet funding successful](https://docs.useknit.io/webhooks/wallet-funding-successful) — `WALLET_FUNDING_SUCCESSFUL` — an API wallet received a deposit. - [Transaction detected](https://docs.useknit.io/webhooks/blockchain-transaction-detected) — `BLOCKCHAIN_TRANSACTION_DETECTED` — a watched address sent or received stablecoin. # `BLOCKCHAIN_TRANSACTION_DETECTED` Delivered when an address covered by one of your [blockchain notification subscriptions](https://docs.useknit.io/blockchain-notifications) sends or receives a matching token. - Envelope: `top level — no body wrapper` - Keys: `camelCase` Delivered to the subscription's `webhookUrl`, falling back to your business webhook URL. > **Warning:** This payload is **not** wrapped in a `body` object — `eventType` sits at the top level, alongside the transaction fields. ## Payload ```json filename="BLOCKCHAIN_TRANSACTION_DETECTED" { "eventType": "BLOCKCHAIN_TRANSACTION_DETECTED", "id": "0a52d1c4-2f43-4e58-9d40-2f0c5cbb9e77", "subscriptionId": "3f6f9d1a-64b2-4c7f-9a1e-7a2f0c8c7e11", "address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "type": "EVM", "network": "MATIC_MAINNET", "token": "USDC", "direction": "INCOMING", "amount": "125.50", "rawAmount": "125500000", "fromAddress": "0x1111111111111111111111111111111111111111", "toAddress": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "transactionHash": "0xb0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f70819", "eventIndex": 3, "blockNumber": 61234567, "metadata": { "customerId": "cus_8121" } } ``` - **`subscriptionId` (string)** — The subscription that matched. Route on this to find what you were watching for. - **`address` (string)** — The watched address, as stored on the subscription. - **`direction` (string)** — `INCOMING` or `OUTGOING`, relative to the watched address. - **`amount` (string)** — The transfer amount in token units, as a decimal string. - **`rawAmount` (string)** — The same amount in the token's base units. - **`eventIndex` (integer)** — The transfer's position within the transaction. A single transaction can contain several transfers, so `transactionHash` alone is not unique. - **`metadata` (object | null)** — Whatever you stored on the subscription, returned verbatim. ## Deduplication Use `transactionHash` **and** `eventIndex` together as the idempotency key. A transaction that moves stablecoin more than once produces one event per transfer, all sharing the same hash. ```js filename="Idempotency key" const key = `${payload.transactionHash}:${payload.eventIndex}`; ``` > **Warning:** Knit does not hold the funds these events describe. The webhook tells you money moved; acting on it — crediting a customer, releasing goods — is entirely your decision, and you should apply your own confirmation policy before treating a transfer as final. ## See also - [Blockchain notifications](https://docs.useknit.io/blockchain-notifications) - [Create a subscription](https://docs.useknit.io/blockchain-notifications/create-subscription) - [Webhooks overview](https://docs.useknit.io/webhooks) — signature verification and retries # `COLLECTION_CONFIRMED` Delivered when the deposit reaches the `confirmationThreshold` you set when [creating the collection](https://docs.useknit.io/collections/create-a-collection). - Envelope: `body wrapper` - Keys: `snake_case` Delivered to the collection's `merchantCallbackUrl`, falling back to your business webhook URL. ## Payload ```json filename="COLLECTION_CONFIRMED" { "body": { "eventType": "COLLECTION_CONFIRMED", "collection": { "id": "4ebdfe45-a62b-47a9-ac31-7599305666b1", "business_id": "b7b93a40-4e18-4e97-9c5f-8a7a4fd0df92", "network": "MATIC_MAINNET", "token": "USDT", "address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "token_amount": "25.000000000000000000", "token_amount_requested": "25.000000000000000000", "token_amount_received": "25.000000000000000000", "token_to_usd": "1.00000000", "status": "SUCCESSFUL", "transaction_hash": "0xb480ed44a275f042e482a78d9c5b54fcf612441c", "transaction_block_number": 61234567, "number_of_confirmations": 10, "confirmation_threshold": 10, "merchant_redirect_url": "https://example.com/thanks", "merchant_callback_url": "https://example.com/webhooks/knit", "expires_at": "2024-02-22T17:29:33.000000Z", "created_at": "2024-02-22T16:59:33.000000Z", "updated_at": "2024-02-22T17:00:30.000000Z" } } } ``` > **Note:** Confirmation and crediting are separate steps. This event says the chain has confirmed the deposit to your satisfaction; [`COLLECTION_SUCCESSFUL`](https://docs.useknit.io/webhooks/collection-successful) says the funds have landed in your API account. ## Handling Compare `token_amount_received` against `token_amount_requested` before treating the payment as settled in full — an underpayment confirms just like a full payment does. ## See also - [`COLLECTION_SUCCESSFUL`](https://docs.useknit.io/webhooks/collection-successful) - [Webhooks overview](https://docs.useknit.io/webhooks) — signature verification and retries # `COLLECTION_CREATED` Delivered immediately after a [collection is created](https://docs.useknit.io/collections/create-a-collection). Nothing has been paid yet — this is your confirmation that the address and payment link are live. - Envelope: `body wrapper` - Keys: `snake_case` Delivered to the collection's `merchantCallbackUrl`, falling back to your business webhook URL. ## Payload ```json filename="COLLECTION_CREATED" { "body": { "eventType": "COLLECTION_CREATED", "collection": { "id": "9cddd3ad-c2a8-463c-a703-158b900beea8", "business_id": "b7b93a40-4e18-4e97-9c5f-8a7a4fd0df92", "network": "MATIC_MAINNET", "token": "USDT", "address": "0x3761f3504104f4faa8959963a5d8dce89989d45b", "token_amount": "2500.000000000000000000", "token_amount_requested": "2500.000000000000000000", "token_amount_received": null, "token_to_usd": "1.00000000", "fee_in_usd": "0.00000000", "status": "PENDING", "transaction_hash": null, "number_of_confirmations": null, "confirmation_threshold": 10, "merchant_redirect_url": "https://example.com/thanks", "merchant_callback_url": "https://example.com/webhooks/knit", "payment_link_url": "https://checkout.collection.useknit.io/9cddd3ad-c2a8-463c-a703-158b900beea8", "expires_at": "2024-08-27T14:02:10.000000Z", "created_at": "2024-08-27T13:32:10.000000Z", "updated_at": "2024-08-27T13:32:10.000000Z" } } } ``` > **Warning:** Collection payloads use **snake_case** keys — `token_amount`, not `tokenAmount`. This differs from the camelCase you get back from `GET /api/v1/collections`. ## Handling Treat this as an acknowledgement, not a payment. Do not release goods or credit a customer here — wait for [`COLLECTION_SUCCESSFUL`](https://docs.useknit.io/webhooks/collection-successful). Because a collection expires 30 minutes after creation, this is a good moment to start your own expiry timer. ## See also - [Create a collection](https://docs.useknit.io/collections/create-a-collection) - [Webhooks overview](https://docs.useknit.io/webhooks) — signature verification and retries # `COLLECTION_FAILED` Delivered when a collection cannot be completed. No funds have been credited to your API account. - Envelope: `body wrapper` - Keys: `snake_case` Delivered to the collection's `merchantCallbackUrl`, falling back to your business webhook URL. ## Payload ```json filename="COLLECTION_FAILED" { "body": { "eventType": "COLLECTION_FAILED", "collection": { "id": "4ebdfe45-a62b-47a9-ac31-7599305666b1", "business_id": "b7b93a40-4e18-4e97-9c5f-8a7a4fd0df92", "network": "MATIC_MAINNET", "token": "USDT", "address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "token_amount": "25.000000000000000000", "token_amount_requested": "25.000000000000000000", "token_amount_received": null, "status": "FAILED", "transaction_hash": null, "number_of_confirmations": null, "confirmation_threshold": 10, "merchant_redirect_url": "https://example.com/thanks", "merchant_callback_url": "https://example.com/webhooks/knit", "expires_at": "2024-02-22T17:29:33.000000Z", "created_at": "2024-02-22T16:59:33.000000Z", "updated_at": "2024-02-22T17:00:30.000000Z" } } } ``` ## Handling Mark the payment attempt as failed on your side and, if the customer still wants to pay, [create a new collection](https://docs.useknit.io/collections/create-a-collection) — addresses are single-use and cannot be revived. > **Note:** A collection that simply expires unpaid is a different situation: no funds moved and there is nothing to reconcile. Use `expires_at` to close out abandoned checkouts rather than waiting for an event. ## See also - [`COLLECTION_SUCCESSFUL`](https://docs.useknit.io/webhooks/collection-successful) - [Webhooks overview](https://docs.useknit.io/webhooks) — signature verification and retries # `COLLECTION_SUCCESSFUL` Delivered when a collection's funds have been credited to your API account. This is the event to act on: the money is yours and available to [pay out](https://docs.useknit.io/payouts/create-a-single-payout). - Envelope: `body wrapper` - Keys: `snake_case` Delivered to the collection's `merchantCallbackUrl`, falling back to your business webhook URL. ## Payload ```json filename="COLLECTION_SUCCESSFUL" { "body": { "eventType": "COLLECTION_SUCCESSFUL", "collection": { "id": "4ebdfe45-a62b-47a9-ac31-7599305666b1", "business_id": "b7b93a40-4e18-4e97-9c5f-8a7a4fd0df92", "network": "MATIC_MAINNET", "token": "USDT", "address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "token_amount": "25.000000000000000000", "token_amount_requested": "25.000000000000000000", "token_amount_received": "25.000000000000000000", "token_to_usd": "1.00000000", "fee_in_usd": "0.00000000", "status": "SUCCESSFUL", "transaction_hash": "0xb480ed44a275f042e482a78d9c5b54fcf612441c", "transaction_block_number": 61234567, "number_of_confirmations": 12, "confirmation_threshold": 10, "merchant_redirect_url": "https://example.com/thanks", "merchant_callback_url": "https://example.com/webhooks/knit", "expires_at": "2024-02-22T17:29:33.000000Z", "created_at": "2024-02-22T16:59:33.000000Z", "updated_at": "2024-02-22T17:00:30.000000Z" } } } ``` ## Partial payments `status` is `SUCCESSFUL_PARTIAL` when less than the requested amount arrived. The received amount is still credited, so treat this event as money-in — but reconcile the shortfall. ```js filename="Distinguishing full from partial" const c = payload.collection; const paidInFull = c.status === "SUCCESSFUL"; const shortfall = Number(c.token_amount_requested) - Number(c.token_amount_received ?? 0); ``` > **Warning:** Always credit your customer against `token_amount_received`, never against `token_amount_requested`. Amounts are decimal strings — parse them with a decimal library rather than a float. ## Handling 1. Verify the signature and return `2xx` promptly. 2. Look the collection up by `id` on your side and check you have not already processed it — this event can arrive more than once. 3. Credit against `token_amount_received`. 4. Fulfil the order. ## See also - [`COLLECTION_FAILED`](https://docs.useknit.io/webhooks/collection-failed) - [Retrieve a collection](https://docs.useknit.io/collections/get-single-collection) — to reconcile - [Webhooks overview](https://docs.useknit.io/webhooks) — signature verification and retries # `PAYOUT_SUCCESSFUL` Delivered when a payout reaches `COMPLETED` — the transfer is confirmed on-chain. Use it to reconcile your ledger, release downstream workflows, or notify your customer. - Envelope: `body wrapper` - Keys: `camelCase` Delivered to your business webhook URL. > **Note:** This event fires **only** for completed payouts. There is no webhook for `PENDING`, `PROCESSING`, or `FAILED` — to detect a failure, read the payout back with [Retrieve a payout](https://docs.useknit.io/payouts/get-single-payout) or [Get payout status](https://docs.useknit.io/payouts/get-payout-status). ## Payload ```json filename="PAYOUT_SUCCESSFUL" { "body": { "eventType": "PAYOUT_SUCCESSFUL", "payout": { "id": "8e5697e3-8265-455b-984a-0eb40e10b0f9", "businessId": "b7b93a40-4e18-4e97-9c5f-8a7a4fd0df92", "network": "MATIC_MAINNET", "token": "USDT", "amount": "100.250000000000000000", "toAddress": "0x56adfcc254ab3b8142a275c1837bcffaff5aa38b", "merchantReference": "INV-2045", "transactionHash": "0xb0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f70819", "status": "COMPLETED", "info": null, "createdAt": "2024-10-02T11:21:33.000000Z", "updatedAt": "2024-10-02T11:32:01.000000Z" } } } ``` > **Warning:** Unlike the collection events, this payload uses **camelCase** keys. Do not share a parser between the two without normalising first. ## Handling ### Verify and acknowledge Check `X-Signature` against the raw body, then return `2xx` straight away. See [Webhooks](https://docs.useknit.io/webhooks) for the verification snippet. ### Deduplicate on `merchantReference` `merchantReference` is the value you supplied when creating the payout, so it maps directly to your own ledger entry. Check whether you have already marked it complete before doing anything else — this event can arrive more than once. ### Settle Mark the payout complete, release any funds you were holding, and record `transactionHash` for your audit trail. ## Not receiving it? - Confirm the webhook URL and secret in **Business → Developer → Webhooks**, and that the endpoint is publicly reachable. - Check the delivery attempts in the dashboard — a `4xx` from your endpoint is recorded there along with the response body. - Confirm the payout actually reached `COMPLETED`; a payout stuck in `PROCESSING` produces no event. - Remember the retry window is roughly 36 minutes. Past that, reconcile with [List payouts](https://docs.useknit.io/payouts/get-all-payouts). ## See also - [Create a payout](https://docs.useknit.io/payouts/create-a-single-payout) - [Webhooks overview](https://docs.useknit.io/webhooks) — signature verification and retries # `WALLET_FUNDING_SUCCESSFUL` Delivered when one of your [API wallets](https://docs.useknit.io/wallets/create-a-wallet) receives a confirmed deposit. The funds are credited to your API account and available to pay out. - Envelope: `top level — no body wrapper` - Keys: `snake_case` Delivered to your business webhook URL. > **Warning:** This payload is **not** wrapped in a `body` object — `eventType` sits at the top level. Collection and payout events are wrapped; this one is not. ## Payload ```json filename="WALLET_FUNDING_SUCCESSFUL" { "eventType": "WALLET_FUNDING_SUCCESSFUL", "wallet": { "id": "3db04cff-3ea7-4387-891f-c1657a02a272", "business_id": "b7b93a40-4e18-4e97-9c5f-8a7a4fd0df92", "network": "MATIC_MAINNET", "network_id": "MATIC_MAINNET", "networks": ["MATIC_MAINNET"], "address": "0x1adb0a39bde00fa0957e519584cb5c51fefcb37f", "is_active": true, "created_at": "2024-02-22T17:12:50.000000Z", "updated_at": "2024-02-22T17:12:50.000000Z" }, "transaction": { "id": "b147db2c-4861-4fa5-9dff-086d6e06d116", "wallet_id": "3db04cff-3ea7-4387-891f-c1657a02a272", "business_id": "b7b93a40-4e18-4e97-9c5f-8a7a4fd0df92", "network": "MATIC_MAINNET", "token": "USDT", "from_address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "to_address": "0x1adb0a39bde00fa0957e519584cb5c51fefcb37f", "value": "250.00000000", "fee_by_token": "0.00000000", "number_of_confirmations": 12, "status": "SUCCESSFUL", "transaction_hash": "0x6c3db59292c98c2a2c14f820", "created_at": "2024-02-23T16:44:49.000000Z", "updated_at": "2024-02-23T16:44:49.000000Z" } } ``` - **`wallet` (object)** — The API wallet that received the deposit. Match `wallet.id` against the customer or ledger you associated with it. - **`transaction` (object)** — The deposit itself. `value` is the amount received, as a decimal string. ## Handling Credit against `transaction.value`, and use `transaction.transaction_hash` for deduplication — a chain reorganisation or a redelivery can produce the same transaction twice. ## See also - [Create a wallet](https://docs.useknit.io/wallets/create-a-wallet) - [List wallet transactions](https://docs.useknit.io/wallets/get-single-wallet-transactions) — to reconcile - [Webhooks overview](https://docs.useknit.io/webhooks) — signature verification and retries