Skip to content

Node.js SDK

  • Node 20 or newer, Cloudflare Workers and browsers: the same code runs everywhere (HTTP via axios’ fetch adapter).
  • ESM and CommonJS, full TypeScript types derived from the API’s OpenAPI document.
  • One client, five services: numbers, emails, captchas, webhooks, account.
  • Typed errors per API error code.
  • Optional request inspector.
npm install @clearance-rest/sdk

Every method maps to one operation of the API reference and is named after its operationId (numbers.rent, emails.wait, webhooks.createEndpoint, …). The guides explain the behaviour behind the calls: Numbers, Emails, Captchas, Webhooks and Billing.

import { createClient } from "@clearance-rest/sdk";
const client = createClient({ apiKey: "cr_live_..." });
// Rent a temporary number and wait for the verification SMS.
const rental = await client.numbers.rent({ type: "temporary", country: "US", service: "tg" });
const sms = await client.numbers.waitForSms({ number: rental.item.phone_number, codeRegex: "(\\d{5,6})" });
console.log(sms.code);
// Pay-per-use calls return the charged `price` and the remaining `balance`.
console.log(rental.price, rental.balance);

Method parameters are camelCase (phoneNumber, maxPrice, codeRegex). Responses are returned as the API sends them, with snake_case field names (phone_number, next_cursor, metadata_version).

const client = createClient({
apiKey: "cr_live_...", // or CLEARANCE_REST_API_KEY
baseUrl: "https://api.clearance.rest", // or CLEARANCE_REST_BASE_URL
headers: { "X-Request-Source": "cron" }, // sent with every request
timeout: 120_000, // default wait time of waiting calls, ms
requestTimeout: 30_000, // client deadline of non-waiting calls, ms
graceMs: 15_000, // how long past the server's wait the client gives up, ms
inspector, // optional, see "Inspector"
labels: { env: "prod" }, // inspector labels for every request
});

Every option is optional. Explicit options win over the environment variables.

  • Authentication. The key is sent as X-API-Key. If headers contains an Authorization header, apiKey is not required. With neither, createClient throws ConfigurationError.
  • Headers. Every request carries X-Client: sdk-node; it cannot be overridden. Per-call headers are available on every method (last argument).
  • Waiting calls. numbers.waitForSms, emails.wait and captchas.resolve keep the request open until a result arrives. They send X-Service-Timeout (milliseconds) with the per-call timeout or, when omitted, the client’s timeout. The client gives up graceMs after the server would, with a RequestTimeoutError, so a call never hangs forever. All other calls use requestTimeout as their deadline.
  • Cancellation. Every method accepts { signal } as its last argument. An aborted call throws RequestAbortedError.
  • No automatic retries. A failed call throws; retrying is your decision.
const sms = await client.numbers.waitForSms({ number }, { timeout: 60_000, signal: controller.signal });

numbers, emails, captchas, webhooks and account are exported ready to use. They are created on first use from CLEARANCE_REST_API_KEY and CLEARANCE_REST_BASE_URL:

import { numbers } from "@clearance-rest/sdk";
await numbers.catalogCountries({ type: "temporary" });

createClient() without apiKey / baseUrl falls back to the same variables. In runtimes without environment variables (Cloudflare Workers) create the client per request with createClient({ apiKey: env.CLEARANCE_REST_API_KEY }).

