Skip to content

JavaScript and TypeScript

Wait for real verification messages with the Phonekit SDK

The SDK gives your server, test runner, or agent a small typed interface to selected Phonekit numbers. Mark the inbox before requesting an SMS, then wait for the first later message or verification code.

Install the SDK

@phonekit/sdk requires Node.js 20.3 or newer and runs on the server or in a Node test runner. API access is included on every plan. Select the task’s permissions and separate readable/managed numbers in workspace Settings → Automation. Existing credentials stay read-only.

Install it from npm in your test or server project. The package contains runtime JavaScript, public TypeScript declarations and an MIT license.

Install the SDK in your projectShell
npm install @phonekit/sdk

Commit your project’s lockfile so CI installs the same version.

This package uses ESM imports. CommonJS projects can load it with dynamic import("@phonekit/sdk"); a require() export is not provided.

Set PHONEKIT_API_KEY and PHONEKIT_TEST_NUMBER in your server environment or CI secret store. The number can be an E.164 phone number or a Phonekit number ID. Keep the key in the Node process; a browser page or mobile app should call your own application, not receive this credential.

Capture the position before sending

Create a client and number handleTypeScript
import { Phonekit } from "@phonekit/sdk"
 
const apiKey = process.env["PHONEKIT_API_KEY"]
const testPhoneNumber = process.env["PHONEKIT_TEST_NUMBER"]
if (!apiKey || !testPhoneNumber) {
  throw new Error("Set PHONEKIT_API_KEY and PHONEKIT_TEST_NUMBER")
}
 
const phonekit = new Phonekit({ apiKey })
const number = phonekit.number(testPhoneNumber)
const after = await number.mark()

Now trigger the application action that sends the SMS. mark() records the current inbox position; it does not send a message or clear existing messages. The explicit after cursor prevents an earlier receipt from satisfying this task.

Choose the result your workflow needs:

Use the detected verification codeTypeScript
const code = await number.waitForOtp({ after, timeoutMs: 120_000 })
// Submit code to your application's verification form or endpoint.
// Keep it in memory; do not print it in test output.
Inspect a message or invitation linkTypeScript
const receipt = await number.waitForMessage({ after, timeoutMs: 120_000 })
// Inspect receipt.message.sender, otp, links, or body deliberately.
// For a later wait, use receipt.cursor after requesting the next message.

What succeeds

The first message after your mark reaches the test runner. Your application still decides whether its code or link is valid; the test should assert the signed-in state or other final outcome. The Playwright guide connects these calls to a complete browser test.

Methods and returned values

MethodReturnsBehavior
new Phonekit({ apiKey, baseUrl?, signal? })ClientAuthenticates reads with a selected-number key. The default base is https://www.phonekit.io.
phonekit.number(reference)Number handleAddresses an existing selected number by ID or E.164. It does not provision or reserve one.
number.mark({ signal? })Promise<PhonekitCursor>Captures an opaque server-issued position before the send action.
number.waitForMessage({ after, timeoutMs, signal? })Promise<MessageReceipt>Returns { message, cursor } for the first later message.
number.waitForOtp({ after, timeoutMs, signal? })Promise<string>Returns that first message's detected OTP; throws missing_otp when it has none.

Scroll horizontally to see all columns.

Optional fields are marked with ? in the table. Omit them in a call when you do not need them. PhonekitCursor is an opaque string type: preserve the value intact. Number handles do not store a mutable current cursor.

MessageReceipt.message has id, numberId, sender, body, otp, links, and receivedAt. otp is a string or null; links is an array of detected links. Extraction is best effort. SMS contents and links come from external senders, so evaluate them as input rather than agent instructions.

For exact message-by-ID reads, pagination, or key inspection, the package also exports the shared PhonekitClient transport:

Read one retained message by its IDTypeScript
import { PhonekitClient } from "@phonekit/sdk"
 
const apiKey = process.env["PHONEKIT_API_KEY"]
if (!apiKey) throw new Error("Set PHONEKIT_API_KEY")
const client = new PhonekitClient(apiKey)
const message = await client.message("8ALpC7nK2dQ4sE6rT9vX1Z")
// Use message in memory. The key must cover its number.

The transport also provides describe(), messages(numberId, { limit?, before? }), latest(numberId), cursor(numberId), and waitForMessage(numberId, after, timeoutMs, signal?, pollIntervalMs?). Its before pagination option maps to the HTTP cursor parameter. The lower-level wait polls immediate reads locally and returns null at the caller's deadline; the Phonekit facade adds matching and throws typed timeout/progress errors.

Authorized line and subscription workflows

The SDK is a typed wrapper around the same HTTP API. allowance(), numberTypes() and inventory() help choose a supported line before acquisition. createNumber() requires numbers:manage, a saved UUID requestId, explicit human sharing and a grantReadAccess choice. Retry an acquisition with the same UUID and identical input. operation() reports server-owned provisioning status; waitUntilReady() follows it by polling locally. Pending acquisitions reserve capacity atomically.

