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 describes the 3.2 client in full. Work through these steps in order:
- Install the 3.2 package.
- Update the client options.
- Flatten route calls.
- Handle write results.
- Move idempotency keys onto the call.
- Handle summary responses.
- Update imports and webhook verification.
- Update error handling.
Install the 3.2 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 has the install command and explains why the versions must match, and Upgrade an integration to DALP 3.2 covers the platform changes.
Update the client options
// 3.1
const dalp = createDalpClient({
url: "https://dalp.example.com",
apiKey: process.env.DALP_API_KEY!,
organizationId: "org_01J00000000000000000000000",
});// 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",
});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 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/. 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
// 3.1
const token = await dalp.token.read({ params: { tokenAddress: "0x1234" } });
const tokens = await dalp.token.list({ query: {} });// 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 covers the filter syntax.
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: { transaction. 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 settlement to keep the 3.1 timing.
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);
}The example sets two timeouts, both in milliseconds:
settlementTimeoutMsis how long the platform holds each queue-backed write response before it returns a handle. The SDK sends it as aPreferheader, rounded up to whole seconds and clamped to 5 to 99 seconds. Without it, the SDK sendsPrefer: wait=15. A value of zero or less sendsPrefer: respond-, which returns the handle at once. A single call can override it withasync settlementTimeoutMsin its second argument.timeoutMsis how longwaitForTransactionpolls 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. Passsignal: Abortfor a hard limit, or setSignal. timeout(ms) holdandWhile Paused: false holdto stop at the first pause.While Rebuilding: false
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. or follow it with dalp.. Mutations and settlement covers both.
In 3.1, waitForTransaction came from the package root. In 3.2, import it from @settlemint/. Its options also changed:
intervalMsis nowpollIntervalMs, and the default poll interval is 1,000 ms instead of 2,000 ms.resolveOnPendingApprovalis removed. The 3.1 helper returned atPENDING_APPROVALby default. The 3.2 helper returns only atCOMPLETED, so a transaction that waits for approval runs until the timeout and throwsDalpTransactionWaitError.
A write the organisation holds for a second user's approval does not return a handle. The client throws a DalpApiError with code sdk.. 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
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: { idempotency as its second argument, or a headers: { "Idempotency- 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:
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 explains when a retry with the same key is safe.
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.
A token list item also needs a second check. Its ref carries a _tag that says which identifier it holds. Only ref._ carries an address. A token that is still deploying has ref._ and a runKey. Check the tag before you read the address:
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
The route client stays at the package root. The Effect client is at @settlemint/, which The Effect surface describes.
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(event and release(event. 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 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:
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
The 3.1 root exported four helpers that decode an error body: isDapiPublicError, decode, get, and create. 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:
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 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.. A non-JSON body, or an empty body where the route declares one, throws code sdk.. Binary routes skip the check.