# Migrate from SDK 3.1 to 3.2

Source: https://docs.settlemint.com/docs/api-reference/sdk/migration
Move a 3.1 TypeScript SDK integration to 3.2: client options, flat route calls, write handles, idempotency keys, summary responses, imports, and errors.

The 3.2 TypeScript SDK changes how you construct the client, call routes, handle writes, and import helpers. Code written against 3.1 does not compile against 3.2. The [TypeScript SDK reference](https://docs.settlemint.com/docs/api-reference/sdk) describes the 3.2 client in full. Work through these steps in order:

1. [Install the 3.2 package](#install-the-32-package).
2. [Update the client options](#update-the-client-options).
3. [Flatten route calls](#flatten-route-calls).
4. [Handle write results](#handle-write-results).
5. [Move idempotency keys onto the call](#move-idempotency-keys-onto-the-call).
6. [Handle summary responses](#handle-summary-responses).
7. [Update imports and webhook verification](#update-imports-and-webhook-verification).
8. [Update error handling](#update-error-handling).

## Install the 3.2 package [#install-the-32-package]

Install the `@settlemint/dalp-sdk` release whose major and minor version match your DALP instance. Move to the 3.2 client when the instance runs DALP 3.2. A 3.2 client against a 3.1 instance can name routes the instance does not serve. [Install and configure the SDK](https://docs.settlemint.com/docs/api-reference/sdk/install#install) has the install command and explains why the versions must match, and [Upgrade an integration to DALP 3.2](https://docs.settlemint.com/docs/api-reference/guides/upgrade-to-3-2) covers the platform changes.

## Update the client options [#update-the-client-options]

```ts
// 3.1
const dalp = createDalpClient({
  url: "https://dalp.example.com",
  apiKey: process.env.DALP_API_KEY!,
  organizationId: "org_01J00000000000000000000000",
});
```

```ts
// 3.2
import { createDalpClient, secret } from "@settlemint/dalp-sdk";

const dalp = createDalpClient({
  baseUrl: "https://dalp.example.com",
  apiKey: secret(process.env.DALP_API_KEY!),
  runtime: "server",
});
```

| 3.1 option | 3.2 option |
| --- | --- |
| `url` | `baseUrl`. |
| `apiKey` | `apiKey`. It accepts a string or a `Secret`. The client wraps a string itself. Pass `secret()` to keep the key redacted in your own code as well. |
| `organizationId` | No option. A credential names the organisation: an API key acts in the organisation it was issued for, and a session in its active organisation, which `organization.setActive` changes. `context` takes only the acting-context fields the platform reads, listed in [The acting context](https://docs.settlemint.com/docs/api-reference/sdk/install#the-acting-context). |
| `headers` | No client option. Pass `headers` per call in the second argument, as an object or a synchronous function. Per-call headers override the credential and context headers. For a header every request needs, wrap `fetch`. See [Per-call options](https://docs.settlemint.com/docs/api-reference/sdk/calling-routes#per-call-options). |
| `idempotencyKey` | A per-call `Idempotency-Key` header. See [Move idempotency keys onto the call](#move-idempotency-keys-onto-the-call). |
| `requestValidation` and `responseValidation` | No option. See [Update error handling](#update-error-handling). |
| `fetch` | `fetch`. |
| `DalpClientConfig` type | `CreateDalpClientOptions`. |
| None | `runtime`: `"server"`, `"browser"`, or `"react-native"`. Optional. The SDK detects it when you leave it out. |

`runtime` tells the client where it runs. A `"server"` client authenticates with the API key. A browser client authenticates with the session cookie. Browser and React Native clients both refuse an API key. [Choose the credential](https://docs.settlemint.com/docs/api-reference/sdk/install#choose-the-credential) explains both.

`createDalpPlatformClient` is removed. It returned a route client, an auth client, and a cookie store from one call. Create the route client with `createDalpClient` and the auth client with `createDalpAuthClient` from `@settlemint/dalp-sdk/auth`. The auth client still takes `url`, not `baseUrl`. Its `sessionToken` option is removed, and its `cookieStore` option takes the `CookieStoreShape` that `makeCookieStore()` returns.

`DalpCookieStore` is replaced by `makeCookieStore()`, which returns a `CookieStoreShape`. In 3.1, `header` and `isEmpty` were getters. In 3.2 they are methods, so call `store.header()` and `store.isEmpty()`. `createCookieStoreFetch` still comes from the package root and accepts the new store.

## Flatten route calls [#flatten-route-calls]

```ts
// 3.1
const token = await dalp.token.read({ params: { tokenAddress: "0x1234" } });
const tokens = await dalp.token.list({ query: {} });
```

```ts
// 3.2
const token = await dalp.api.token.read({ assetId: "0x1234" });
const tokens = await dalp.api.token.list({
  page: { limit: 50, offset: 0 },
  sort: "-name",
});
```

In 3.2, routes sit under `dalp.api`. A 3.1 call wrapped its arguments in `params`, `query`, and `body` objects. A 3.2 call takes one flat object, and the client puts each field in the URL path, the query string, or the request body as the route declares. Argument names follow the 3.2 route, so some change: token read takes `assetId` where 3.1 took `tokenAddress`. Your editor shows each route's argument names.

A list call takes flat `page`, `sort`, and `filter` fields. Prefix a sort field with `-` for descending order. [Paging and filtering](https://docs.settlemint.com/docs/api-reference/sdk/calling-routes#paging-and-filtering) covers the filter syntax.

## Handle write results [#handle-write-results]

A write that submits a blockchain transaction still resolves in one of two shapes, as in 3.1. If the transaction settles within the settlement wait that `settlementTimeoutMs` sets, the call returns the route's result. If it does not, the call returns a handle: `{ transactionId, status, statusUrl }`. Check for `transactionId` to tell them apart.

The default wait is shorter. The 3.1 client sent `Prefer: wait=99` on every non-GET request. The 3.2 client sends `Prefer: wait=15`, only on queue-backed writes, and never when you set `Prefer` yourself. Migrated code therefore receives handles more often. Set `settlementTimeoutMs: 99_000` to keep the 3.1 timing.

```ts
import { createDalpClient, secret } from "@settlemint/dalp-sdk";
import { fiatCurrency } from "@settlemint/dalp-sdk/scalars";
import { waitForTransaction } from "@settlemint/dalp-sdk/wait-for-transaction";

const dalp = createDalpClient({
  baseUrl: "https://dalp.example.com",
  apiKey: secret(process.env.DALP_API_KEY!),
  runtime: "server",
  settlementTimeoutMs: 30_000,
});

const result = await dalp.api.token.create(
  {
    name: "Northwind Coffee",
    symbol: "NWC",
    type: "deposit",
    decimals: "2",
    countryCode: "528",
    priceCurrency: fiatCurrency("USD"),
    basePrice: "1.00",
  },
  { headers: { "Idempotency-Key": crypto.randomUUID() } }
);

if ("transactionId" in result) {
  await waitForTransaction(dalp, result.transactionId, { timeoutMs: 120_000 });
} else {
  console.log(result.links.self);
}
```

Do not submit the write again when you receive a handle. The platform already has the instruction. A second request can perform the action twice, even with the same idempotency key, when the route does other work before it queues the instruction. Follow the handle until it reports a final status.

The example sets two timeouts, both in milliseconds:

- `settlementTimeoutMs` is how long the platform holds each queue-backed write response before it returns a handle. The SDK sends it as a `Prefer` header, rounded up to whole seconds and clamped to 5 to 99 seconds. Without it, the SDK sends `Prefer: wait=15`. A value of zero or less sends `Prefer: respond-async`, which returns the handle at once. A single call can override it with `settlementTimeoutMs` in its second argument.
- `timeoutMs` is how long `waitForTransaction` polls the status route without progress before it gives up. While a write is parked on a gas payment or an indexer rebuild, the helper restarts that budget on every poll, so the total wait has no upper limit. Pass `signal: AbortSignal.timeout(ms)` for a hard limit, or set `holdWhilePaused: false` and `holdWhileRebuilding: false` to stop at the first pause.

`waitForTransaction` resolves with the transaction status once it reports `COMPLETED`. It throws a `DalpTransactionWaitError` when the wait runs out, is aborted, or ends in a failed status. On the settled branch, `result.data` is the created token and `result.links.self` is the URL that reads it again. Instead of the helper, you can read a handle's status with `dalp.api.transactionsV2.status({ transactionId })` or follow it with `dalp.api.transactionsV2.statusStream({ transactionId })`. [Mutations and settlement](https://docs.settlemint.com/docs/api-reference/sdk/mutations) covers both.

In 3.1, `waitForTransaction` came from the package root. In 3.2, import it from `@settlemint/dalp-sdk/wait-for-transaction`. Its options also changed:

- `intervalMs` is now `pollIntervalMs`, and the default poll interval is 1,000 ms instead of 2,000 ms.
- `resolveOnPendingApproval` is removed. The 3.1 helper returned at `PENDING_APPROVAL` by default. The 3.2 helper returns only at `COMPLETED`, so a transaction that waits for approval runs until the timeout and throws `DalpTransactionWaitError`.

A write the organisation holds for a second user's approval does not return a handle. The client throws a `DalpApiError` with code `sdk.transport.APPROVAL_HELD`. Do not repeat the request. The second approver approves it, and you read the approval at the `statusUrl` in the error's `data`.

## Move idempotency keys onto the call [#move-idempotency-keys-onto-the-call]

An idempotency key is a unique id for one instruction. The platform uses it to recognise a repeated request for the same instruction.

3.1 accepted a key in three places. The `idempotencyKey` client option sent the same key on every request from that client. A call could also pass `{ context: { idempotencyKey } }` as its second argument, or a `headers: { "Idempotency-Key": key }` field inside its input. In 3.2, all three become the `Idempotency-Key` header in the call's second argument. The per-call `context` and its `skipLoggingFor` field are removed. Generate one key per instruction, and reuse it only when you retry that same instruction on a route that documents replay safety. `token.create` does not, so read the operation's status before you send it again:

```ts
const idempotencyKey = crypto.randomUUID();
const options = { headers: { "Idempotency-Key": idempotencyKey } };

await dalp.api.token.create(input, options);
```

`context.idempotencyKey` also exists, but it sends one key on every request from the client, like the 3.1 option did. Use it only for a client that submits a single instruction. [Retry safety](https://docs.settlemint.com/docs/api-reference/sdk/mutations#retry-safety) explains when a retry with the same key is safe.

## Handle summary responses [#handle-summary-responses]

Some read routes accept a `view: "summary"` argument or a `fields` projection. Their 3.2 response type is a union of the full record and a summary record. The summary record has the same fields with every field optional, so the union has no tag to switch on. A field read on the union has type `T | undefined`, and 3.1 code that uses it as a defined value no longer compiles. Check that a field is defined before you use it. The platform sends the full record when you pass neither `view` nor `fields`, but the type stays the union, so the check is still required.

| Response type | Full record | Summary record |
| --- | --- | --- |
| `PlatformSystemReadRecord` | `PlatformSystemView` | `PlatformSystemSummaryView` |
| `AssumableParticipantRead` | `AssumableParticipant` | `AssumableParticipantSummary` |
| `UserListReadItem` | `UserListMember` | `UserListSummaryItem` |
| `HeldPriceRead` | `HeldPriceView` | `HeldPriceSummary` |
| `PriceSourceRead` | `PriceSourceView` | `PriceSourceSummary` |
| `ReportingSubmissionRead` | `ReportingSubmission` | `ReportingSubmissionSummary` |
| `TradingVenueOrderRead` | `TradingVenueOrder` | `TradingVenueOrderSummary` |
| `TokenListReadItem` | `TokenListDeploymentItem` or a deployed token item | `TokenListSummaryItem` |

A token list item also needs a second check. Its `ref` carries a `_tag` that says which identifier it holds. Only `ref._tag === "evm"` carries an `address`. A token that is still deploying has `ref._tag === "deployment"` and a `runKey`. Check the tag before you read the address:

```ts
const tokens = await dalp.api.token.list();

for (const item of tokens.data) {
  if (item.ref?._tag === "evm") {
    console.log(item.ref.address);
  }
}
```

## Update imports and webhook verification [#update-imports-and-webhook-verification]

The route client stays at the package root. The Effect client is at `@settlemint/dalp-sdk/effect`, which [The Effect surface](https://docs.settlemint.com/docs/api-reference/sdk/effect) describes.

| 3.1 import from the package root | 3.2 import |
| --- | --- |
| `waitForTransaction`, `DalpTransactionWaitError`, `WaitForTransactionOptions`, `WaitForTransactionStatus` | `@settlemint/dalp-sdk/wait-for-transaction`. |
| `createDalpAuthClient`, its types, and `WalletAuthEndpointPath` | `@settlemint/dalp-sdk/auth`. |
| `verifyWebhook`, `toStandardWebhookSecret`, and their types | `@settlemint/dalp-sdk/verify-webhook`. |
| `createDalpPlatformClient`, `DalpPlatformClient`, `DalpPlatformClientConfig` | `createDalpClient` from the root and `createDalpAuthClient` from `@settlemint/dalp-sdk/auth`. |
| `DalpCookieStore` | `makeCookieStore` from the root. |
| `createDalpBundlerClient` and the other bundler exports | No SDK client. Call the bundler routes under `/api/v2/bundler/{chainId}` described in [Smart wallet user operations](https://docs.settlemint.com/docs/api-reference/guides/user-operations), or send JSON-RPC to `POST <baseUrl>/bundler/{chainId}`. |
| `bigDecimalSerializer`, `bigIntSerializer`, `timestampSerializer`, `dalpSerializers` | No serialiser setting. From `@settlemint/dalp-sdk/scalars`, `decodeBigInt` and `decodeBigDecimal` turn a wire string into a value, and `encodeBigDecimal` turns a value back into a wire string. |
| `DALP_CONTRACT_ERROR_CODES`, `DalpContractErrorCode` | `DalpErrorCode` and `dalpErrorCatalog` at the root. |
| `RequestValidationPlugin`, `ResponseValidationPlugin`, `BatchLinkPlugin` from the removed `plugins` subpath of `@settlemint/dalp-sdk` | No replacement. Response checking is built in. Request validation and request batching are removed. |

In 3.2, `verifyWebhook` returns a promise, so `await` it. A 3.1 receiver that reads `.ok` on the result without `await` does not compile, and in plain JavaScript it rejects every event.

The function also takes an optional `replayStore` with two methods, `claim(eventId, claimId)` and `release(eventId, claimId)`. `claim` records the event id with the claim id in one atomic write and returns `true` only when no record for that event id exists. `release` removes the record only while it still holds that claim id. With a store, the function claims the event id once the signature verifies and refuses a delivery whose id is already claimed with `REPLAYED_EVENT`. A platform retry carries the same id, so call `release` on the verified result when your handler fails, or the retry is refused. Answer `REPLAYED_EVENT` with a `2xx` and `REPLAY_STORE_FAILURE` with a `5xx`, because a `401` ends delivery of the event. [Refuse a repeated delivery](https://docs.settlemint.com/docs/api-reference/guides/webhooks#refuse-a-repeated-delivery) explains the store. Without a store, a copy of a delivery resent within the timestamp tolerance, five minutes by default, verifies again. The function returns three new failure codes: `REPLAYED_EVENT`, `MALFORMED_BODY` and `REPLAY_STORE_FAILURE`.

A receiver without a store answers every failure with `401`:

```ts
import { verifyWebhook } from "@settlemint/dalp-sdk/verify-webhook";

const result = await verifyWebhook({ rawBody, headers, secret });
if (!result.ok) {
  return new Response(null, { status: 401 });
}
```

## Update error handling [#update-error-handling]

The 3.1 root exported four helpers that decode an error body: `isDapiPublicError`, `decodeDapiErrorResponseBody`, `getDapiPublicErrorFromResponseBody`, and `createDalpSdkErrorFromDapiPublicError`. It also exported two code lists, `CUSTOM_ERROR_CODES` and `DAPI_ERROR_IDS`, and the types `ErrorCode`, `DapiErrorId` and `DapiPublicError`. The 3.2 SDK removes all of them.

In 3.2, every non-2xx response throws a `DalpApiError`, and so does a 2xx response whose body the SDK cannot accept. Check for it with `isDalpApiError`, then read its `code`, `id`, `status`, and `data`:

```ts
import { isDalpApiError } from "@settlemint/dalp-sdk";

try {
  await dalp.api.token.read({ assetId: "0x1234" });
} catch (error) {
  if (isDalpApiError(error)) {
    console.error(error.code, error.id, error.status);
  }
}
```

Do not assume that a code you stored under 3.1 still exists. Check it with `isDalpErrorCode`. The [error reference](https://docs.settlemint.com/docs/api-reference/errors) lists the platform codes.

The SDK no longer checks request input, so invalid input reaches the platform and comes back as a `DalpApiError`. The SDK does check each JSON success response against the route. A body that does not match throws a `DalpApiError` with code `sdk.response.MALFORMED_SUCCESS`. A non-JSON body, or an empty body where the route declares one, throws code `sdk.transport.MALFORMED_RESPONSE`. Binary routes skip the check.

## Next [#next]

- [Install and configure the SDK](https://docs.settlemint.com/docs/api-reference/sdk/install)
- [Call routes, page collections, and read responses](https://docs.settlemint.com/docs/api-reference/sdk/calling-routes)
- [Handle mutations and settlement](https://docs.settlemint.com/docs/api-reference/sdk/mutations)
- [Use the Effect surface](https://docs.settlemint.com/docs/api-reference/sdk/effect)
