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

@axis-social/sdk

v0.1.2

Published

Axis Social SDK — typed client with Bearer auth, automatic idempotency keys, retries, and a webhook signature verifier

Readme

@axis-social/sdk

Typed TypeScript client for the Axis Social API — one class that wraps every endpoint (connections, publishing, inbox, outbound webhooks, realtime) with Bearer auth, automatic idempotency keys, retry-on-429/5xx, and a webhook signature verifier.


1. Install

yarn add @axis-social/sdk
# or
npm install @axis-social/sdk

Requires Node 18+ (or any runtime with a global fetch and crypto.randomUUID).


2. The mental model

Everything in Axis Social hangs off three nouns. Knowing how they relate keeps you from guessing at IDs later.

App key (axs_…)  ──auth──►  Tenant  ──owns──►  Connection (a connected IG/FB/… account)
                                                    │
                                  ┌─────────────────┼──────────────────┐
                                  ▼                 ▼                  ▼
                              Post (publish)   InboxThread       InboxAccount
                                                (DMs / comments)  (per-connection settings)
  • App keyaxs_{env}_{keyId}.{secret}. Identifies which integration app is calling. Sent as a Bearer token. The SDK validates the shape locally and refuses to construct if it's malformed.
  • Tenant — the identity everything is scoped to, sent on every request as x-axis-tenant. It MUST be the Axis Accounts (SSO) user id, namespaced as ws_<axisAuthId> — derived from the logged-in session the same way in every Axis app. The SSO id is the only identifier shared across Axis apps, so using it as the tenant is what lets a connection made in one app (e.g. Engage) appear in another (Social) with no reconnect. Do NOT pass a local per-app workspace/DB id — those differ per app and fragment the same user across apps. The server auto-provisions the internal tenant on first use, so a new ws_<axisAuthId> value works immediately. One app key serves many tenants; you fix the tenant when you build the client.
  • Connection — one connected social account (an Instagram account, a Facebook Page, …), identified by a ConnectionDto.id. You get one by running the connect flow (§4). Almost everything downstream — posting, inbox threads, account settings — is keyed by connectionId.

Auth is Bearer-only. Despite the historical package description mentioning HMAC request signing, the client authenticates requests purely with the Bearer app key plus the x-axis-tenant header. HMAC only appears in one place: verifying inbound outbound-webhook deliveries (§7). Don't sign your own requests.


3. Construct the client

import { AxisSocial, AxisSocialError } from '@axis-social/sdk';

const axis = new AxisSocial({
  baseUrl: 'https://api.myaxis.ai', // no trailing slash needed — it's trimmed for you
  appKey: process.env.AXIS_APP_KEY!, // axs_{env}_{keyId}.{secret}
  tenant: `ws_${axisAuthId}`, // Axis Accounts (SSO) user id, namespaced ws_<axisAuthId> — same in every Axis app
  // optional:
  // fetch:      customFetch,            // defaults to global fetch (auto-bound)
  // maxRetries: 3,                      // 429/5xx retries before giving up (default 3)
});

// Smoke-test credentials:
await axis.ping(); // -> { ok: true }
await axis.whoami(); // -> identity the key/tenant resolve to

Every method returns a Promise of the unwrapped payload — the SDK strips the { data } envelope for you. List endpoints that paginate return { data, meta } instead (see §6).

How errors surface

Any non-2xx response throws an AxisSocialError (never a bare fetch rejection for HTTP errors):

try {
  await axis.posts.create({
    targets: [
      /* … */
    ],
  });
} catch (e) {
  if (e instanceof AxisSocialError) {
    e.status; // HTTP status, e.g. 409
    e.code; // machine code, e.g. "missing_capability"
    e.message; // human-readable
    e.details; // structured hints, e.g. { connectIntegrationKey: "meta" }
    e.body; // full parsed error envelope
  }
}

e.details.connectIntegrationKey is the actionable one: when a write fails because the connection lacks a capability, it tells you which integration to connect to gain it.


4. Connect a social account (the OAuth-style flow)

You can't post or read an inbox until a tenant has a connection. Connecting is a two-step redirect dance — start hands you a URL to send the user to, the provider redirects back, and complete finalizes it.

// Step 1 — begin. `network` is the platform: "instagram" | "facebook" | …
const startRes = await axis.connections.start('instagram', {
  redirectUrl: 'https://app.example.com/connections/callback', // where the provider returns the user
  // integration: "aggregator",  // default; pick a specific integration if you support more than one
});