// Catalog
await client.numbers.catalogCountries({ type: "temporary" }); // type: temporary | monthly
await client.numbers.catalogServices({ country: "US", q: "tele" }); // q: case-insensitive match on name or code
await client.numbers.catalogOperators({ country: "US" });
await client.numbers.catalogPrices({ type: "temporary", country: "US", service: "tg" }); // your price
await client.numbers.catalogAvailable({ country: "US", lineType: "local", areaCode: "415" });
// Rent
await client.numbers.rent({ type: "temporary", country: "US", service: "tg", maxPrice: 0.5, metadata: { order: "o1" } });
// For temporary numbers the catalogPrices price is the first price a rental tries, so passing it as maxPrice never fails
// with max_price_too_low. Without maxPrice the cap is about twice that price; the balance is held up to the cap and only
// the actual price is charged.
await client.numbers.rent({ type: "monthly", country: "US", phoneNumber: "+14155550100" });
// Inventory
const page = await client.numbers.listItems({ status: ["ACTIVE"], type: "temporary", metadata: { order: "o1" }, limit: 50 });
for await (const item of client.numbers.iterateItems({ status: "ACTIVE" })) {
// every page, one item at a time
}
const { item } = await client.numbers.getItem(id);
// Metadata is a JSON Merge Patch: set keys are merged, null removes a key.
await client.numbers.updateItemMetadata(id, { owner: "org_1", old: null }, { ifMatch: item.metadata_version });
// Release (temporary numbers released before any SMS are refunded) and resend
await client.numbers.releaseItem(id);
await client.numbers.resendItem(id);
// Wait for an SMS
const sms = await client.numbers.waitForSms({ number: "+14155550100", sender: "Service", timestamp: new Date(), matchRegex: "code", codeRegex: "(\\d{6})" });
// Connect a device number: a single-use QR token (30 s) for the Android app, or a ticket (60 s) for the live claim
// socket. Both return apk_url, the download address of the app.
const claim = await client.numbers.deviceClaim({ label: "office phone" });
console.log(claim.qr_payload, claim.apk_url);
const { mobile_claim_ticket } = await client.numbers.deviceClaimTicket({ label: "office phone" });
// Wait for an email; `mark_as_read` in the response tells whether marking was scheduled.
const email = await client.emails.wait({
emailAccount: "me@example.com",
emailSender: "noreply@example.com",
codeRegex: "(\\d{6})",
markAsRead: true,
});
console.log(email.code, email.subject);
// Attachments (keys come from email.attachments)
const file = await client.emails.getAttachment(email.attachments[0].key);
// file.data (Uint8Array), file.contentType, file.fileName
// Mailboxes
await client.emails.listAccounts({ status: "CONNECTED", emailProvider: ["gmail", "outlook"] });
for await (const account of client.emails.iterateAccounts()) {}
await client.emails.getAccount("me@example.com");
await client.emails.updateAccountMetadata("me@example.com", { team: "a" }, { ifMatch: 3 });
await client.emails.disconnectAccount("me@example.com");
// Connect a Gmail or Outlook mailbox: send the user's browser to authorize_url.
const start = await client.emails.oauthStart("gmail", { returnUrl: "https://app.example.com/connected", clientState: sessionId });
// ... the browser returns to returnUrl with ?result_token=...&client_state=...
const result = await client.emails.oauthResult("gmail", resultToken); // single use
// Cloudflare addresses
const { domains } = await client.emails.cloudflareDomains();
await client.emails.createCloudflareAccount(`inbox@${domains[0]}`);
// Authorization links: let anyone connect a mailbox without an account.
const link = await client.emails.createAuthLink({ expiresIn: 86_400, maxUses: 1, providers: ["gmail"], metadata: { customer: "c1" } });
console.log(link.url);
await client.emails.listAuthLinks({ status: "active" });
await client.emails.getAuthLink(link.id); // includes the mailboxes connected with it
await client.emails.revokeAuthLink(link.id);
const result = await client.captchas.resolve({
type: "turnstile",
website_url: "https://example.com",
website_key: "0x4AAA...",
});
switch (result.status) {
case "solved": // charged
console.log(result.solution); // e.g. { token: "...", user_agent: "..." }
break;
case "failed": // not charged
// e.g. "invalid_solution": the solver's answer did not match the type's contract
console.error(result.error.code, result.error.message);
break;
case "timeout": // no solution within the wait time; not charged
break;
}
// Solve hCaptcha in a real browser
await client.captchas.resolve({ type: "hcaptcha", website_url: "...", website_key: "...", mode: "browser" });
// Supported types, their fields, solution fields and the price of 1000 solutions
await client.captchas.types();

The response is typed by captcha type: after result.type === "image_to_text" && result.status === "solved", result.solution.text is a string; for turnstile it is result.solution.token.

The request body is type, the type-specific fields (named exactly as in the API: website_url, website_key, proxy, …) and the optional mode. It is a closed, typed union: unknown fields are rejected with ValidationError, and TypeScript rejects them at compile time. resolve returns the response, which carries its own status (solved, failed or timeout); only HTTP errors and error envelopes throw. mode: "browser" throws ProviderUnavailableError when no browser worker is connected.

Subscribe a URL of yours to events; deliveries are signed HTTP POSTs.

