oqimDocs
API referenceConsole

Start here

  • Introduction
  • Getting started

Guides

  • Telegram accounts
  • Recipients
  • Campaigns
  • Scheduling
  • Webhooks
  • AI writing assistant

AI Seller

  • AI Seller
  • Channels
  • Live inbox
  • Past conversations and privacy
  • Business insights

API

  • Authentication
  • API reference
  • SDK
  • Errors
  • Rate limits

Trust and operations

  • Security
  • Compliance
  • Administration
Docs / SDK

Start here

  • Introduction
  • Getting started

Guides

  • Telegram accounts
  • Recipients
  • Campaigns
  • Scheduling
  • Webhooks
  • AI writing assistant

AI Seller

  • AI Seller
  • Channels
  • Live inbox
  • Past conversations and privacy
  • Business insights

API

  • Authentication
  • API reference
  • SDK
  • Errors
  • Rate limits

Trust and operations

  • Security
  • Compliance
  • Administration

SDK

@oqim/sdk is the TypeScript client for the Oqim API. It has no runtime dependencies and uses fetch, so it runs on Node.js 18 and later, Deno, Bun and in browsers. It isn't on npm yet: install it from the repository.

Install#

The package isn't published to npm yet. Build it from a clone of the Oqim repository, then install the file that makes:

Terminal
git clone https://github.com/national-development-community/oqim.git
cd oqim/packages/sdk
npm install
npm pack    # builds the package into oqim-sdk-0.1.0.tgz

# Then, in your project:
npm install /path/to/oqim/packages/sdk/oqim-sdk-0.1.0.tgz

It's an ES module with type declarations: import { Oqim } from "@oqim/sdk". The package's README, in packages/sdk, has more examples.

The client#

Create one client with your host and an API key, and reuse it. Each property groups the endpoints of one resource, with typed parameters and responses.

example.ts
import { Oqim } from "@oqim/sdk";

const oqim = new Oqim({
  baseUrl: "https://app.example.com", // your Oqim host; /api/v1 is added for you
  apiKey: process.env.OQIM_API_KEY,
});

const running = await oqim.campaigns.list({ status: "RUNNING" });
for (const c of running.data) {
  console.log(c.name, `${c.delivered_count}/${c.total_recipients}`);
}

await oqim.recipients.create({
  telegram_id: 123456789,
  first_name: "Aziza",
  consent_status: "OPTED_IN",
  consent_note: "Registered for Tashkent Expo 2026",
  tags: ["expo-2026"],
});
  • The key acts as the API_CLIENT role. Endpoints for people, such as connecting Telegram accounts or managing the team, answer 403 forbidden to it. Keep keys on servers.
  • In the browser, on the console's own origin, leave the key out: requests then use the console session.
  • Every method takes an optional last argument with signal, timeoutMs, maxRetries and headers for that request.

Access tokens

To avoid sending the long-lived key on every request, exchange it for a one-hour access token with auth.token(). Give the client a function, and it asks for the current token before each request:

TypeScript
const bootstrap = new Oqim({ baseUrl: "https://app.example.com", apiKey: process.env.OQIM_API_KEY });
let token = await bootstrap.auth.token(); // POST /auth/token: valid for one hour

const oqim = new Oqim({
  baseUrl: "https://app.example.com",
  // Called before every request: fetch a new token shortly before this one expires.
  accessToken: async () => {
    if (Date.parse(token.expires_at) - Date.now() < 60_000) token = await bootstrap.auth.token();
    return token.access_token;
  },
});

Resources

PropertyMethods
authtoken
meget
organizationget, update, acceptPolicy, usage, dashboard, analytics
memberslist, update, remove
invitationslist, create, revoke, lookup
accountslist, get, update, delete, pause, resume, check, activity, testMessage; connecting: connect, submitCode, submitPassword, reauth
proxieslist, create, get, update, delete, check, checks, import, bulk
recipientslist, create, get, update, delete, optOut, import, bulk, tags, imports
listslist, create, get, update, delete, addMembers, removeMembers
suppressionslist, create, delete
templateslist, create, update, delete
messagespreview
attachmentsupload, content
campaignslist, create, get, update, delete, preview, validate, start, pause, resume, cancel, duplicate, statistics, recipients
jobslist
eventslist, stream
webhookslist, create, get, update, delete, test, rotateSecret, deliveries, verifySignature
apiKeyslist, create, rotate, revoke
auditLogslist
notificationslist, markRead

For an endpoint the SDK doesn't wrap yet, call it directly with a path relative to /api/v1: oqim.request("GET", "/public/config"). You get the same credentials, retries and errors.

Errors#

Every failure is an OqimError, built from the API's error envelope:

PropertyWhat it holds
statusThe HTTP status, or 0 when no response came.
codeThe stable error code, such as validation_failed. Without a response it's network_error or timeout.
messageA sentence for people. It may change, so don't parse it.
fieldsWith validation_failed: each invalid field and its message.
requestIdThe X-Request-Id of the request. Quote it when you report a problem.
retryAfterOn 429: the seconds Retry-After asked for.
validationWith campaign_invalid: the pre-launch report.
bodyThe whole parsed response body.
TypeScript
import { OqimError } from "@oqim/sdk";

try {
  await oqim.campaigns.start("cmp_01j9r4c6e8g0j2m4p6r8t0w2y4");
} catch (e) {
  if (!(e instanceof OqimError)) throw e;
  if (e.code === "campaign_invalid") {
    for (const check of e.validation?.checks ?? []) {
      if (check.status === "FAIL") console.error(`${check.label}: ${check.message}`);
    }
  } else {
    console.error(e.status, e.code, e.message, e.fields, `request ${e.requestId}`);
  }
}