Number handles also provide get(), configure(), release() and recheck(). Provider-confirmed release returns capacity and preserves receipt-based retention. Management permission does not grant content reads. Webhook CRUD uses selected management-scoped numbers and webhooks:manage; deliveries() and delivery() expose sanitized attempts under diagnostics:read. Signing secrets are returned only on creation.

For a single immediate read, use number.messagesAfter(after, {limit: 25}). It returns {data,cursor,hasMore} with an unchanged cursor when empty. Your runner controls when to repeat the read, its deadline and any parallel work.

Matching, resends and continuation

Pass match: {sender, contentIncludes, linkHostname} to convenience waits when the task has an expected sender, content marker or link host. Only explicitly unrelated receipts are skipped. onUnmatched(receipt) can persist progress. waitForOtpReceipt() returns {message,cursor,code}; waitForLink() adds the expected link. Extraction fails on absent or ambiguous candidates instead of silently selecting another matching receipt. PhonekitError.cursor preserves progress.

After a timeout or resend, the caller decides whether to resume from saved progress or mark again before requesting a replacement. The target service owns code expiry and validity. Use the Playwright verification recipe or GitHub Actions workflow for a complete application example.

Deadlines, cancellation, and errors

timeoutMs is an integer from 1 to 3,600,000 milliseconds. It covers the whole wait: immediate HTTP reads, local polling intervals, response-body reads, transient retries and retry delays. The SDK never creates a server message wait. Use pollIntervalMs to choose the local interval (100–60,000 ms; default 1,000).

Use a per-operation signal to cancel one wait, or a constructor signal to cancel every request from that client:

Cancel a single waitTypeScript
const controller = new AbortController()
const pending = number.waitForMessage({
  after,
  timeoutMs: 120_000,
  signal: controller.signal,
})
// In your teardown or stop handler: controller.abort()
const receipt = await pending

Branch on PhonekitError.kind; avoid treating all failures as missing SMS:

Distinguish delivery from extractionTypeScript
import { PhonekitError } from "@phonekit/sdk"
 
try {
  const code = await number.waitForOtp({ after, timeoutMs: 120_000 })
  // Submit code and assert the application's final state.
} catch (error) {
  if (error instanceof PhonekitError) {
    if (error.kind === "timeout") {
      // No new receipt arrived within the total wait budget.
    } else if (error.kind === "missing_otp") {
      // A receipt arrived; inspect it with waitForMessage({ after, ... }).
    } else if (error.kind === "cancelled") {
      // The caller deliberately stopped this operation.
    }
  }
  throw error
}
Error kindAction
unauthorizedCheck that the key is valid and has not been revoked.
not_foundCheck the number or message ID and the key's selected numbers.
invalid_requestCorrect the reference, cursor, deadline, or other input.
rate_limited, unavailable, networkTransient retries were exhausted within the operation's deadline; apply a bounded task-level retry if appropriate.
unexpected_responseUpdate the client or investigate an API-contract mismatch.
timeout, no_match, missing_otp, ambiguous, cancelledHandle the distinct outcomes shown above.

Scroll horizontally to see all columns.

Safe HTTP errors include status and a generated requestId when available. They do not copy message bodies, keys, or abort reasons. The client retries transient failures up to three attempts per request, with backoff and Retry-After support, while respecting the overall wait deadline.

Questions about the SDK

Can parallel tests share one number?

They can read the same number, but a cursor does not identify which test requested a message. Two overlapping send actions can make both waits return the same first receipt. Give concurrent workers separate numbers, or serialize verification flows that use one number. See the reliability guide for allocation and resend patterns.

What should I do when an SMS arrives without a detected code?

waitForOtp() raises missing_otp for that first later receipt. Call waitForMessage() with the same after cursor to retrieve it, then deliberately handle a link, a changed message format, or an unrelated sender. Advancing to the receipt's cursor is an explicit decision; the SDK only skips receipts outside your explicit matching filters.

Does a timeout mean the application never sent the SMS?

It means this wait did not receive a later message within its total budget. Check whether your application accepted the send request, whether the provider accepted delivery, and whether you selected the right Phonekit number. A message may arrive after the wait ends. For a new resend attempt, capture a new mark and follow your application's code-expiration and resend rules.

Can I call the SDK from a browser or Cypress test body?

Keep the key in a Node process. Playwright tests run there directly; Cypress browser tests should use a server-side cy.task() boundary. The Cypress guide shows that split. Mobile apps and simulator tests can use your test service or the HTTP API from a trusted runner.

Will creating a handle buy a number or send a verification SMS?

No. A handle addresses an existing number selected for the key. Your application or auth provider sends the verification message. Phonekit receives it and makes it readable; number ownership, key scope, and the issuing service's acceptance rules still apply.