Skip to main content
Use Quickstart with TypeScript SDK for first setup. Use this page after connection to choose client methods, signing helpers, diagnostics, path helpers, and exports. MCP users who do not already have a wallet or signing service can also use this page to set up the TypeScript SDK with viem as a local testnet signing harness for typed data prepared by Quickstart with MCP. Use literal SDK symbols exactly as exported by the package, including ApiClient, AgreementsApiError, agreementsApiPaths, and API_BASE_PATH. The package targets Node >=18.

Install

npm install @cns-labs/agreements-api-client viem
@cns-labs/agreements-protocol-evm is installed automatically as a package dependency. Install viem in your app when you use permit-signing helpers. The Agreements API client and the onchain protocol SDK are both published under the @cns-labs npm organization. The agreements-api-playground sample application uses @cns-labs/agreements-api-client for API calls and @cns-labs/agreements-protocol-evm for onchain agreement typing and signing support.

Create a client

import { ApiClient } from '@cns-labs/agreements-api-client';

const client = new ApiClient({
  environment: 'testnet',
  apiKey: process.env.API_KEY,
});

const health = await client.getHealth();
Constructor config:
FieldUse
environmentNamed API environment. Supported values are testnet and production.
baseUrlOptional API host base URL override, without /v0 appended. Use for local proxies, staging hosts, or custom deployments.
apiKeyOptional X-API-Key value. Most API methods require it.
headersOptional header record or header factory merged into each request.
fetchOptional fetch implementation. Defaults to globalThis.fetch.
If both environment and baseUrl are supplied, baseUrl wins. testnet resolves to https://test-api.shodai.network; production resolves to https://api.shodai.network. For key provisioning, scopes, entitlements, and 401/402/403 behavior, see Authentication.

Wallet and RPC requirements

The API key authenticates requests. Deploying agreements and submitting agreement inputs also require a wallet your integration controls because those workflows depend on EIP-712 signatures.
RequirementWhy it matters
walletClientControls the signing account and produces EIP-712 signatures. A plain wallet address is not enough.
publicClientReads target-chain context, factory or agreement contract state, and current permit nonces.
Target chain/RPCMust match the selected deployment chain or the deployed agreement’s chain.
Signer eligibilityFor inputs, the signing wallet must be allowed by the authored input issuer.

Create a test-only wallet client

For automated tests that only need EIP-712 signatures, create an ephemeral wallet with viem. This wallet does not need gas when it only signs permits. Do not use this pattern for production wallets, and do not commit generated private keys.
import { createPublicClient, createWalletClient, http } from 'viem';
import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts';
import { lineaSepolia } from 'viem/chains';

const account = privateKeyToAccount(generatePrivateKey());

const walletClient = createWalletClient({
  account,
  chain: lineaSepolia,
  transport: http(process.env.RPC_URL),
});

const publicClient = createPublicClient({
  chain: lineaSepolia,
  transport: http(process.env.RPC_URL),
});
The testnet API environment supports Linea Sepolia (59141), Ethereum Sepolia (11155111), and Base Sepolia (84532) for agreement deployments. The production API environment supports Linea Mainnet (59144) and Base Mainnet (8453). Choose the chainId explicitly for deployment preflight and deploy requests, then reuse the deployed agreement record’s chainId when signing inputs. Use account.address in participant mappings when the test wallet needs to deploy an agreement or submit an input for a participant role. Before calling deployAgreementWithPermit(...) or submitAgreementInputWithPermit(...), confirm that:
  1. you have an API key for authenticated requests
  2. you have a walletClient that can sign with the intended account
  3. you have a publicClient connected to the target chain
  4. the wallet chain matches the deployment or agreement chain
  5. the signer is appropriate for the deployment or input issuer
  6. you can regenerate signatures when nonce, deadline, payload, chain, agreement JSON, or values change

Choose the right SDK surface

Use casePrefer
Normal API callsApiClient methods such as validateTemplate, validateDeployment, and getAgreementState.
Deploy with an EIP-712 permitdeployAgreementWithPermit(...).
Submit a signed inputsubmitAgreementInputWithPermit(...).
Sign first, submit later, or customize request compositionsignDeployWithPermit(...) or signAgreementInputPermit(...) plus client.deployWithPermit(...) or client.submitAgreementInput(...).
Debug HTTP status, headers, or raw body textclient.exchangeJson(...).
Compose a raw request path safelyagreementsApiPaths.
Construct typed data manually without SDK helpersEIP-712 Signing Reference.

Main client methods

The public API returns JSON envelopes for authenticated agreement routes. SDK methods that read one resource unwrap data and return the resource directly. List methods return the list envelope so your integration can read data, pageInfo, and meta.
const agreement = await client.getAgreement(agreementId);