Pagination#

List methods return a page request. Await it for one page, or loop over it with for await to get every item: the SDK fetches pages of 100 as the loop goes.

TypeScript
// One page: { data, total, limit, offset }
const page = await oqim.recipients.list({ tag: "vip", limit: 100 });

// Every item, fetched page by page as the loop goes
for await (const recipient of oqim.recipients.list({ consent: "OPTED_IN" })) {
  console.log(recipient.id, recipient.username);
}

Lists are newest first and paged by offset, so an item created while you loop can push one you've already seen into the next page. The loop skips IDs it has already given you. .all() collects every item into an array, for small lists.

Retries and timeouts#

  • 429 rate_limited is retried for any method after the Retry-After the API sends: the rate limiter answers before the request does anything, so repeating it is safe. A wait longer than maxRetryAfterSeconds (default 60) is thrown instead. Other 429s, such as test_limit_reached, aren't retried. See Rate limits.
  • 5xx responses, network errors and timeouts are retried only for requests that are safe to repeat: GET, PUT and DELETE, plus POSTs that can't do anything twice, such as pausing, resuming, validating and previews.
  • Creating things (recipients, campaigns, keys) isn't retried after a 5xx or a timeout: it may have worked. Check before sending it again; recipients are unique by Telegram ID and username, so a repeat fails with 409 recipient_exists rather than creating a duplicate.
  • Waits grow from half a second to at most 8 seconds, with jitter so clients don't retry in step.
TypeScript
const oqim = new Oqim({
  baseUrl: "https://app.example.com",
  apiKey: process.env.OQIM_API_KEY,
  maxRetries: 4, // default 2
  timeoutMs: 10_000, // per attempt; default 30,000
});

// Per request: its own limits, and a signal that cancels it and its retries
await oqim.campaigns.get("cmp_01j9r4c6e8g0j2m4p6r8t0w2y4", { timeoutMs: 5_000, signal: AbortSignal.timeout(20_000) });

Live events#

events.stream() reads GET /events/stream as an async iterator, with the same events webhooks deliver:

TypeScript
const stop = new AbortController();

for await (const event of oqim.events.stream({ types: ["campaign.*", "account.restricted"], signal: stop.signal })) {
  console.log(event.type, event.data);
  if (event.type === "campaign.completed") stop.abort(); // ends the loop
}
  • It reconnects after network errors, API restarts and silence: the API pings every 20 seconds, and after 45 seconds without anything the SDK connects again.
  • After reconnecting it fetches what it missed from GET /events and skips events it has already given you, so a short outage loses nothing.
  • since replays earlier events first, then goes live. types takes event types, with campaign.* matching a prefix.
  • Aborting the signal, or break, ends the loop and closes the connection. A revoked key or a missing permission is thrown rather than retried.

Webhook verification#

verifyWebhookSignature checks a delivery's Oqim-Signature header exactly as the API makes it: HMAC-SHA256 over <t>.<raw body> with the endpoint's whsec_ secret, compared in constant time, and a t more than 5 minutes from your clock rejected. It needs no API key; it's also on the client as oqim.webhooks.verifySignature.

server.ts
import express from "express";
import { verifyWebhookSignature, type WebhookEvent } from "@oqim/sdk";

const app = express();

// express.raw keeps the exact bytes Oqim signed.
app.post("/oqim/webhooks", express.raw({ type: "application/json" }), async (req, res) => {
  const ok = await verifyWebhookSignature(req.body, req.get("Oqim-Signature"), process.env.OQIM_WEBHOOK_SECRET!);
  if (!ok) return res.status(400).send("invalid signature");

  const event = JSON.parse(req.body.toString("utf8")) as WebhookEvent;
  // Deliveries are at least once: skip event.id values you've already handled.
  res.sendStatus(204);
});

app.listen(3000);

Verify the bytes, not the JSON

Pass the body exactly as it arrived. Parsing the JSON and serializing it again changes whitespace and key order, and the signature won't match.

While you roll out a rotated secret, pass both: [newSecret, oldSecret]. See Webhooks for the scheme and examples in Go and Python.

The same with curl#

Shell
# List running campaigns
curl "https://app.example.com/api/v1/campaigns?status=RUNNING" \
  -H "Authorization: Bearer $OQIM_API_KEY"

# Get one campaign
curl https://app.example.com/api/v1/campaigns/cmp_01j9r4c6e8g0j2m4p6r8t0w2y4 \
  -H "Authorization: Bearer $OQIM_API_KEY"

# Pause it
curl -X POST https://app.example.com/api/v1/campaigns/cmp_01j9r4c6e8g0j2m4p6r8t0w2y4/pause \
  -H "Authorization: Bearer $OQIM_API_KEY"

# Add a recipient with their consent basis
curl https://app.example.com/api/v1/recipients \
  -H "Authorization: Bearer $OQIM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"telegram_id": 123456789, "first_name": "Aziza", "consent_status": "OPTED_IN",
       "consent_note": "Registered for Tashkent Expo 2026", "tags": ["expo-2026"]}'

# Record an opt-out made by email
curl -X POST https://app.example.com/api/v1/recipients/rcp_01j9r5b3d5f7h9k1m3p5r7t9v1/opt-out \
  -H "Authorization: Bearer $OQIM_API_KEY"

# Follow events as they happen (-N: don't buffer)
curl -N https://app.example.com/api/v1/events/stream \
  -H "Authorization: Bearer $OQIM_API_KEY"

Other languages#

The API reference is also an OpenAPI 3.1 spec. Point a client generator at it for another language, or import it into Postman or Insomnia.

PreviousAPI referenceNext Errors