// startRes.kind tells you how to proceed:
//   "redirect"        -> send the browser to startRes.redirectUrl
//   "qr"              -> render startRes.qr for the user to scan
//   "embeddedSignup"  -> launch the provider's embedded signup widget
window.location.href = startRes.redirectUrl!;

// Step 2 — after the provider redirects back to your redirectUrl, finalize:
const connection = await axis.connections.complete('instagram', {
  // accountId / network / provider params arrive on your callback query string — forward them here
});
connection.id; // <- the connectionId you'll use everywhere downstream
connection.connected; // true
connection.bindings; // the surfaces (feed, story, dm, comments…) this account can act on

Inspecting, switching, and the non-destructive lifecycle

await axis.connections.list(); // every connection for this tenant
await axis.connections.capabilities('instagram'); // what a network *could* do once connected

// Disconnect WITHOUT losing history. Tears down the upstream account (stops vendor billing,
// fully disconnects on Meta) but KEEPS the connection record and all saved DMs/comments.
await axis.connections.disconnect(connection.id);

// Reconnect re-authorizes the retained account and reuses its stored history.
// Returns a redirect to follow, exactly like start(). Pass redirectUrl for a one-call connect.
const re = await axis.connections.reconnect(connection.id, {
  redirectUrl: 'https://app.example.com/connections/callback',
});

// Move an existing connection to a different integration without re-running the full connect:
await axis.connections.switchIntegration(connection.id, {
  integration: 'meta',
});

Disconnect is non-destructive by design. There is no "delete connection" in this SDK. Disconnect → reconnect preserves the thread/entry history end-to-end.


5. Publish a post

A post fans out to one or more targets. Each target names a connectionId and a surface (e.g. "feed", "story") — the surfaces available come from the connection's bindings.

const post = await axis.posts.create({
  content: 'Launch day. 🚀',
  media: [{ url: 'https://cdn.example.com/hero.jpg', type: 'image' }],
  targets: [
    { connectionId: connection.id, surface: 'feed' },
    { connectionId: connection.id, surface: 'story' },
  ],
  // scheduledFor: "2026-07-01T09:00:00Z",  // omit to publish now
});

post.id;
post.status; // "scheduled" | "publishing" | "published" | "failed" | …
post.results; // per-surface outcome
post.externalIds; // provider-side ids once published, keyed by surface

// Lifecycle:
await axis.posts.get(post.id);
await axis.posts.update(post.id, { content: 'Edited copy' }); // before it publishes
await axis.posts.retry(post.id); // re-attempt a failed post
await axis.posts.delete(post.id);

// Poll several posts at once (e.g. a feed of cards):
await axis.posts.liveStatus([post.id, 'post_2', 'post_3']);

Writes are idempotent automatically — see §8. To dedupe a user double-clicking "Publish", pass your own key:

await axis.posts.create(input, { idempotencyKey: `publish:${draftId}` });

6. Read and work the inbox

The inbox unifies DMs and comments into threads. A thread holds entries (individual messages or comments). Comment threads nest one level of replies.

List threads (cursor-paginated)

This is the one family that returns the { data, meta } envelope, because you page through it:

let cursor: string | undefined;
do {
  const page = await axis.inbox.threads.list({
    type: 'dm', // "dm" | "comment"
    status: 'open',
    unreadOnly: true,
    connectionId: connection.id,
    limit: 50, // default 25, max 100
    cursor, // omit on the first call
  });

  for (const thread of page.data) {
    thread.unreadCount;
    thread.previewText;
    thread.lastInboundAt; // provider event time, not ingest time
  }

  cursor = page.meta.nextCursor; // undefined when you've reached the end
} while (cursor);

Times are provider times. Entries expose platformCreatedAt (when the user actually sent it) alongside createdAt (when Axis ingested it). Ordering and lastInboundAt use the provider time, so backfilled history shows real send times. Sort/display on platformCreatedAt ?? createdAt.

Read one thread and reply

const thread = await axis.inbox.threads.get(threadId); // includes participants + entries

const entry = await axis.inbox.threads.reply(threadId, {
  message: 'Thanks for reaching out!',
  // attachmentUrl / attachmentType  — to send media
  // parentEntryId                   — to reply under a specific comment
});

await axis.inbox.threads.setStatus(threadId, { status: 'closed' });

Moderate comments

action() is the general verb; the named helpers are thin wrappers over it. entryId is the comment entry's id.

