@dataships/sdk
v2.0.0
Published
Dataships client SDK — transport and consent-session lifecycle for the Dataships consent-session API. Platform-agnostic core with adapters per integration (Shopify checkout, Salesforce, custom platform, …).
Readme
@dataships/sdk
Transport and session-lifecycle client for the Dataships consent-session API
(/v1/consent-session).
The SDK is platform-agnostic. It takes a ConsentSessionAdapter that knows how
to read order/customer context from a host environment (Shopify checkout,
Salesforce, a custom platform, …) and posts the consent-session envelope to the
Dataships backend.
Install
The package is published publicly to npm under the @dataships scope:
npm install @dataships/sdk
# or
yarn add @dataships/sdkQuickstart (Salesforce SFCC storefront)
import { DatashipsClient } from "@dataships/sdk";
import {
SalesforceAdapter,
getConfigFromScriptTag,
} from "@dataships/sdk/adapters/salesforce";
const config = getConfigFromScriptTag();
const client = new DatashipsClient({
// Public key + the JWT the cartridge minted at render; both come from the
// script tag. The SDK sends the token verbatim and never inspects it.
auth: { publicKey: config.publicKey, token: config.token! },
// Any env other than "production" also needs a baseUrl; see "Environments".
env: config.env,
app: "my-sfcc-cartridge",
version: "1.0.0",
adapter: new SalesforceAdapter({ config }),
});
const response = await client.upsertSession({ identifierType: "EMAIL" });
// Render one widget per entry; an empty array means render nothing.
for (const entry of response.consents) {
render(entry.channel, entry.consentType, entry.checked, entry.widgetConfig);
}
const session = { consentSessionId: response.consentSessionId };
const stop = client.startHeartbeat(session, {
onError: err => console.warn("heartbeat failed", err),
});
await client.saveConsent({
channel: "EMAIL",
consent: true,
session,
});The Salesforce adapter speaks the consent-session protocol (see below) and
reads its config from the cartridge-rendered data-* attributes on the
Dataships <script> tag. Rotate the token with client.setToken(next) before
it expires; a missed rotation surfaces as a 401 DatashipsError and stops the
heartbeat.
Environments
Only production has a built-in API host (https://api.dataships.io).
The package ships no internal hostnames, so env: "staging" or env: "local"
without a baseUrl throws at construction. Pass the host Dataships gives you
for a non-production environment:
new DatashipsClient({ env: "staging", baseUrl: STAGING_API_URL, ... });Script tag (CDN)
For pages without a bundler, dist/dataships.min.js is a minified IIFE that
assigns window.Dataships with the browser client, every adapter (not the
server client) and version. unpkg and jsdelivr serve it for a bare
package URL.
<script src="https://sdk.dataships.io/sdk/v2/dataships.min.js"></script>
<script>
const { DatashipsClient, CustomPlatformCheckoutAdapter } = window.Dataships;
const client = new DatashipsClient({
auth: { publicKey, token },
app: "my-storefront",
version: "1.0.0",
adapter: new CustomPlatformCheckoutAdapter({ collectionStep: "CHECKOUT_INFORMATION" }),
});
</script>Versions and CDN URLs
https://sdk.dataships.io/sdk/v2/follows the latest 2.x release and is the URL to use. A new major gets a new alias, so a page never moves majors on its own.https://sdk.dataships.io/sdk/2.0.0/is an exact release and never changes.https://sdk.dataships.io/sdk/next/follows the latest release candidate.
Every exact release publishes integrity.json beside its scripts, with the
SHA-384 of each one. To use subresource integrity, pin the exact version (an
alias moves with every release, so its hash would stop matching):
<script
src="https://sdk.dataships.io/sdk/2.0.0/dataships.min.js"
integrity="sha384-…"
crossorigin="anonymous"></script>Quickstart (custom platform / headless storefront)
import { DatashipsClient } from "@dataships/sdk";
import { CustomPlatformCheckoutAdapter } from "@dataships/sdk/adapters/custom-platform-checkout";
const adapter = new CustomPlatformCheckoutAdapter({
collectionStep: "CHECKOUT_INFORMATION", // or "THANK_YOU" on the confirmation page
locale: navigator.language, // coerced to a SupportedLocale
storageScope: publicKey, // keys the datashipsSessionId in localStorage
});
const client = new DatashipsClient({
// The merchant's server mints the short-lived ES256 JWT and injects it into the page.
auth: { publicKey, token },
env: "production",
app: "my-storefront",
version: "1.0.0",
adapter,
});
// Anonymous first, then again once the shopper types an email.
let response = await client.upsertSession({ identifierType: "EMAIL" });
response = await client.upsertSession({ identifierType: "EMAIL", identifier: typedEmail });
const session = { consentSessionId: response.consentSessionId };
const stop = client.startHeartbeat(session);
await client.saveConsent({ channel: "EMAIL", consent: true, session });The adapter posts integration: { type: "CUSTOM_PLATFORM_CHECKOUT", collectionStep,
datashipsSessionId, isFastCheckout }.
The signup adapter (CustomPlatformSignupAdapter, @dataships/sdk/adapters/custom-platform-signup)
posts integration: { type: "CUSTOM_PLATFORM_SIGNUP", datashipsSessionId } for a merchant-built
form: same beacon lifecycle under its own storage key, one id per form fill, resetDatashipsSessionId()
after the form is submitted; identifiers come from a user literal or resolver. The datashipsSessionId beacon is minted on the
checkout step, carried to the thank-you page through localStorage (scoped by
storageScope), and rotated when a new checkout starts; pass your own
datashipsSessionId to own that lifecycle instead. A thank-you upsert for a checkout
that already showed the widget answers 204, which the SDK resolves to an empty
consents.
Completing the flow — the placed order landing on every session opened under the
beacon and processing being queued — is client.submitSession(), backed by
POST /v1/consent-session/submit:
await client.submitSession({
user: { firstName: "Jane", lastName: "Doe" }, // both required; contact details never travel here
details: { id: "ORD-1001", total: 149.9, billingCountryCode: "IE", submittedAt: new Date() },
});The beacon defaults to the adapter's own and is discarded once the submit is
accepted; pass datashipsSessionId to complete a beacon you carried yourself.
The identifier is what the widget collected on the upsert; the submit carries
only the shopper's names, and the SDK refuses a blank one before sending.
The backend is idempotent and answers 200 even when the beacon matches no
session. details is the flow's payload merged under integration next to the
adapter's type: for CUSTOM_PLATFORM_CHECKOUT that is the order above
(total as a number in the integration's currency, billingCountryCode ISO
3166-1 alpha-2). The server client exposes the same call with
datashipsSessionId required, for a merchant backend completing from its
order-created hook.
Quickstart (Shopify checkout UI extension)
import { DatashipsClient } from "@dataships/sdk";
import { ShopifyCheckoutAdapter } from "@dataships/sdk/adapters/shopify-checkout";
const adapter = new ShopifyCheckoutAdapter({
collectionStep: "CHECKOUT_INFORMATION", // or "THANK_YOU"
datashipsOrderUuid, // the checkout's cart-attribute UUID, when it has one
locale: shopify.localization.language.value.isoCode,
checkoutToken: shopify.checkoutToken.value,
});
const client = new DatashipsClient({
// Shopify session tokens live about a minute: fetch one per request.
auth: { publicKey, getToken: () => shopify.sessionToken.get() },
env: "production",
app: "my-checkout-extension",
version: "1.0.0",
adapter,
});
const response = await client.upsertSession({ identifierType: "EMAIL", identifier: email });getToken is called before every request, retries and heartbeats included,
and is exclusive with token (setToken() throws under it). Update facts that
change on the page with adapter.setCheckoutDetails({ orderId, wallet, … })
and adapter.setCollectionStep(...).
In the checkout editor, run the same flow with no credentials and no network call:
const client = new DatashipsClient({
app: "my-checkout-extension",
version: "1.0.0",
adapter: new ShopifyCheckoutAdapter({
collectionStep: "CHECKOUT_INFORMATION",
previewMode: true,
}),
});Unsafe: the server key in the browser
A merchant with no server at all cannot mint tokens. As an explicit, accepted-risk escape hatch the browser client takes the server key under its own name:
const client = new DatashipsClient({
auth: { unsafeServerKey: "ds_sk_live_…" }, // UNSAFE: exposed to every visitor
env: "production",
app: "my-storefront",
version: "1.0.0",
adapter,
});The key is sent alone as X-Api-Key. Anyone who views the page source can open
sessions, save consent and submit orders as that merchant until the key is
revoked (ask Dataships support to revoke it); revocation is the only remedy. The SDK refuses a public key under this name, warns once in the
console at construction, appends unsafe-server-key to X-Dataships-App so the
backend can tell these calls from genuine server-to-server ones, and setToken()
throws. Prefer auth: { publicKey, token } wherever a token can be minted.
Server-to-server (merchant backend)
The server key never leaves the merchant's servers. A separate entry keeps it
out of storefront bundles: @dataships/sdk/server refuses to construct where
window exists, and the browser client refuses server keys.
import { DatashipsServerClient } from "@dataships/sdk/server";
const client = new DatashipsServerClient({
auth: { serverKey: process.env.DATASHIPS_SERVER_KEY! },
app: "my-backend",
version: "1.0.0",
adapter, // any consent-session adapter supplying the integration context
});
// The connection IP is your server's, so the user's IP is mandatory.
const response = await client.upsertSession({
identifierType: "EMAIL",
identifier: "[email protected]",
userIp: request.ip,
});The server client exposes upsertSession, saveConsent, heartbeat (one
beat; schedule your own cadence), submitSession (with an explicit
datashipsSessionId), triggerVerificationCode and submitVerificationCode,
with the same responses and DatashipsError as the browser client.
Adapters
| Adapter | Import | Use case |
| --- | --- | --- |
| Shopify checkout UI extensions | @dataships/sdk/adapters/shopify-checkout | Checkout UI extensions (SHOPIFY_CHECKOUT). No DOM: the extension passes the collection step, datashipsOrderUuid and checkout details from the Shopify checkout API. Authenticate with auth: { publicKey, getToken: () => shopify.sessionToken.get() } so every request carries a fresh session token. Used by the Dataships Shopify app. |
| Salesforce (SFCC) | @dataships/sdk/adapters/salesforce | Reads data-* attributes off the SFCC cartridge <script> tag. Used by the Dataships SFCC cartridge. |
| Custom platform checkout | @dataships/sdk/adapters/custom-platform-checkout | Bespoke / headless storefronts with no native integration (CUSTOM_PLATFORM). Every input comes from the integrator; the adapter owns the per-checkout datashipsSessionId. Used by @dataships/elements. |
| Custom platform signup | @dataships/sdk/adapters/custom-platform-signup | Merchant-built signup forms (newsletter, account creation) on platforms with no native integration. Posts CUSTOM_PLATFORM_SIGNUP with the per-form-fill datashipsSessionId; the merchant supplies the user (email, phone, name) as a literal or a resolver. |
Consent-session protocol
Every adapter drives /v1/consent-session. The client authenticates with
auth: { publicKey, token } or auth: { publicKey, getToken } (X-Api-Key +
Authorization: Bearer). upsertSession({ identifierType, identifier?, userIp?, format? })
posts the { identifier, identifierType, locale, userIp?, format?, integration: { type, … } }
envelope to POST /v1/consent-session and returns
{ consentSessionId, consents[], userVerificationRequired, lastHeartbeat, identifier }:
one entry per channel that renders (channel, consentType, checked, and
widgetConfig with the nullable copy fields), plus the identifier the session
holds so a widget can refill a field it lost. format picks how the copy comes
back: json (the API default) for plain strings you format yourself, html for
text and consentDescription with links and emphasis applied. In json copy,
{{MARKETING_PREFERENCES}} is the marketing modal link showing
widgetConfig.placeholderValues.MARKETING_PREFERENCES, which the API resolves
to the author's text or the served language's "Click here". Save-consent, heartbeat and the
OTP calls live under /v1/consent-session/*. Pass
{ consentSessionId: response.consentSessionId } as the session for
startHeartbeat / saveConsent. A 204 No Content on upsert resolves to an
empty consents as well.
In preview mode (previewMode: true on the adapter) the client makes no
network call and needs no auth. upsertSession returns a synthetic response
for the requested identifier type: fakeEmailUpsertResponse (one EMAIL
consent) for EMAIL, fakePhoneUpsertResponse (one SMS consent,
userVerificationRequired: false) for PHONE. The other calls
resolve without a request: triggerVerificationCode reports untriggered and
submitVerificationCode resolves verified.
Errors surface as DatashipsError { status, code, reason, challenge, body }.
401 / 403 are never retried; a 422 COUNTRY_NOT_FOUND on upsert resolves to
an empty consents instead of throwing. A 409 is retried when it is a held
checkout lock (PESSIMISTIC_LOCK_EXCEPTION) but not when the session has
already been processed (CONSENT_SESSION_ALREADY_PROCESSED, exposed as
error.isSessionProcessed), which also stops the heartbeat.
Contributing
See CONTRIBUTING.md in the repository for local development, prereleases
and the release process.
License
Licensed under the Apache License, Version 2.0.