const page = await client.listAgreements({ limit: 25 });
console.log(page.data, page.pageInfo.nextCursor, page.meta.requestId);
MethodUse
getOpenApiDocument()Fetch GET /v0/openapi.json.
getHealth()Check GET /v0/health.
listAgreements(params)List paged agreement summaries visible to the API key. Supports limit, cursor, sort, chainId, state, and created/updated date filters.
getAgreement(agreementId)Read one agreement record.
validateTemplate(agreement)Validate authored agreement JSON with POST /v0/agreements/validate-template.
validateDeployment(body)Preflight deployment context with POST /v0/agreements/validate.
deployWithPermit(body)Deploy with a prepared EIP-712 permit.
getAgreementState(agreementId)Read the current agreement state.
listAgreementInputs(agreementId, params)Read paged input history. Supports limit, cursor, sort, userId, inputId, status, and created/updated date filters.
submitAgreementInput(agreementId, body)Submit a signed input.
exchangeJson(method, path, body?)Inspect raw response metadata without throwing for HTTP errors.
request<T>(method, path, body?, okStatus?)Low-level JSON request that returns the raw JSON body and throws on unexpected status.
List filters use qs-style bracket modifiers. For example, { createdAt: { gte: '...' }, sort: { createdAt: 'desc' } } serializes as createdAt[gte]=...&sort[createdAt]=desc. Agreement lists support chainId, state, createdAt, updatedAt, limit, cursor, and one sort field from createdAt, updatedAt, or displayName. Input history supports userId, inputId, status, createdAt, updatedAt, limit, cursor, and one sort field from createdAt or updatedAt. Date filters support gt, gte, lt, and lte; limit must be between 1 and 100.
const agreementsPage = await client.listAgreements({
  chainId: 59141,
  state: 'AWAITING_PAYMENT',
  createdAt: { gte: '2026-05-01T00:00:00.000Z' },
  sort: { createdAt: 'desc' },
  limit: 25,
});
const nextAgreementPage = agreementsPage.pageInfo.nextCursor
  ? await client.listAgreements({ cursor: agreementsPage.pageInfo.nextCursor, limit: 25 })
  : null;

const inputsPage = await client.listAgreementInputs('agreement-123', {
  userId: 'platform-user-id',
  status: 'MINED',
  updatedAt: { lt: '2026-06-01T00:00:00.000Z' },
  sort: { updatedAt: 'asc' },
  limit: 25,
});

Validate and deploy with helpers

Use deployment preflight before signing when initValues, participant mappings, or observers affect the deployment request.
const chainId = 59141;

const validation = await client.validateDeployment({
  agreement,
  chainId,
  initValues,
  participants,
  observers,
});

console.log(validation.variables);
Then use the high-level helper for the normal sign-and-submit path.
import { deployAgreementWithPermit } from '@cns-labs/agreements-api-client';

const agreementRecord = await deployAgreementWithPermit({
  client,
  walletClient,
  publicClient,
  chainId,
  agreement,
  displayName: 'Consulting Agreement',
  initValues: validation.variables,
  participants,
  observers,
});
Participant-derived values in validation.variables are the effective values the SDK signs for deployment. Keep the same participants array in deployAgreementWithPermit(...) so hosted agreement context records the participant mappings.

Submit signed inputs with helpers

import { submitAgreementInputWithPermit } from '@cns-labs/agreements-api-client';

await submitAgreementInputWithPermit({
  client,
  agreementId: agreementRecord.id,
  walletClient,
  publicClient,
  chainId: agreementRecord.chainId,
  agreementContractAddress,
  agreement,
  inputId: 'partyASignature',
  values,
});
publicClient must be connected to the target chain/RPC. Input submissions also require chainId from the deployed agreement record so the helper can fail before signing if the client is connected to the wrong chain. The signing helpers use publicClient to resolve the chain-specific AgreementFactory or agreement contract context and to read the current permit nonce before signing.

Control deadlines and low-level signing

import {
  computeDefaultDeadlineSeconds,
  signAgreementInputPermit,
  signDeployWithPermit,
} from '@cns-labs/agreements-api-client';
DEFAULT_PERMIT_DEADLINE_SECONDS is 3600. computeDefaultDeadlineSeconds(offsetSeconds = 3600) returns the current Unix time plus the offset. High-level deployAgreementWithPermit(...) and submitAgreementInputWithPermit(...) default the deadline. Low-level signDeployWithPermit(...) and signAgreementInputPermit(...) require an explicit deadline. Use low-level signing helpers when your application needs to sign first and submit later, inspect the signature, or compose the request body itself. Use EIP-712 Signing Reference only when you need to construct the typed data directly or debug helper behavior.

Handle API errors

The client throws AgreementsApiError when the response status does not match the expected success status.
import { AgreementsApiError } from '@cns-labs/agreements-api-client';

try {
  await client.listAgreements();
} catch (error) {
  if (error instanceof AgreementsApiError) {
    console.error(error.status, error.errorPayload?.error.message ?? error.bodyText);
  }
  throw error;
}
AgreementsApiError exposes status, bodyText, parsedBody, and errorPayload. errorPayload.error.code is stable for branching, and errorPayload.error.requestId is the value to share when you need support to trace a request.

Use raw exchange for diagnostics

Use exchangeJson() when you need response metadata or raw body text during debugging. It does not throw for HTTP error status codes.
const response = await client.exchangeJson('GET', '/v0/agreements');

console.log(response.status, response.ok, response.parsedBody);
exchangeJson() accepts only GET and POST. It returns status, ok, headers, bodyText, and parsedBody.

Use path helpers when composing raw calls

import { agreementsApiPaths } from '@cns-labs/agreements-api-client';

const path = agreementsApiPaths.agreementState('agreement-123');
Path helper names are openapiJson, health, agreements, agreementsValidate, agreementsValidateTemplate, agreementsDeployWithPermit, agreement, agreementState, agreementInputs, and agreementInput. ID path helpers encode IDs and use API_BASE_PATH, which is /v0.

Root exports

The package root exports:
  • ApiClient
  • AgreementsApiError
  • extractAgreementsApiErrorMessage
  • agreementsApiPaths
  • getExecutionInputIds
  • API_BASE_PATH
  • API_ENVIRONMENT_BASE_URLS
  • API_MAJOR_VERSION
  • DEFAULT_API_ENVIRONMENT
  • resolveApiBaseUrl
  • joinUrl
  • computeDefaultDeadlineSeconds
  • DEFAULT_PERMIT_DEADLINE_SECONDS
  • signDeployWithPermit
  • signAgreementInputPermit
  • deployAgreementWithPermit
  • submitAgreementInputWithPermit