Skip to main content

Migrate the TypeScript SDK from 3.1 to 3.2

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 describes the 3.2 client in full. Work through these steps in order:

  1. Install the 3.2 package.
  2. Update the client options.
  3. Flatten route calls.
  4. Handle write results.
  5. Move idempotency keys onto the call.
  6. Handle summary responses.
  7. Update imports and webhook verification.
  8. 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",
});
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.
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.
idempotencyKey
A per-call Idempotency-Key header. See Move idempotency keys onto the call.
requestValidation and responseValidation
No option. See 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 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

// 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: { 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.

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:

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

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:

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.

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:

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/dalp-sdk/effect, which The Effect surface 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, 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 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, 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:

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

Was this page helpful?