Server SDKs
The Dregs server SDKs are official clients for calling Dregs from your backend. They send events, read scores, and verify webhooks, using your credential's secret key. Every SDK exposes the same small set of operations, so an integration written in one language reads the same as one written in another.
These are server-side only. Browser tracking stays with the
dregs.js script, which uses your public key
(pk_) and is what fingerprints devices. The SDKs use your secret
key (sk_), which can read identities and scores and must never reach a browser.
Most integrations want both: the script in the page, an SDK on the server.
Installing
| Language | Install | Requires |
|---|---|---|
| Python | pip install dregs |
Python 3.10+ |
| Node | npm install @dregs/sdk |
Node 20+ |
| Java | implementation 'com.dregs:dregs-sdk:0.1.0' |
Java 17+ |
| Ruby | bundle add dregs |
Ruby 3.1+ |
| PHP | composer require dregs/dregs-sdk |
PHP 8.2+ |
On npm, the server SDK is @dregs/sdk. The unscoped dregs package is the
browser tracking script, which is a different thing and will not read scores.
Each SDK lives in its own repository under github.com/dregs-sdk, where the README carries the full method reference: Python, TypeScript, Java, Ruby, and PHP.
Authenticating
Every SDK reads the secret key from the DREGS_SECRET_KEY environment variable when you
don't pass one, so a client usually takes no arguments at all. Find the key under
Settings → Credentials; it starts with sk_.
Build one client at startup and reuse it for the life of the process rather than constructing one per request. Every client is safe to share across threads; some also hold a connection pool, which is wasted if you keep making new ones.
# Python
from dregs import Dregs
client = Dregs()
// TypeScript
import { Dregs } from '@dregs/sdk';
const client = new Dregs();
// Java
import com.dregs.sdk.Dregs;
Dregs client = Dregs.builder().build();
# Ruby
require "dregs"
client = Dregs::Client.new
// PHP
use Dregs\Client;
$client = new Client();
Sending an event
The identity is your own id for the user, the same one you pass to dregs.identify() in
the browser and the one you look scores up by. It is required: a server-side event carries no device
signature, so the identity is the only thing tying the event to a user.
Send user attributes as well as event data wherever you have them. The analyzers lean on attributes such as email, name, and username heavily. Name the keys the way your application already does, then map them to Dregs's canonical fields under Settings → Mappings in the dashboard; the same goes for event names.
# Python
client.track(
"user.signup",
identity="user_12345",
data={"plan": "pro", "referrer": "partner-x"},
identity_data={"email": "ada@example.com", "name": "Ada Lovelace"},
)
// TypeScript
await client.track('user.signup', {
identity: 'user_12345',
data: { plan: 'pro', referrer: 'partner-x' },
identityData: { email: 'ada@example.com', name: 'Ada Lovelace' },
});
// Java
client.track(TrackRequest.builder("user.signup", "user_12345")
.data(Map.of("plan", "pro", "referrer", "partner-x"))
.identityData(Map.of("email", "ada@example.com", "name", "Ada Lovelace"))
.build());
# Ruby
client.track(
"user.signup",
identity: "user_12345",
data: { plan: "pro", referrer: "partner-x" },
identity_data: { email: "ada@example.com", name: "Ada Lovelace" }
)
// PHP
$client->track(
'user.signup',
identity: 'user_12345',
data: ['plan' => 'pro', 'referrer' => 'partner-x'],
identityData: ['email' => 'ada@example.com', 'name' => 'Ada Lovelace'],
);
Checking the result
track returns a result that says whether Dregs recorded the event and carries the
event's id. The accessors follow each language's own conventions, so a snippet ported literally
from one language to another will not work: result.accepted is a property in Python
and TypeScript, a method in Java, a predicate in Ruby, and isAccepted() in PHP.
| Language | Recorded? | Event id |
|---|---|---|
| Python | result.accepted |
result.id |
| TypeScript | result.accepted |
result.id |
| Java | result.accepted() |
result.id() |
| Ruby | result.accepted? |
result.id |
| PHP | $result->isAccepted() |
$result->id |
The result is not recorded only in the uncommon case where Dregs accepts the request without recording an event, which a correctly configured secret key should not normally see; it is worth a log line. Failures that are yours to act on, such as a bad request, an unknown key, or a rate limit, raise an error instead. See Errors and retries.
A recorded event has not been scored yet. See Scoring is
asynchronous below before reading scores straight after a track.
Groups
If your application groups users into organizations, companies, teams, workspaces, or the like,
pass the groups the user is acting in. Dregs records each group and makes the identity a member of
it. Each group has a type, your own id, and optional data that is merged into the group, so later
events can send the type and id alone. The type defaults to organization, and an event
carries at most one group of each type. The parameter is groups, a list of entries with
type, id, and data, in Python, TypeScript, Ruby, and PHP; the
Java builder takes group(type, id, data) once per group. See
Groups.
# Python
client.track(
"user.login",
identity="user_12345",
groups=[
{"type": "organization", "id": "org_678", "data": {"name": "Acme Inc", "plan": "enterprise"}},
{"type": "team", "id": "team_42", "data": {"name": "Payments"}},
],
)
Idempotency
Every event is sent with an id, which makes ingestion idempotent: reposting the same id returns the
original event instead of recording a second one. Pass the id your application already has, such as
the row id of the record that triggered the event, and a retry after a timeout can never
double-count. The parameter is event_id in Python and Ruby, eventId in
TypeScript and PHP, and eventId() on the Java builder; the wire field it sets is
id. When you omit it the SDK generates one, which is what makes its own retries safe.
The id must be at most 64 characters and must not start with dregs-, which is reserved
for identifiers Dregs generates.
What to send from the server
Send the actions only the server sees: payments and failures, refunds, subscription changes, password resets and changes, role changes, invitations, API key usage, and background jobs the user triggered. Avoid double-counting what the browser already tracks. See Events for the wider picture.
Reading scores
This is the cheap read and the one most integrations want. It returns the four category scores Dregs has already computed, without triggering any work. A category Dregs has not scored yet reads as null, and a brand-new identity comes back empty.
# Python
scores = client.identities.scores("user_12345")
scores.humanity # 85
scores.authenticity # 72
scores.uniqueness # 91
scores.behavior # 68
// TypeScript
const scores = await client.identities.scores('user_12345');
scores.humanity; // 85
scores.authenticity; // 72
// Java
Scores scores = client.identities().scores("user_12345");
scores.humanity(); // 85
scores.authenticity(); // 72
# Ruby
scores = client.identities.scores("user_12345")
scores.humanity # 85
scores.authenticity # 72
// PHP
$scores = $client->identities->scores('user_12345');
$scores->humanity(); // 85
$scores->authenticity(); // 72
Scoring is asynchronous
track returns as soon as Dregs has recorded the event, before any analyzer has run.
Scores appear moments later, once an analysis cycle has finished, not in the same breath. Until
the first cycle finishes for a new identity:
-
identities.scoresreturns every category as null:Nonein Python,nullin TypeScript, Java, and PHP, andnilin Ruby. -
identities.analysisraises the not-found error (HTTP 404):NotFoundErrorin Python, TypeScript, and Ruby, andNotFoundExceptionin Java and PHP.
Neither means anything went wrong. Read scores at a decision point, such as when a user reaches
for a feature worth protecting, rather than on the line after track, and treat a null
category as not scored yet rather than as zero. identities.analyze queues a rescore and
is asynchronous in the same way: it returns once the job is queued, not when it has run. If you need
to act the moment scores change, subscribe to a webhook instead of
polling.
Seeing exactly why
The scores are the summary; the observations are the evidence. When you need to show or log why an identity scored the way it did, ask for the analysis instead. It returns the most recent analysis cycle with the per-analyzer observations behind each score, and raises a not-found error until the identity has been analyzed at least once (see Scoring is asynchronous).
# Python
analysis = client.identities.analysis("user_12345")
for observation in analysis.observations:
print(observation.label, observation.explanation, observation.value)
Each observation carries the analyzer that produced it, a value from 0.0 (suspicious) to 1.0 (legitimate), a confidence, a weight, and the counts behind the finding. See Scoring for how observations roll up into the four category scores.
Errors and retries
Each SDK raises a distinct error type per status, all descending from one base type so you can catch broadly when you do not need to distinguish. The two worth handling explicitly on an ingestion path are the monthly event limit (402) and the rate limit (429).
| Condition | Status |
|---|---|
| The event was malformed | 400 |
| The secret key was not recognized | 401 |
| The account is over its monthly event limit | 402 |
| The credential may not do this | 403 |
| No such identity, or it has not been analyzed | 404 |
| Too many requests | 429 |
Connection failures, timeouts, 429s, and 5xx responses are retried automatically with exponential
backoff and jitter, honoring the Retry-After header when Dregs sends one. The default
is two retries, and you can configure it. Because every event carries an idempotency id, those retries cannot
record an event twice.
Treat tracking as fire-and-forget or queue it. A Dregs outage should never fail your user's request.
Verifying webhooks
Each SDK ships a helper that verifies the X-Dregs-Signature header against the raw
request body in constant time, and rejects payloads older than five minutes as replays. Verify
against the bytes you received, not a re-serialized object: key order and whitespace
change when you re-encode, and the signature will not match.
# Python
from dregs.webhooks import verify
event = verify(
payload=request.body,
signature=request.headers["X-Dregs-Signature"],
secret=os.environ["DREGS_WEBHOOK_SECRET"],
)
The signing secret belongs to the webhook channel and is shown once, when you create it. It is not your API secret key. See Webhooks for the payload shape and headers.
What the SDKs cover
The v1 surface is deliberately small: send events, read an identity and its scores, look at the analysis behind them, queue a rescore, and verify webhooks.
track: record a backend event against an identityidentities.get: the identity, its current scores, badges, and attributesidentities.scores: the four category scoresidentities.analysis: the most recent analysis cycle, with observationsidentities.analyze: queue a rescore- webhook signature verification
Anything beyond that, such as listing identities, managing escalation rules, channels, or datasets, is available over the REST API, which the SDKs sit on top of. The machine-readable description is the public OpenAPI document. The API is extended additively, so an SDK built against it today keeps working as endpoints are added.
If you would rather have an AI agent do the integration, the Dregs MCP server gives coding agents the same picture of your account, and its setup skills know these SDKs.