All articles Integration

Why Linking linkedId After Signup Makes or Breaks Multi-Accounting

Most multi-account detection that “does not work” has the same root cause, and it is not the fingerprint. The device was identified correctly on every signup. The server checked accountsOnDevice every time. And every time the count was zero, because nobody ever told Prynt which account each signup became.

This post is about that missing step: attaching your user id, the linkedId, to an identification. It is one HTTP call, and it decides whether your signup limit is a control or a decoration.

The chicken-and-egg problem at signup

At signup, the browser identifies before the account exists. The order of operations is:

  1. The signup page calls identify() and gets a requestId.
  2. Your server fetches the event with GET /v1/events/{requestId} and checks the device’s history.
  3. You create the user, which is the first moment a user id exists.

By step 3 the identification is already recorded, with no account attached. So Prynt provides an update endpoint that attaches the account after the fact:

PUT https://api.pryntid.com/v1/events/{requestId}
Authorization: Bearer sk_live_…
Content-Type: application/json

{ "linkedId": "user_8f2c1a" }

The response confirms what was attached and returns the device’s refreshed history:

{
  "requestId": "…",
  "visitorId": "…",
  "linkedId": "user_8f2c1a",
  "accountsOnDevice": { "count": 1, "truncated": false, "accounts": [
    { "linkedId": "user_8f2c1a", "events": 1, "firstSeenAt": "…", "lastSeenAt": "…" }
  ] }
}

In the SDKs it is updateEvent(requestId, { linkedId }) in @prynt/node and update_event(request_id, linked_id=...) in the Python package. The Node verification guide shows the full signup handler.

What happens when you skip it

accountsOnDevice is built from events that carry a linkedId. Without the PUT, the device accumulates unlinked signup events, and the answer to “which accounts has this device created?” is always “none that I know of.”

The consequences cascade:

  • The signup limit never fires. accountsOnDevice.count >= 1 is never true, so the fifth trial from one laptop sails through.
  • The risk engine is blind to account counts. On paid plans, multiAccount (two or more distinct accounts on a device in 30 days) and accountSharing (more than three in 24 hours) count linked accounts. With nothing linked, neither can trigger, and the reason codes MULTI_ACCOUNT and ACCOUNT_SHARING never appear.
  • Rules have nothing to match. A console rule on distinctAccounts30d is evaluating zero.

The failure is silent. Nothing errors. Your dashboards show identifications flowing in, and the one number you care about stays flat. If you have shipped a device-based signup limit and never seen it block anything, check this first.

Signup is where the account is born, but it is not the only time the device matters. Link on every login as well, for three reasons:

Returning users on new devices. Someone who signed up on a laptop and later logs in from a desktop has two devices. Only the laptop knows about them unless the desktop login is linked. If they then try to create a second “fresh” account from the desktop, you want that device to already list their first account.

Account-level signals. Signals such as device spread look at one account across many devices: how many distinct devices logged into this account recently. They can only see devices where the account was linked.

Shared devices become legible. A family computer with three linked accounts that each log in regularly looks very different from a device that created three accounts in an afternoon and never used them again. The events, firstSeenAt and lastSeenAt fields on each entry in accountsOnDevice tell those apart.

At login the user is already known, so you can skip the PUT and pass the id directly to the browser call:

const { requestId } = await prynt.identify({
  tag: { action: 'login' },
  linkedId: session.userId,
});

Passing linkedId at identify time has one advantage over linking afterwards: the behavioral risk computed for that event already knows which account it belongs to, so account-level checks run on that very request. Linking with PUT afterwards updates the history for future checks only.

Backfilling existing users

If you add Prynt to a product that already has users, their devices have no history yet. There is no bulk import that creates device history from nothing, because a link needs a real identification to attach to. Backfill happens through traffic:

  1. Identify on login with linkedId. Every returning user gets linked the next time they sign in.
  2. Identify once per session for already-authenticated users. Long-lived sessions may not log in for weeks. One identify per session on an authenticated page, with linkedId, links them without waiting.
  3. Do not identify on every page view. Each identify counts toward your monthly quota. Once per session, plus the key moments (signup, login, payment, key creation), is plenty.

Expect the first few weeks after launch to under-count: a device that created accounts last year will not show them until those accounts come back. Many teams run new signup limits in flag-only mode during this window and switch to blocking once active users have been linked.

Getting the linkedId itself right

A few rules prevent painful clean-ups later:

  • Use a stable internal id. Your user primary key or a UUID. Not the email, which changes and puts personal data into the device graph.
  • Stay consistent. If one service links 42 and another links user_42, Prynt sees two accounts. Pick a format once.
  • Respect the limits. The linkedId must be a non-empty string of up to 190 characters; anything else returns 400 invalid_linked_id.
  • Use the right environment. A secret key only sees its own environment, so linking a live requestId with a test key returns 404 not_found.
  • Guard against replay. A requestId does not expire and is not single-use. Before linking at signup, check that the fetched event does not already carry a different linkedId. If it does, someone is reusing a captured id.

Consistent ids also make privacy requests simple: right-to-erasure can be executed by linkedId, which is only possible if each account has exactly one. See the privacy page for how that works.

A quick health check

To confirm linking works in your integration:

  1. Sign up a test account and confirm your server logs a successful PUT.
  2. Sign up a second account from the same browser.
  3. In the second signup’s GET /v1/events/{requestId} response, accountsOnDevice.count should be 1 and list the first account.

If it says zero, the PUT is not happening, is using the wrong key, or is linking a different requestId than the one the browser sent. Fix that before tuning any threshold. The broader multi-accounting guide covers what to do with the counts once they are real, and the docs list every field on the event.

Try it free

Prynt is device intelligence with a free tier — visitor IDs, bot & fraud Smart Signals, and behavioral biometrics, powered by a cross-site network. Start free.

Keep reading