const types = await client.webhooks.eventTypes();
const created = await client.webhooks.createEndpoint({ url: "https://app.example.com/hooks", eventTypes: ["number.*", "email.received"] });
const secret = created.endpoint.secret; // returned only here and by rotateSecret
await client.webhooks.listEndpoints();
await client.webhooks.getEndpoint(id);
await client.webhooks.updateEndpoint(id, { eventTypes: ["*"], enabled: true }); // enabled: true re-enables an auto-disabled endpoint
await client.webhooks.rotateSecret(id); // the previous secret stops working at once
await client.webhooks.testEndpoint(id); // queues a webhook.test event; returns delivery_id
await client.webhooks.deleteEndpoint(id);
await client.webhooks.listDeliveries(id, { status: "failed" });
for await (const delivery of client.webhooks.iterateDeliveries(id)) {}
await client.webhooks.getDelivery(deliveryId); // includes the event payload
await client.webhooks.retryDelivery(deliveryId); // only failed deliveries

See Verifying signatures for the full explanation.

Each delivery carries X-Webhook-Timestamp and X-Webhook-Signature: v1=<hex>, where the signature is HMAC-SHA256(secret, "<timestamp>.<raw body>"). Verify against the raw request body, before parsing it:

import { constructWebhookEvent, WebhookSignatureError } from "@clearance-rest/sdk";
const raw = await request.text(); // the raw body, not parsed JSON
try {
const event = await constructWebhookEvent(raw, request.headers, secret); // 300 s tolerance by default
if (event.type === "number.renewal_failed") {
console.log(event.data.reason);
}
} catch (error) {
if (error instanceof WebhookSignatureError) return new Response(null, { status: 400 });
throw error;
}
return new Response(null, { status: 200 }); // any 2xx acknowledges the delivery

verifyWebhookSignature(raw, headers, secret, { toleranceSec }) returns a boolean instead. Both use Web Crypto, make no network request and are also available as client.webhooks.verifySignature / constructEvent. headers may be a Headers object or a plain object (Node’s req.headers); names are matched case-insensitively. Deliveries are at-least-once and unordered: deduplicate on event.id.

const balance = await client.account.balance();
// balance, held, currency, project: { spent_today, spent_this_month, daily_limit, monthly_limit }

Everything the SDK throws extends ClearanceRestError and carries code, status, details, body and requestId. Branch on the class with instanceof or on the API’s error code with error.code.

Class When
ApiError Base of all API errors (status, code, details, body). Thrown directly for unmapped codes.
ValidationError HTTP 400, validation_error and other invalid input (issues).
AuthenticationError HTTP 401, missing or invalid API key.
InsufficientBalanceError insufficient_balance (402).
SpendLimitExceededError spend_limit_exceeded (402).
PermissionError HTTP 403, for example team_suspended.
NotFoundError HTTP 404.
ConflictError HTTP 409, for example no_numbers_available, number_not_active.
PreconditionFailedError metadata_version_mismatch (412): ifMatch is stale; currentVersion.
RateLimitError HTTP 429.
ProviderUnavailableError provider_unavailable, supply_unavailable (503): nothing can serve the request right now; retry shortly.
ServerError Other HTTP 5xx, for example upstream_error.
WaitTimeoutError wait_timeout: nothing matched within the wait time.
NetworkError The request did not reach the API.
RequestTimeoutError Extends NetworkError; the client deadline passed (waitedMs).
RequestAbortedError Extends NetworkError; cancelled through signal.
WebhookSignatureError constructWebhookEvent rejected a delivery.
ConfigurationError No API key or Authorization header.

Calling a method with an empty identifier throws TypeError before any request is made.

The API answers a wait that found nothing with HTTP 200 and { success: false, error: { code: "wait_timeout" } }. The SDK treats every success: false envelope as an error, so numbers.waitForSms and emails.wait throw WaitTimeoutError and never return null. A timeout is not a failure: wait again.

import { InsufficientBalanceError, WaitTimeoutError, ApiError } from "@clearance-rest/sdk";
try {
const sms = await client.numbers.waitForSms({ number }, { timeout: 30_000 });
} catch (error) {
if (error instanceof WaitTimeoutError) {
// Extend the wait or ask again.
} else if (error instanceof InsufficientBalanceError) {
// Top up.
} else if (error instanceof ApiError) {
console.error(error.code, error.message);
} else {
throw error;
}
}

If a regex matches but extracts nothing, the result is code: null; that is not an error. captchas.resolve is the exception to the throwing model: it returns its status (solved, failed, timeout) in the response.

Pass an inspector to have every request’s lifecycle (request, response, body, error, settled) reported. Any object with onRequest, onResponse, onBody, onError and onSettled methods works; snake_case methods (on_request, …) are accepted too, so an @inspector-rest/sdk inspector can be passed as is:

import { create_inspector } from "@inspector-rest/sdk"; // optional peer dependency
const inspector = create_inspector({ server_key: "...", service: "my-app" });
const client = createClient({ apiKey: "...", inspector });

Events carry the labels service (numbers, emails, …) and op (<service>.<method in snake_case>, for example numbers.wait_for_sms, emails.create_auth_link), plus the labels client option. The SDK never imports the inspector package and works without it; nothing is reported unless you pass an inspector. An inspector that throws never breaks a request. Event order for a request: request, response, body, then settled (outcome: "ok"); failures report error (phase: network, parse or domain) before settled (outcome: "error").

adapter replaces the HTTP transport with an axios adapter function, which makes it easy to run without network access:

const client = createClient({
apiKey: "cr_live_test",
adapter: async (config) => ({
data: new TextEncoder().encode(JSON.stringify({ success: true, balance: 10 })).buffer,
status: 200,
statusText: "OK",
headers: { "content-type": "application/json" },
config,
}),
});

Alternatively point baseUrl at a local HTTP server.

Service Method Endpoint
numbers catalogCountries({ type }) GET /numbers/catalog/countries
catalogServices({ country?, q? }) GET /numbers/catalog/services
catalogOperators({ country }) GET /numbers/catalog/operators
catalogPrices({ type, country, service?, lineType? }) GET /numbers/catalog/prices
catalogAvailable({ country, lineType?, areaCode?, contains?, mms?, voice? }) GET /numbers/catalog/available
rent({ type, country, service?, operator?, phoneNumber?, lineType?, maxPrice?, metadata? }) POST /numbers/rent
listItems(query), iterateItems(query) GET /numbers/items
getItem(id) GET /numbers/items/{id}
updateItemMetadata(id, metadata, { ifMatch? }) PATCH /numbers/items/{id}
releaseItem(id) POST /numbers/items/{id}/release
resendItem(id) POST /numbers/items/{id}/resend
waitForSms({ number, sender?, timestamp?, matchRegex?, codeRegex? }, { timeout? }) GET /numbers/sms/wait
deviceClaim({ label? }) POST /numbers/device/claim/rest
deviceClaimTicket({ label? }) POST /numbers/device/claim/ticket
emails wait({ emailAccount?, emailSender?, timestamp?, matchRegex?, codeRegex?, markAsRead? }, { timeout? }) GET /emails/wait
getAttachment(key) GET /emails/attachments/{key}
listAccounts(query), iterateAccounts(query) GET /emails/accounts
getAccount(email) GET /emails/accounts/{email}
updateAccountMetadata(email, metadata, { ifMatch? }) PATCH /emails/accounts/{email}
disconnectAccount(email) POST /emails/accounts/{email}/disconnect
oauthStart(provider, { returnUrl, clientState? }) GET /emails/{provider}/oauth/start
oauthResult(provider, token) GET /emails/{provider}/oauth/result
cloudflareDomains() GET /emails/cloudflare/domains
createCloudflareAccount(email) POST /emails/cloudflare/accounts
createAuthLink(request) POST /emails/auth-links
listAuthLinks(query), iterateAuthLinks(query) GET /emails/auth-links
getAuthLink(id) GET /emails/auth-links/{id}
revokeAuthLink(id) DELETE /emails/auth-links/{id}
captchas resolve(request, { timeout? }) POST /captchas/resolve
types() GET /captchas/types
webhooks eventTypes() GET /webhooks/event-types
listEndpoints(), createEndpoint(request) GET, POST /webhooks/endpoints
getEndpoint(id), updateEndpoint(id, request), deleteEndpoint(id) GET, PATCH, DELETE /webhooks/endpoints/{id}
rotateSecret(id) POST /webhooks/endpoints/{id}/rotate-secret
testEndpoint(id) POST /webhooks/endpoints/{id}/test
listDeliveries(id, query), iterateDeliveries(id, query) GET /webhooks/endpoints/{id}/deliveries
getDelivery(id) GET /webhooks/deliveries/{id}
retryDelivery(id) POST /webhooks/deliveries/{id}/retry
verifySignature(...), constructEvent(...) local, no request
account balance() GET /account/balance

The API’s OpenAPI document names every operation after these methods (operationId numbers.rent, numbers.listItems, emails.wait, webhooks.createEndpoint, account.balance, …).

List methods return the response object (items, accounts, links, deliveries plus next_cursor); pass cursor to continue, or use the matching iterate* generator. Every method takes { signal, headers } as its last argument.