await axis.inbox.threads.hide(threadId, entryId);
await axis.inbox.threads.unhide(threadId, entryId);
await axis.inbox.threads.like(threadId, entryId);
await axis.inbox.threads.unlike(threadId, entryId);
await axis.inbox.threads.deleteComment(threadId, entryId);

// Reply to a public comment privately, as a DM to its author:
await axis.inbox.threads.privateReply(
  threadId,
  entryId,
  'DMing you the details!',
);

// Equivalent low-level form:
await axis.inbox.threads.action(threadId, { action: 'hide', entryId });

Per-account inbox settings

await axis.inbox.accounts.list();
await axis.inbox.accounts.updateSettings(connection.id, {
  isEnabled: true,
  config: { aiMode: 'suggest', syncEnabled: true }, // stored verbatim — the service never acts on these
});

config is a free-form bag the service persists but never interprets. Use it to stash your own per-connection flags.


7. Receive events via outbound webhooks

Instead of polling, register an HTTPS endpoint and Axis will POST events to it (new DM, new comment, post status change, …).

Register an endpoint

const ep = await axis.webhooks.register(
  'https://app.example.com/hooks/axis',
  ['message.received', 'comment.received'], // omit the array to subscribe to ALL events
);
ep.secret; // ⚠️ returned EXACTLY ONCE — store it now; list() never returns it again

await axis.webhooks.list(); // endpoints (without secrets)
await axis.webhooks.delete(ep.id);

Verify and handle a delivery

Every delivery carries a signature header. Verify it before trusting the body. The SDK ships the exact verifier:

import { verifyWebhookSignature } from '@axis-social/sdk';

// Express-style handler. You MUST have the raw, unparsed request body.
app.post('/hooks/axis', express.raw({ type: '*/*' }), (req, res) => {
  const rawBody = req.body.toString('utf8');
  const timestamp = req.header('x-axis-timestamp')!;
  const signature = req.header('x-axis-signature')!; // "sha256=<hex>"

  const ok = verifyWebhookSignature({
    secret: process.env.AXIS_WEBHOOK_SECRET!, // the secret from register()
    rawBody,
    timestamp,
    signature,
  });
  if (!ok) return res.status(401).end();

  const event = JSON.parse(rawBody); // { type, …payload }
  switch (event.type) {
    case 'message.received':
      /* … */ break;
    case 'comment.received':
      /* … */ break;
  }
  res.status(200).end();
});

The signature is sha256= + HMAC-SHA256 of `${timestamp}.${rawBody}` using your endpoint secret. Other headers on each delivery: x-axis-event (the event type) and x-axis-delivery (a unique delivery id, useful for your own dedupe/logging).

Browser realtime (alternative to webhooks)

For live UI, mint a short-lived token instead of exposing the app key to the browser:

const { token, wsUrl, expiresIn } = await axis.realtime.token();
// open a WebSocket to wsUrl with `token` — the tenant is embedded in it.

