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:1.0.0' |
Java 17+ |
| Ruby | bundle add dregs |
Ruby 3.1+ |
| PHP | composer require dregs/dregs-sdk |
PHP 8.2+ |
| Rust | cargo add dregs |
Rust 1.85+, async on Tokio |
| Elixir | {:dregs, "~> 1.0"} in mix.exs |
Elixir 1.15+ |
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. The Rust and Elixir references are on docs.rs and HexDocs.
The Rust and Elixir SDKs follow their languages' conventions more closely than the others. Rust is
async only and runs on Tokio. Elixir returns {:ok, value} or
{:error, exception} from every call, and each function has a ! twin that
returns the value or raises. The snippets below show both.
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();
// Rust
use dregs::Client;
let client = Client::from_env()?;
# Elixir
client = Dregs.Client.new()
A Rust client is cheap to clone, and every clone shares one connection pool, so keep one in your application state and hand clones to handlers and tasks. An Elixir client is a plain struct holding configuration; building one opens no connections.
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'],
);
// Rust
use dregs::Event;
use serde_json::json;
client
.track(
Event::new("user.signup", "user_12345")
.data(json!({ "plan": "pro", "referrer": "partner-x" }))
.identity_data(json!({ "email": "ada@example.com", "name": "Ada Lovelace" })),
)
.await?;
# Elixir
Dregs.track(client, "user.signup",
identity: "user_12345",
data: %{plan: "pro", referrer: "partner-x"},
identity_data: %{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 and Rust, a predicate in Ruby, isAccepted() in PHP,
and a function on Dregs.TrackResult in Elixir. In Elixir the result arrives inside
{:ok, result}.
| 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 |
| Rust | result.accepted() |
result.id |
| Elixir | Dregs.TrackResult.accepted?(result) |
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 (in Rust and Elixir, they come back as an error value). 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, PHP, and
Elixir; the Java builder takes group(type, id, data) once per group, and the Rust
Event takes .group(Group::new(type, id)) once per group, with optional
.data(...) on the 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, Ruby, and Elixir,
eventId in TypeScript and PHP, eventId() on the Java builder, and
.event_id() on the Rust Event; 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
// Rust
let scores = client.identities().scores("user_12345").await?;
scores.humanity(); // Some(85)
scores.authenticity(); // Some(72)
# Elixir
{:ok, scores} = Dregs.Identities.scores(client, "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 and Rust,nullin TypeScript, Java, and PHP, andnilin Ruby and Elixir. -
identities.analysisraises the not-found error (HTTP 404):NotFoundErrorin Python, TypeScript, and Ruby,NotFoundExceptionin Java and PHP,ApiErrorKind::NotFoundin Rust, and aDregs.APIErrorwith reason:not_foundin Elixir.
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
The Python, TypeScript, Java, Ruby, and PHP SDKs raise a distinct error type per status, all
descending from one base type so you can catch broadly when you do not need to distinguish. Rust
returns a single dregs::Error, and error.kind() gives an
ApiErrorKind such as QuotaExceeded or RateLimited. Elixir
returns {:error, %Dregs.APIError{reason: ...}} with a reason such as
:quota_exceeded or :rate_limited, and the ! variants raise
it. The two worth handling explicitly on an ingestion path are the monthly event limit (402) and
the rate limit (429).
// Rust
use dregs::ApiErrorKind;
match client.track(Event::new("user.signup", "user_12345")).await {
Ok(result) => { /* check result.accepted() */ }
Err(error) => match error.kind() {
Some(ApiErrorKind::QuotaExceeded) => { /* over the monthly event limit */ }
Some(ApiErrorKind::RateLimited) => { /* ingesting too fast */ }
_ => { /* anything else */ }
},
}
# Elixir
case Dregs.track(client, "user.signup", identity: "user_12345") do
{:ok, result} -> result
{:error, %Dregs.APIError{reason: :quota_exceeded}} -> :dropped
{:error, %Dregs.APIError{reason: :rate_limited, retry_after: seconds}} -> {:retry_in, seconds}
{:error, error} -> Logger.warning("Dregs call failed: #{Exception.message(error)}")
end
| 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.
Lists, groups, and the rest of the API
Beyond tracking and scores, the SDKs cover what a backend integration reaches for: walking your identities, keeping groups in step with your application, reading events, disregarding a device, and working escalations. The names below are Python's; each SDK's README has its own spelling.
Every list returns one page at a time, numbered from 0, with at most 100 items. A
list_all beside it walks every page for you. Filters you leave out are not sent.
# Python
for identity in client.identities.list_all(group="org_678", sort="humanityScore", order="asc"):
print(identity.id, identity.humanity_score)
client.groups.create("org_678", attributes={"name": "Acme Inc", "plan": "enterprise"})
client.groups.remove_member("team", "team_42", "user_12345") # the user left the team
client.identities.update("qa_bot_7", disregarded=True) # leave a test account out of scoring
for escalation in client.escalations.list_all(status="open", severity="critical"):
client.escalations.update(escalation.id, status="acknowledged")
groups.create merges attributes into the group when one with the same type and id already
exists, so it is safe to call on every sync. Membership changes made with add_member and
remove_member are for changes that happen outside an event; track already
records membership for the groups it is sent with.
What the SDKs cover
track: record a backend event against an identity, with its groups-
identities:get,scores,analysis(the latest cycle or a past one),analyze,list,update(disregard, or merge attributes),history,links,reviews, andreview -
groups:list,get,create,update,add_member, andremove_member events:listandgetdevices:list,get, andupdate(disregard)-
escalations:list,get,update(acknowledge or close), andsummary - webhook signature verification
Account configuration, such as escalation and badge rules, channels, mappings, and datasets, is left to the dashboard and 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.