Node.js SDK
- Node 20 or newer, Cloudflare Workers and browsers: the same code runs everywhere (HTTP via axios’
fetchadapter). - 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/sdkEvery 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.
Quick start
Section titled “Quick start”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).
Client options
Section titled “Client options”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. Ifheaderscontains anAuthorizationheader,apiKeyis not required. With neither,createClientthrowsConfigurationError. - Headers. Every request carries
X-Client: sdk-node; it cannot be overridden. Per-callheadersare available on every method (last argument). - Waiting calls.
numbers.waitForSms,emails.waitandcaptchas.resolvekeep the request open until a result arrives. They sendX-Service-Timeout(milliseconds) with the per-calltimeoutor, when omitted, the client’stimeout. The client gives upgraceMsafter the server would, with aRequestTimeoutError, so a call never hangs forever. All other calls userequestTimeoutas their deadline. - Cancellation. Every method accepts
{ signal }as its last argument. An aborted call throwsRequestAbortedError. - No automatic retries. A failed call throws; retrying is your decision.
const sms = await client.numbers.waitForSms({ number }, { timeout: 60_000, signal: controller.signal });Default client from the environment
Section titled “Default client from the environment”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 }).
Numbers
Section titled “Numbers”// Catalogawait client.numbers.catalogCountries({ type: "temporary" }); // type: temporary | monthlyawait client.numbers.catalogServices({ country: "US", q: "tele" }); // q: case-insensitive match on name or codeawait client.numbers.catalogOperators({ country: "US" });await client.numbers.catalogPrices({ type: "temporary", country: "US", service: "tg" }); // your priceawait client.numbers.catalogAvailable({ country: "US", lineType: "local", areaCode: "415" });
// Rentawait 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" });
// Inventoryconst 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 resendawait client.numbers.releaseItem(id);await client.numbers.resendItem(id);
// Wait for an SMSconst 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" });Emails
Section titled “Emails”// 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
// Mailboxesawait 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 addressesconst { 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 itawait client.emails.revokeAuthLink(link.id);Captchas
Section titled “Captchas”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 browserawait client.captchas.resolve({ type: "hcaptcha", website_url: "...", website_key: "...", mode: "browser" });
// Supported types, their fields, solution fields and the price of 1000 solutionsawait 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.
Webhooks
Section titled “Webhooks”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 endpointawait client.webhooks.rotateSecret(id); // the previous secret stops working at onceawait client.webhooks.testEndpoint(id); // queues a webhook.test event; returns delivery_idawait 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 payloadawait client.webhooks.retryDelivery(deliveryId); // only failed deliveriesVerifying deliveries
Section titled “Verifying 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 deliveryverifyWebhookSignature(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.
Account
Section titled “Account”const balance = await client.account.balance();// balance, held, currency, project: { spent_today, spent_this_month, daily_limit, monthly_limit }Errors
Section titled “Errors”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.
Waiting calls and wait_timeout
Section titled “Waiting calls and wait_timeout”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.
Inspector (optional)
Section titled “Inspector (optional)”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").
Testing your code
Section titled “Testing your code”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.
Method reference
Section titled “Method reference”| 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.