8. Cross-cutting behavior (how the client treats every call)

  • Idempotency. Every write (POST/PATCH/DELETE) sends an Idempotency-Key. If you don't pass one, the SDK generates a UUID per logical write and keeps it stable across retries, so a retried request never double-applies. Pass your own ({ idempotencyKey }) to dedupe across separate calls — e.g. a user clicking a button twice.
  • Retries. 429 and 5xx responses retry up to maxRetries (default 3). 429 honors the Retry-After header; otherwise backoff is min(2^attempt, 8) seconds. The idempotency key is reused on every retry, so retries are safe.
  • Envelope unwrapping. request()-based methods return data directly; paginated list() methods return { data, meta }.
  • Abort. Pass { signal } (an AbortSignal) to any call to cancel it.
  • Headers, automatically: Authorization: Bearer <appKey>, x-axis-tenant: <tenant>, Idempotency-Key (writes), content-type: application/json (when there's a body).

9. Invariants & gotchas

The rules that bite if you get them wrong:

  1. Never construct or sign requests by hand. Use the AxisSocial instance. Auth is Bearer-only; the SDK adds every required header.
  2. tenant MUST be ws_<axisAuthId> — the Axis Accounts (SSO) user id, never a local per-app workspace/DB id. Using anything else fragments the user's data across Axis apps.
  3. connectionId is the spine. To post, read inbox, or change settings, you first need a connection from connections.list() or the connect flow. Don't fabricate one.
  4. Don't delete connections. Use disconnectreconnect; history is preserved deliberately.
  5. Read provider time, not ingest time when ordering or displaying messages: platformCreatedAt ?? createdAt.
  6. The webhook secret is shown once. When you write register() code, also write the code that persists ep.secret.
  7. Catch AxisSocialError and branch on .code / .details.connectIntegrationKey, not on string-matching .message.
  8. Pagination is cursor-based. Loop on meta.nextCursor until it's undefined; never assume offset/limit.

10. Reference

Client surface

| Group | Method | HTTP | Returns | | ---------------- | ------------------------------------------------------------------------ | -------------------------------------------------- | -------------------------------------- | | connections | list() | GET /v1/connections | ConnectionDto[] | | | capabilities(network?) | GET /v1/integrations/capabilities | CapabilitiesResponse | | | start(network, body?, opts?) | POST /v1/connections/{network}/start | StartConnectResponse | | | complete(network, body?, opts?) | POST /v1/connections/{network}/complete | ConnectionDto | | | reconnect(id, body?, opts?) | POST /v1/connections/{id}/reconnect | StartConnectResponse | | | disconnect(id, opts?) | POST /v1/connections/{id}/disconnect | ConnectionDto | | | switchIntegration(id, body, opts?) | POST /v1/connections/{id}/switch-integration | ConnectionDto | | posts | create(input, opts?) | POST /v1/posts | PostDto | | | list(query?) | GET /v1/posts | PagedResponse<PostDto> | | | get(id) | GET /v1/posts/{id} | PostDto | | | update(id, patch, opts?) | PATCH /v1/posts/{id} | PostDto | | | retry(id, opts?) | POST /v1/posts/{id}/retry | PostDto | | | delete(id, opts?) | DELETE /v1/posts/{id} | — | | | liveStatus(ids) | GET /v1/posts/live-status | LiveStatusResponse | | inbox.threads | list(query?) | GET /v1/inbox/threads | PagedResponse<InboxThreadDto> | | | get(id) | GET /v1/inbox/threads/{id} | InboxThreadDto | | | reply(id, body, opts?) | POST /v1/inbox/threads/{id}/reply | InboxEntryDto | | | action(id, body, opts?) | POST /v1/inbox/threads/{id}/actions | — | | | setStatus(id, body, opts?) | POST /v1/inbox/threads/{id}/status | — | | | hide / unhide / like / unlike / deleteComment / privateReply | POST …/actions | — | | inbox.accounts | list() | GET /v1/inbox/accounts | InboxAccountDto[] | | | updateSettings(connectionId, settings, opts?) | PATCH /v1/inbox/accounts/{connectionId}/settings | InboxAccountDto | | webhooks | register(url, events?, opts?) | POST /v1/webhooks | { id, url, events, enabled, secret } | | | list() | GET /v1/webhooks | endpoints (no secret) | | | delete(id, opts?) | DELETE /v1/webhooks/{id} | — | | realtime | token() | POST /v1/realtime/tokens | { token, expiresIn, wsUrl } | | admin | releaseNativeClaim(network, externalAccountId, opts?) | POST /v1/admin/native-claims/release | { released, previousOwnerTenantId? } | | | webhookInbox.list(status?) | GET /v1/admin/webhook-inbox | inbound deliveries | | | webhookInbox.requeue(id, opts?) | POST /v1/admin/webhook-inbox/{id}/requeue | — | | — | ping() | GET /v1/ping | { ok: true } | | — | whoami() | GET /v1/whoami | identity |

admin.* requires the admin scope on the app key.

Webhook event types

message.received · comment.received · thread.reply · post.status · account.updated (plus any future string types — handle the default case).

Headers the SDK sends / expects

| Header | Direction | Purpose | | -------------------------------- | ---------------- | -------------------------------------------- | | Authorization: Bearer <appKey> | request | Auth | | x-axis-tenant | request | Tenant scope | | Idempotency-Key | request (writes) | Safe retries / dedupe | | Retry-After | response | Backoff hint on 429 | | x-axis-signature | inbound webhook | sha256=<hmac> over {timestamp}.{rawBody} | | x-axis-timestamp | inbound webhook | Signed timestamp | | x-axis-event | inbound webhook | Event type | | x-axis-delivery | inbound webhook | Unique delivery id |

Exports

import {
  AxisSocial, // the client
  AxisSocialError, // thrown on non-2xx
  verifyWebhookSignature, // inbound-webhook verifier
} from '@axis-social/sdk';
import type {
  AxisSocialOptions,
  RequestOptions,
  AxisErrorBody,
  AxisErrorCode,
  Scope,
  QueryParams,
} from '@axis-social/sdk';

All request/response DTOs (ConnectionDto, PostDto, InboxThreadDto, …) are re-exported from @axis-social/types.