Skip to content
Cowliss is in beta: features and pricing may change.

Install and configure

One client, five calls: identify, track, batch, emails.send and preferences.link.

Terminal window
npm install @cowliss/sdk
import { Cow } from "@cowliss/sdk";
const cow = new Cow({
apiKey: process.env.COW_KEY!,
sourceId: process.env.COW_SOURCE_ID!,
baseUrl: process.env.COW_API_URL,
});
Option Default Meaning
apiKey required Your org’s API key, the Bearer credential on every request. One key serves every app.
sourceId none The source calls arrive through when they do not name one. Set it once per app.
baseUrl https://api.cowliss.com The origin your Cowliss API is served at, with no trailing slash.
maxRetries 2 Retries after the first attempt, on network errors and 5xx only.
timeoutMs 10000 How long one call may take before it is abandoned and retried.
batchTimeoutMs 30000 The same for one batch, which carries up to 500 rows and is slower by design.
onError none Called with the CowError of any failed call. This is where a call you do not await gets logged.
fetch global An injectable fetch, for tests and runtimes that do not have one.
Id Looks like You use it for
Source id src_01j2x8q7… Every call. It goes in the client config and on every HTTP body.
App id app_my-app Segment definitions, automation triggers, filtering the event feed.

You send the source; you filter on the app. A process writing into two apps names sourceId per call, or holds one client per app. See Apps and sources.

// You need the result: await it, and handle the error.
const profile = await cow.identify({ identifiers: { userId: "user_1" } });
// You do not: let it run, and keep serving the request.
cow.track({ identifiers: { userId: "user_1" }, event: "checkout_started" });

A call you do not wait for never rejects into your process. If Cowliss is unreachable it gives up on its own, within timeoutMs per attempt, and reports through onError:

const cow = new Cow({
apiKey: process.env.COW_KEY!,
sourceId: process.env.COW_SOURCE_ID!,
onError: (error) => logger.warn({ err: error }, "cowliss write dropped"),
});

Wait when the next thing you do depends on the write: a profile you are about to read back, or a segment you expect the event to move.

A 4xx surfaces as a typed CowError carrying the API’s error code, and is never retried. Network errors, timeouts and 5xx retry with exponential backoff.

error.code is the API’s code and error.status the HTTP status it came with. status is 0 when no answer arrived (network_error), or when the client refused the call before sending it, such as one with no sourceId.

import { CowError } from "@cowliss/sdk";
try {
await cow.track({ identifiers: { userId: "user_1" }, event: "checkout_started" });
} catch (error) {
if (error instanceof CowError && error.code === "over_quota") {
// out of credit: the write did not land
}
}

An unknown sourceId, an inactive source, a reserved event name and a future timestamp are all refused rather than quietly dropped. Every code and its status is in API conventions.

A retry never writes or sends twice: track and emails.send carry an idempotency key on every attempt, and identify matches on its identifiers. A retried send whose first attempt went through answers with that send’s deliveryId.

The package’s CHANGELOG.md lists every version after 0.16.0; the Changelog page is the CLI’s. Before that, 0.15.0 added the source_inactive and version_outdated error codes and 0.15.1 made baseUrl default to https://api.cowliss.com; 0.14.0 and 0.16.0 changed nothing in the SDK.

Requests and responses use the { data } envelope, and the endpoints are documented in the API reference.

Terminal window
curl -X POST $COW_API_URL/v1/identify \
-H "Authorization: Bearer $COW_KEY" \
-H "Content-Type: application/json" \
-d '{
"data": {
"sourceId": "'"$COW_SOURCE_ID"'",
"identifiers": { "userId": "user_1" },
"traits": { "email": "[email protected]", "plan": "pro" }
}
}'

A raw client sends its own messageId on a track.