Agent Mail and Agent Phone SDK
Use qoni.genauth.agentMail and qoni.genauth.agentPhone to manage communication resources, extract verification codes, and inspect asynchronous operations.
Unreleased interfaces
This page describes the Qoni SDK communication extension merged into the SDK source. As of 2026-10-12, the npm release @qoniai/qoni@0.9.0 does not include these methods. The examples require a build containing this extension. Deploy the corresponding server modules as well; installing the SDK does not enable providers, purchasing, or sending.
The two namespaces provide 111 HTTP methods: 20 Mail runtime methods, 25 Mail administration methods, 26 Phone runtime methods, and 40 Phone administration methods. Credential binding and bounded polling are additional helpers. See the communication method index for complete types and routes.
Prerequisites and initialization
Use a trusted Node.js service. Prepare Qoni AK/SK, native GenAuth AK/SK for the same userpool, and deployed Mail/Phone modules. Phone runtime access keys must also satisfy administrator policies for personas, resources, actions, and spending.
import { Qoni } from '@qoniai/qoni'
const qoni = new Qoni({
accessKey: process.env.QONI_ACCESS_KEY!,
secretKey: process.env.QONI_SECRET_KEY!,
genauthCommunicationCredentials: {
accessKeyId: process.env.GENAUTH_ACCESS_KEY_ID!,
accessKeySecret: process.env.GENAUTH_ACCESS_KEY_SECRET!,
userPoolId: process.env.GENAUTH_USER_POOL_ID!,
},
})| Entry / option | Behavior |
|---|---|
agentMail.*, agentPhone.* | Runtime. Native GenAuth AK/SK exchanges for a standard management bearer, with shared caching and concurrent exchange deduplication |
agentMail.admin.*, agentPhone.admin.* | Administration. Reuses Qoni administrator-token exchange and adds the required administration confirmation header |
genauthCommunicationCredentials | Native { accessKeyId, accessKeySecret, userPoolId }, or standard { accessToken, userPoolId } |
genauthHost | Optional private deployment address; otherwise discovered through Qoni |
timeoutMs | Per-request HTTP timeout, default 30000 milliseconds |
Second argument { signal } | Cancels the request; polling helpers also cancel their sleep |
Qoni administrator tokens and standard access-key tokens represent different identities. Qoni AK/SK alone supports administration; runtime calls fail locally when native credentials are missing. Override administration with paired adminToken / userPoolId, or runtime with paired accessToken / userPoolId. Explicit bearers are never refreshed automatically.
Bind credentials once to a namespace and reuse that client:
const mail = qoni.genauth.agentMail.withCredentials({
accessKeyId: process.env.GENAUTH_ACCESS_KEY_ID!,
accessKeySecret: process.env.GENAUTH_ACCESS_KEY_SECRET!,
userPoolId: process.env.GENAUTH_USER_POOL_ID!,
})Bound clients have independent credential caches. Constructor communication credentials share one runtime token cache across Mail and Phone. Native credentials refresh and retry at most once following 401; permission failures, revision conflicts, network failures, and provider failures never replay writes.
Personas and communication resources
Unified Agent Management uses an Agent as the owner of communication resources. Its agentId equals the communication personaId; the linked IAM identity is returned separately as capabilities.identity.iamAgentId. Use personaId for the Mail/Phone methods on this page, and use the IAM ID for delegation. Each Agent can own multiple mailboxes and phone numbers. Identity, Mail, and Phone are optional capabilities; creating communication resources does not change a user's login email or phone.
The Console manages this relationship through /api/v3/agents. These unified management routes are not included in the 111-method communication SDK catalog below. Existing persona APIs remain compatible.
const { data: persona } = await qoni.genauth.agentMail.admin.personas.create({
displayName: 'Communication assistant',
externalRef: 'business-persona-001',
})
const { data: resources } = await qoni.genauth.agentMail.resources.list({
personaId: persona.personaId,
limit: 20,
})Mail resources.registerPhone records a number. Phone admin.numbers.register records a manual number. Manual records do not establish provider SMS capability. Use Phone methods for purchasing, sending, and lease management.
agentMail.*
Create a mailbox and wait for its result
const { data: mailboxRequest } = await qoni.genauth.agentMail.mailboxes.create({
personaId: persona.personaId,
idempotencyKey: 'mailbox-create-business-001',
})
const { data: mailboxOperation } = await qoni.genauth.agentMail.operations.wait({
operationId: mailboxRequest.operationId,
}, { timeoutMs: 120_000 })
if (mailboxOperation.status !== 'succeeded') {
throw new Error(`Mailbox operation: ${mailboxOperation.status}`)
}
const { data: mailbox } = await qoni.genauth.agentMail.mailboxes.get({
personaId: persona.personaId,
mailboxId: mailboxRequest.resourceId,
})mailboxes.create returns HTTP 202 with { operationId, resourceId, status, pollAfterSeconds }; the mailbox is not ready yet. If providerConfigId is omitted, the server selects a provider only when exactly one eligible configuration exists. With multiple providers, specify it explicitly. A mailbox ID is also its resource ID.
List, read, and send mail
const { data: messages } = await qoni.genauth.agentMail.messages.list({
personaId: persona.personaId,
mailboxId: mailbox.mailboxId,
direction: 'inbound',
limit: 20,
})
const { data: sendRequest } = await qoni.genauth.agentMail.messages.send({
personaId: persona.personaId,
mailboxId: mailbox.mailboxId,
to: 'recipient@example.com',
subject: 'Business notification',
text: 'Your request is complete.',
idempotencyKey: 'mail-send-business-001',
})Lists contain message summaries. Read bodies with messages.get({ personaId, mailboxId, messageId }) and inspect contentStatus. to is a single email address. After HTTP 202, continue querying the original operation. succeeded means the provider accepted the request; inspect deliveryStatus for delivery progress, which does not prove the recipient received it.
Email verification
Create the extraction task before triggering the external platform to send its code. Matching is restricted to this task's time window.
const { data: verification } = await qoni.genauth.agentMail.verifications.create({
personaId: persona.personaId,
mailboxId: mailbox.mailboxId,
platformKey: 'demo-platform',
externalRef: 'registration-attempt-001',
match: { fromDomains: ['demo.example.com'], subjectContains: 'Verification code' },
extract: { kind: 'code', pattern: 'Verification code[: ]*([0-9]{6})' },
timeoutSeconds: 180,
idempotencyKey: 'mail-verification-business-001',
})
// Trigger the external platform to send its code here, then wait for this task.
const { data: mailResult } = await qoni.genauth.agentMail.verifications.wait({
personaId: persona.personaId,
mailboxId: mailbox.mailboxId,
verificationRequestId: verification.verificationRequestId,
}, { timeoutMs: 190_000 })Use either templateId or match / extract. Match requires at least fromAddresses or fromDomains; restricted patterns require one capture group. For links, use kind: 'link' and allowedLinkDomains. After matched, inspect result and resultAvailableUntil. Handle multiple results when ambiguous. Task timeouts range from 30 to 600 seconds, default 180.
External accounts and administration
externalAccounts.create/list/get/update records platform registration results and resource bindings, without passwords. Updating an account or persona requires its current revision, encoded as quoted If-Match.
Administration covers providers and credential rotation, connectivity tests, extraction templates, persona status and deletion jobs, operation reconciliation, send-outcome resolution, events, audits, and health. Template updates use a business version, rather than revision. Deletion creates a job; delete_mailboxes requires confirming all current mailboxIds. Unresolved Phone resources or legacy Agent Identity state can block deletion; SDK errors retain the server code.
agentPhone.*
Quote and acquire a number
Acquiring numbers incurs provider charges. Inspect candidates, request a quote, and submit acquisition with the quote's currency and a spending cap.
const { data: phoneProviders } = await qoni.genauth.agentPhone.admin.providers.list()
const phoneProvider = phoneProviders.items.find(p => p.provider === 'twilio' && p.enabled)
if (!phoneProvider) throw new Error('No enabled Twilio provider')
const { data: quote } = await qoni.genauth.agentPhone.quotes.create({
personaId: persona.personaId,
providerConfigId: phoneProvider.id,
kind: 'number',
spec: { country: 'US' },
})
const { data: acquisition } = await qoni.genauth.agentPhone.numbers.acquire({
personaId: persona.personaId,
quoteId: quote.id,
maxCost: { amount: quote.amount, currency: quote.currency },
idempotencyKey: 'phone-acquire-business-001',
})
const { data: phoneOperation } = await qoni.genauth.agentPhone.operations.wait({
personaId: persona.personaId,
operationId: acquisition.id,
}, { timeoutMs: 120_000 })
if (phoneOperation.status !== 'succeeded') {
throw new Error(`Phone operation: ${phoneOperation.status}`)
}Quotes expire after two minutes and are scoped to their creator and persona. maxCost.amount is a decimal string with up to six fractional digits. The server rejects unverified pricing, expired health, unmet spending policies, and disabled purchasing. The SDK never raises spending caps or repeats purchases automatically.
SMS and number release
| Method | Main input | Result |
|---|---|---|
numbers.list/get | personaId; detail also needs resourceId | Number capabilities and lease state |
numbers.messages | personaId, resourceId | SMS summaries without bodies |
messages.content | personaId, messageId | { body }; content can expire |
numbers.send | personaId, resourceId, to, body, maxCost, idempotencyKey | Asynchronous operation; may incur charges |
numbers.release | personaId, resourceId, reason, revision, idempotencyKey | Asynchronous release |
Check capabilities and lease state first. smsReceive, smsSend, and thirdPartyOtp are string capability states; unknown does not establish support. Release requires the current revision. Current login, recovery, or contact bindings can block release or archival.
Verification tasks and temporary activations
verifications.create creates a code task for an existing number, using rules: { pattern, sender? } and a 30–900 second timeout. verifications.wait polls only waiting, returning ready, ambiguous, or another final state. Reading candidates also requires sms.content.read.
activations.create uses a kind: 'activation' quote to create a temporary activation. Inspect it with activations.get/messages/candidates. cancel / finish require current revision and idempotencyKey; consume requires a stable key and, when multiple results exist, a candidateId. Consumption returns { candidateId, value, messageId, ... }. Consumed, expired, revoked, or lease-invalidated results cannot be reused.
Phone administration additionally supports provider creation, updates and health tests, manual numbers and archival, binding inspection, policies, and reconciliation. Configuration updates invalidate old health results; run providers.test before enabling. Administrative quotes.create also requires personaId; some lists accept optional persona filters. The server enforces immutable provider accounts and resource ownership.
Phone admin.operations.wait and admin.verifications.wait reuse administrator credentials, so provider health checks and other administrative tasks can be awaited without native runtime keys.
Idempotency, revisions, pagination, and polling
| Input / result | Behavior |
|---|---|
idempotencyKey | Sent as Idempotency-Key, outside business JSON. Persist and reuse the key for the same command; changed inputs represent a new command |
revision | Positive integer mapped to If-Match: "N"; after 412, reread and reassess the intended change |
meta.etag | Preserves the quoted server ETag; meta.httpStatus retains communication HTTP status |
| Mail lists | { items, nextCursor }; pass cursor for the next page, with limit 1–100 |
| Phone number lists | { items, total, page, limit }; other lists usually contain at most 100 { items } |
operations.wait / verifications.wait | Default total deadline 120 seconds and minimum polling interval 1 second. Honors Mail pollAfterSeconds; configure timeoutMs, pollIntervalMs, and signal in the second argument |
Polling helpers only perform GETs. Unknown, failed, or business-action states return unchanged. The overall deadline raises QoniTimeoutError without cancelling the server operation. Keep querying the original operation ID: Phone operation objects use id, whereas Mail uses operationId.
Check results and handle errors
QoniResponse<T> retains { data, meta }. Communication error.code and error.retryable propagate into QoniError, along with HTTP status, request ID, and the original response.
| Error / state | Action |
|---|---|
401 / 403 | Check native credentials, userpool, administrator identity, and Phone policies. Callers update explicit bearers |
PRECONDITION_FAILED / 412 | Reread and reassess the intended update; do not replay stale revisions |
RESULT_UNKNOWN, operation unknown | Inspect the original operation and reconcile administratively if needed; do not send or purchase again with a new key |
RESULT_AMBIGUOUS | Inspect candidates and select candidateId explicitly |
CONTENT_EXPIRED / RESULT_UNAVAILABLE | Content or results are no longer readable; start a new business flow |
Route 404 | Check genauthHost and module deployment; installing the SDK cannot enable server routes |
Next steps
Use the communication method index for complete input types, result types, and HTTP routes. See GenAuth management SDK for users, organizations, and permissions.