npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@blotout/qinto-sdk-server

v0.1.9

Published

Qinto server-side SDK: a token-authenticated client for the /v1 identity graph API. Zero runtime dependencies.

Readme

@blotout/qinto-sdk-server

The Qinto server-side client. It sends events, identity, and consent to your site's Qinto edge API from a backend, with a scoped access token. Zero runtime dependencies.

Why we built it

The browser is the worst place to record the things that matter most. A refund happens in your admin tool, a subscription renews on a schedule, an order is confirmed by a payment webhook minutes after the customer closed the tab — and none of that reaches a page script. What does reach it arrives from a device you do not control, on a connection that may drop.

So Qinto accepts the same actions from your servers, against the same identity graph:

  • Address a person by the id you already have. An email, a phone number, an internal customer id — the identifier the browser linked earlier reaches the same person, and you never store a Qinto id to make that work.
  • Events your page cannot see. Offline conversions, webhook confirmations, back-office corrections, CRM syncs. Pass the time it really happened through the event context.
  • One integration for every destination. As in the browser, an action is sent once and the site's installed apps each receive it. Adding a destination is a change in the Qinto platform, not in your backend.
  • Scoped credentials. An edge token grants only the endpoints it names, so the checkout service that records purchases cannot read a visitor's stored profile.

Install

npm i @blotout/qinto-sdk-server

Needs Node.js 24 or newer. It calls only global fetch, AbortSignal.timeout, and crypto.randomUUID, so it also runs on Cloudflare Workers, Deno, and Bun.

Create a client

import { createClient } from '@blotout/qinto-sdk-server'

const qinto = createClient({
  destination: 'https://q.example.com',
  token: process.env.QINTO_EDGE_TOKEN
})

| Option | Default | What it does | | --- | --- | --- | | destination | — | Required. Your site's Qinto host — the same origin your page's tag talks to. | | token | — | Required. An edge token (qwat_…). | | requestTimeoutMs | 10000 | Per-request timeout; on expiry the call rejects with QintoTimeoutError. |

Get the token from the Qinto platform, under your site's Settings → Edge tokens. Tick only the permissions the service needs; the table in Send names the permission each method uses. The token is shown once, so store it as a secret.

createClient refuses to run in a browser and throws QintoEnvironmentError there — the token would be readable by every visitor. Use @blotout/qinto-sdk-browser in a page.

Send

await qinto.track('crm-user-8412', 'Purchase', {
  currency: 'USD',
  value: 20.5,
  orderId: '190315'
})

await qinto.identify('crm-user-8412', { email: '[email protected]', plan: 'pro' })
await qinto.alias('anon_7b8d1a52-6c1e-4b6f-9a30-2f8a54d1c3e9', 'crm-user-8412')
await qinto.consent('crm-user-8412', { categories: { marketing: false } })

| Method | Permission | What it does | | --- | --- | --- | | track(identifier, event, properties?, options?) | Track events | Records something the user did. Resolves to { ok, id, eventId }. | | identify(identifier, traits?, options?) | Identify users | Merges identity traits into the user's profile. | | alias(identifier, alias) | Link identifiers | Links an identifier of your own to that user. | | consent(identifier, input, options?) | Save consent | Records the user's consent choices. |

The first argument is the user, named by a canonical Qinto id (anon_…) or any identifier of your own — an email, a phone number, a custom id. The same identifier always reaches the same person. Internal gid_… ids are rejected.

  • Tracking never creates a user. An identifier this site has never seen fails with HTTP 404. Bind it first with identify or alias, or send the canonical anon_… id.
  • track returns the event id. A payload with a non-empty orderId uses that id, so the same purchase sent from the browser and from your backend deduplicates at the destination. Otherwise one is minted; pass your own through options.eventId.
  • Traits merge, and a trait sent as null clears the stored one. An email or phone trait is validated before it is stored, and it stays a trait: linking an identifier takes alias, or addressing the call by that identifier.
  • Consent choices merge, so you can send one toggle. necessary is always granted and accepts only true.

Event context

An event that did not happen just now, or that happened somewhere Qinto cannot observe, carries its own context:

await qinto.track(userId, 'Purchase', properties, {
  context: {
    occurredAt: completedAt.getTime(),
    offline: true,
    network: { ip: checkoutIp, userAgent: checkoutUserAgent }
  }
})

Apps that requested the event-context capability receive it. occurredAt is when it happened; when Qinto accepted it is recorded separately and cannot be overridden, and neither can the fact that the action came from a server.

Read

const { anonId, globalId } = await qinto.getSession('[email protected]')

| Method | Permission | What it returns | | --- | --- | --- | | getPerson(identifier, options?) | Read session ids | { globalId, anonIds, nextCursor } — the person an identifier belongs to and every session linked to them, 100 per page. Pass nextCursor back as options.cursor until it is null. With no person yet, anonIds holds the one session the identifier resolves to; it is empty for an identifier never seen. | | getSession(identifier) | Read session ids | { anonId, globalId } for an identifier of your own or an anon_… id. Both are null for an identifier never seen; globalId alone is null until something links the visitor to a person. | | getConsent(anonId) | Read consent | The consent state stored for that user. | | getProperties(anonId) | Read properties | Every property stored for that user. |

getConsent and getProperties take a canonical anon_… id — resolve one with getSession first. A read never creates anything.

Errors

Every error extends QintoError.

| Class | When | | --- | --- | | QintoApiError | The edge answered with a failure. Carries status and code. | | QintoTimeoutError | The request exceeded requestTimeoutMs. | | QintoEnvironmentError | createClient was called in a browser. |

code is one of invalid_body, invalid_params, unauthenticated, forbidden, internal, or unknown for anything else — including the 404 an unseen identifier produces, which is why that case is best recognized by status.

import { QintoApiError } from '@blotout/qinto-sdk-server'

try {
  await qinto.track(userId, 'Purchase', properties)
} catch (error) {
  if (error instanceof QintoApiError && error.status === 404) {
    await qinto.identify(userId, { email })
    await qinto.track(userId, 'Purchase', properties)
  }
}

What each failure means for a retry:

  • 401 unauthenticated — the token is missing, malformed, or no longer valid. Retrying it unchanged will not help.
  • 403 forbidden — the token is valid but lacks the permission for this call. The message names the permission to add.
  • 400 — the request itself is wrong; fix it rather than retry it.
  • 500 internal — nothing is guaranteed to have been written. Reads are safe to retry. A retried write may record parts of the action a second time, except a conversion, which deduplicates on its order id.

Types

QintoServerClient, QintoServerOptions, AcceptedResult, TrackResult, ActionOptions, ServerTrackOptions, ServerActionContextInput, ConsentCategory, ConsentInput, ConsentState, Properties, SessionInfo, TrackOptions, Traits, and QintoApiErrorCode are all exported.

Documentation

The standard events, the identity model, and the edge API reference are at docs.qinto.io. Every deployed site also serves its own OpenAPI document at https://<your-qinto-host>/openapi.json.