SDK

@simhook/sdk is a thin, typed client over fetch. It has no dependencies, so it runs on Node 20 and newer, Bun, Deno, Cloudflare Workers, and Vercel Edge. It is for servers: an API key in a browser is a key given away. Field names are the API’s own, in snake_case.

npm install @simhook/sdk
import { Simhook } from "@simhook/sdk";

const simhook = new Simhook({ apiKey: process.env.SIMHOOK_API_KEY });

apiKey falls back to the SIMHOOK_API_KEY environment variable, and baseUrl to SIMHOOK_BASE_URL, then to https://api.simhook.dev.

Sending

const { batch } = await simhook.messages.send({
  to: "+14155550123",
  body: "Your table is ready.",
});

// Acceptance is not delivery. Follow the send until the phone reports back.
const result = await simhook.batches.waitUntilDone(batch.id);
console.log(result.batch.status, result.messages[0]?.status); // completed delivered

to takes one number or an array. Add device_id, sim_subscription_id, or scheduled_at as in the API. waitUntilDone polls with backoff and stops at a timeout you can set.

Reading

const page = await simhook.messages.list({ direction: "inbound", limit: 50 });
const next = await simhook.messages.list({ direction: "inbound", cursor: page.next_cursor });

// Or let the SDK walk the pages
for await (const message of simhook.messages.iterate({ direction: "inbound" })) {
  console.log(message.sender, message.body);
}

Phones

const devices = await simhook.devices.list();
const online = devices.filter((d) => d.online);

await simhook.devices.update(online[0].id, { send_delay_seconds: 3 });
await simhook.devices.setDefault(online[0].id);

// Pair a new phone: show pair_url as a QR code, or code as text
const { code, pair_url, expires_at } = await simhook.devices.createPairingCode();

Webhooks

const { webhook, secret } = await simhook.webhooks.create({
  url: "https://example.com/hooks/simhook",
  events: ["message.received", "message.delivered", "message.failed"],
});

Verify deliveries with the raw request body:

import { constructWebhookEvent, SIGNATURE_HEADER, SimhookSignatureError } from "@simhook/sdk";

// Express, with a raw body
app.post("/hooks/simhook", express.raw({ type: "*/*" }), async (req, res) => {
  try {
    const event = await constructWebhookEvent({
      payload: req.body,
      signature: req.header(SIGNATURE_HEADER),
      secret: process.env.SIMHOOK_WEBHOOK_SECRET,
    });
    if (event.event === "message.received") console.log("text from", event.data.sender, ":", event.data.body);
    res.sendStatus(204);
  } catch (err) {
    if (err instanceof SimhookSignatureError) return res.sendStatus(401);
    throw err;
  }
});

verifyWebhookSignature returns a boolean instead of throwing, and signWebhookPayload makes a valid header for your own tests.

Errors

Every failure is a SimhookError with status (the HTTP status, or 0 when nothing came back), a stable code, and per-field errors for validation problems.

import { SimhookError } from "@simhook/sdk";

try {
  await simhook.messages.send({ to: "not a number", body: "hi" });
} catch (err) {
  if (err instanceof SimhookError) {
    err.code;            // "validation_failed"
    err.fieldMessages(); // { "body.to[0]": "not a phone number" }
    err.isPlanLimit;     // true for plan_limit_daily, plan_limit_monthly, ...
  }
}

Reads retry twice on 429, 5xx, and network failures. Writes never retry, so the SDK never duplicates a send. Pass { signal } to cancel a call, or { timeoutMs, maxRetries } to override the defaults for one call.

Options

Option Default Notes
apiKey SIMHOOK_API_KEY Required
baseUrl SIMHOOK_BASE_URL, then https://api.simhook.dev Your own host when self-hosting
fetch global fetch For proxies or tests
timeoutMs 30000 Per request
maxRetries 2 Reads only

Segments

import { countSegments } from "@simhook/sdk";

countSegments("Your code is 4921");
// { encoding: "GSM-7", length: 17, segments: 1, per_segment: 160, remaining: 143 }

An estimate following GSM 03.38; the phone performs the real split.