npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tyxter/sdk-js

v0.9.0

Published

Tyxter Messaging TypeScript/JavaScript SDK.

Readme


status: active owner: tyxter-core

@tyxter/sdk-js

Phone provision/connect and Meta WhatsApp registration return advisory warnings with stable code, field: 'display_name', and informational message. These suggestions do not block submission or predict Meta approval. The field may be absent on older cached idempotency responses; the SDK passes the accepted response through unchanged.

Purpose

Tyxter Messaging SDK for TypeScript / JavaScript. Wraps the public /v1/* API and provides the customer-side webhook signature verifier so integrators don't have to re-implement HMAC checking.

The public repository for release source and issue reporting is tyxter-dev/tyxter-node. Report SDK bugs through its public issues. Canonical development and npm publishing remain in this monorepo. The public checkout is generated from published releases; its portable build and tests exclude the private contract/parity checks and the canonical Salvy README test. The npm homepage and issue links point to the public repository. Its repository metadata identifies this monorepo to match npm Trusted Publishing.

Public API

Version 0.9.0 includes the WhatsApp Business groups beta, payment cancellation, phone renewal reads, phone management, and customer-owned Salvy connection/phone methods. Salvy phone import admission remains unavailable when its rollout is inactive; an SDK method is not evidence of runtime availability.

Subscription writes preserve their signatures and honor the optional idempotency key: billing.plans.subscribe({ plan_offering_id }, key), .change({ plan_offering_id }, key), and .cancel(key). Package purchases accept an optional saved-card payment_method_id; their only payment_method is card (the default).

contacts.list({ search, status, tag_id }) filters on the server. Use llm.getRoute({ phone_number_id }) and llm.deleteRoute({ idempotencyKey }, { phone_number_id }) for phone-specific routes; omitting the phone selector targets the environment default. Automation updates accept description: null to clear it. Consent, AI/LLM, flow, retention, auto-top-up, card-saving, and cost-estimate requests expose typed API parameters.

Device authorization create/token, batch reads/controls, media reads/delete, and transcription retrieval accept optional traceId. Read methods take options after their existing query or id arguments; methods without arguments take options first.

whatsappChannels, WhatsAppChannelsResource, and WhatsAppChannelMessage are deprecated legacy exports: WhatsApp Channel publishing is unsupported. Their requests retain the API's 400 invalid_message_request refusal. Use whatsapp for supported one-to-one sends.

import { Tyxter, WhatsAppMessage } from '@tyxter/sdk-js';

const tyxter = new Tyxter({ apiKey: process.env.TYXTER_API_KEY! });

const account = await tyxter.account.retrieve();
// account.dashboard_owner?.email identifies the dashboard account that owns this key.

const project = await tyxter.projects.create(
  { name: 'Acme Clinics', slug: 'acme-clinics' },
  { idempotencyKey: crypto.randomUUID() },
);
const projects = await tyxter.projects.list({ limit: 20 });
const sameProject = await tyxter.projects.retrieve(project.id);
// Project management is organization-scoped. The list contains active projects only.

// A dashboard-created administrative source key can bootstrap an operational
// key for an active sibling project. It must already hold every delegated scope.
const operationalKey = await tyxter.apiKeys.create(
  {
    name: 'Acme Clinics sender',
    environment: 'sandbox',
    project_id: project.id,
    scopes: ['messages:send'],
  },
  { idempotencyKey: crypto.randomUUID() },
);
// Store operationalKey.secret securely; Tyxter returns it only on this create response.

// Generate returns an editable NAMED draft; it does not create a Template row.
const namedDraft = await tyxter.templates.generate({
  description: 'Tell a customer that their order is ready to track.',
  language: 'pt_BR',
  category: 'utility',
  parameter_format: 'NAMED',
});

// Create is the durable authoring step. NAMED BODY and TEXT HEADER examples are
// exact, one-for-one mappings for each unique lowercase {{name}} token.
const namedTemplate = await tyxter.templates.create({
  name: 'order_tracking_update',
  language: 'pt_BR',
  category: 'utility',
  parameter_format: 'NAMED',
  components: [
    {
      type: 'HEADER',
      format: 'TEXT',
      text: 'Pedido {{order_id}}',
      example: {
        header_text_named_params: [{ param_name: 'order_id', example: 'ORD-123' }],
      },
    },
    {
      type: 'BODY',
      text: 'Olá {{customer_name}}, acompanhe o pedido {{order_id}}.',
      example: {
        body_text_named_params: [
          { param_name: 'customer_name', example: 'Ana' },
          { param_name: 'order_id', example: 'ORD-123' },
        ],
      },
    },
    {
      type: 'BUTTONS',
      buttons: [
        { type: 'URL', text: 'Acompanhar', url: 'https://example.com/orders/{{order_id}}' },
      ],
    },
  ],
});
await tyxter.templates.submit(namedTemplate.id);
// Wait for the template's approved status before sending.

const message = await tyxter.whatsapp.sendTemplate({
  from: 'phone_number_id',
  to: '+5511999999999',
  name: namedTemplate.name,
  language: 'pt_BR',
  // No send parameter_format: the approved TemplateVersion chooses NAMED.
  // BODY keys must exactly match its BODY names; numeric keys are rejected.
  variables: { customer_name: 'Ana', order_id: 'ORD-123' },
  components: [
    {
      type: 'header',
      parameters: [{ type: 'text', parameter_name: 'order_id', text: 'ORD-123' }],
    },
    {
      type: 'button',
      sub_type: 'url',
      index: 0,
      parameters: [{ type: 'text', parameter_name: 'order_id', text: 'ORD-123' }],
    },
  ],
});

// Standalone COPY_CODE is a marketing coupon button (distinct from an OTP
// button whose otp_type is COPY_CODE). Author it without a text field.
const couponTemplate = await tyxter.templates.create({
  name: 'winter_coupon',
  language: 'en_US',
  category: 'marketing',
  components: [
    { type: 'BODY', text: 'Use this coupon at checkout.' },
    { type: 'BUTTONS', buttons: [{ type: 'COPY_CODE', example: 'WINTER25' }] },
  ],
});

// After approval, direct sends provide the runtime coupon value in Meta's exact shape.
await tyxter.whatsapp.sendTemplate({
  from: 'phone_number_id',
  to: '+5511999999999',
  name: couponTemplate.name,
  language: couponTemplate.language,
  components: [
    {
      type: 'button',
      sub_type: 'copy_code',
      index: 0,
      parameters: [{ type: 'coupon_code', coupon_code: 'WINTER25' }],
    },
  ],
});
// COPY_CODE templates are intentionally unsupported by batches because the batch
// contract has no per-recipient button-parameter source. Use interactive.order_details
// with pix_dynamic_code for Pix rather than placing a Pix payload in COPY_CODE.

await tyxter.whatsapp.sendText({
  from: 'phone_number_id',
  to: '+5511999999999',
  body: 'Your order is ready.',
});

// Native Brazil Pix order details for a WABA eligible for Meta Payments API in Brazil.
// Optional alternative: most integrations should collect Pix with a Tyxter payment
// request through Abacate Pay instead (see the payments example further down).
// This free-form interactive send needs an open 24-hour customer-service window;
// otherwise Tyxter rejects synchronously with service_window_required. A 202 is returned
// only after contract/auth/feature/service-window prevalidation. Meta then evaluates
// WABA/message eligibility at provider-send time, so acceptance does not promise delivery.
// The caller owns the reference, Pix code, merchant name, key, and key type.
await tyxter.whatsapp.sendInteractive({
  from: 'phone_number_id',
  to: '+5511999999999',
  interactive: {
    type: 'order_details',
    body: { text: 'Review and pay your order.' },
    action: {
      name: 'review_and_pay',
      parameters: {
        reference_id: 'ord_123-20260807',
        type: 'digital-goods',
        payment_type: 'br',
        payment_settings: [
          {
            type: 'pix_dynamic_code',
            pix_dynamic_code: {
              code: '00020101021226700014br.gov.bcb.pix...',
              merchant_name: 'Tyxter Store',
              key: '[email protected]',
              key_type: 'EMAIL',
            },
          },
        ],
        currency: 'BRL',
        total_amount: { value: 1290, offset: 100 },
      },
    },
  },
});
// This message has no payment_id resolution or order_status update. Meta does not reconcile
// settlement; confirm it with the merchant or PSP.

// Additive strict structured-phone input; the legacy `to: "+E.164"` form remains supported.
await tyxter.whatsapp.sendText({
  from: 'phone_number_id',
  to: { countryCallingCode: '55', nationalNumber: '11903244174' },
  body: 'Your order is ready.',
});

const media = await tyxter.media.upload(
  {
    kind: 'image',
    filename: 'promo.png',
    mime_type: 'image/png',
    byte_length: fileBytes.byteLength,
    body: fileBytes,
  },
  {
    createIdempotencyKey: crypto.randomUUID(),
    completeIdempotencyKey: crypto.randomUUID(),
  },
);
await tyxter.whatsapp.sendMedia({
  from: 'phone_number_id',
  to: '+5511999999999',
  media: { kind: 'image', asset_id: media.id, caption: 'Launch promo' },
});

// WhatsApp voice notes require OGG/Opus mono audio. Omit `voice` (or pass
// `false`) to send ordinary audio instead.
await tyxter.whatsapp.sendMedia({
  from: 'phone_number_id',
  to: '+5511999999999',
  media: {
    kind: 'audio',
    link: 'https://example.com/appointment-reminder.ogg',
    mime_type: 'audio/ogg',
    voice: true,
  },
});

// Inbound WhatsApp media: use Tyxter's mda_* id from the message read or message.received.
// Meta image.id/audio.id and lookaside URLs are not SDK download handles.
const inbound = await tyxter.messages.retrieve('msg_inbound_123');
if (!inbound.media || inbound.media.status !== 'consumed') throw new Error('Media unavailable');
// This relative hint is safe to retain; it is not the signed capability URL.
console.log(inbound.media.download.method, inbound.media.download.path);
const download = await tyxter.media.createDownloadUrl(inbound.media.asset_id);
const image = document.querySelector<HTMLImageElement>('#inbound-media');
if (image) image.src = download.download_url; // five-minute capability URL

// Transcription is an explicit, asynchronous opt-in for inbound audio.
const transcript = await tyxter.messages.requestTranscription('msg_inbound_audio_123', {
  language: 'pt',
  prompt: 'Appointment at the clinic',
  keywords: ['Tyxter', 'Dr. Ada'],
});
// Poll, or subscribe to both terminal webhooks: message.media_transcribed
// (success) and message.media_transcription_failed (failure).
await tyxter.messages.retrieveTranscription(transcript.message_id);

// A failed receipt is not reopened by calling requestTranscription again.
// Retry the same original inbound audio with a required idempotency key instead.
await tyxter.messages.retryTranscription(
  transcript.message_id,
  { language: 'pt' },
  { idempotencyKey: crypto.randomUUID(), traceId: 'trc_transcription_retry' },
);

await tyxter.whatsapp.sendMedia({
  from: 'phone_number_id',
  to: '+5511999999999',
  media: { kind: 'image', link: 'https://example.com/promo.png' },
});

await tyxter.whatsapp.sendAudioFromText({
  from: 'phone_number_id',
  to: '+5511999999999',
  tts: {
    type: 'tts',
    provider: 'openai',
    voice: 'alloy',
    language: 'en',
    text: 'Your appointment is confirmed.',
  },
});

await tyxter.instagram.sendText({
  accountId: 'ig_business_account_id',
  userId: 'igsid_123',
  body: 'Thanks for your DM.',
});

// Raw channel-native requests remain available for advanced integrations.
await tyxter.messages.create(
  WhatsAppMessage.text({
    from: 'phone_number_id',
    to: '+5511999999999',
    body: 'Built with the SDK payload builder.',
  }),
);

// Billing packages (Stage 12)
const rateCards = await tyxter.billing.rateCards.list();
const packages = await tyxter.billing.packages.list();
await tyxter.billing.packages.purchase(
  { package_code: 'starter_30k', payment_method: 'card' },
  crypto.randomUUID(),
);

// Read-only prepaid phone renewal decisions. An at-risk decision points to
// existing authenticated credit or auto-top-up recovery — it never creates a
// manual renewal or returns a payment URL. A terminal `cancelled` decision
// records an operational lifecycle closure, not a non-payment release.
const renewals = await tyxter.billing.phoneRenewals.list({ status: 'funding_required' });
const renewal = await tyxter.billing.phoneRenewals.retrieve(renewals.data[0].id);
console.log(renewal.actionable_state, renewal.period_end);

// Saved card + auto top-up (Stage 12)
const setup = await tyxter.billing.paymentMethods.createSetupIntent();
// Confirm setup.stripe_client_secret with Stripe.js, then save the resulting pm_* id.
const card = await tyxter.billing.paymentMethods.save({
  stripe_payment_method_id: 'pm_123',
  set_default: true,
});
await tyxter.billing.autoTopup.update({
  enabled: true,
  threshold_brl: '50.00',
  amount_brl: '200.00',
  payment_method_id: card.id,
});

// AI Agents + Automations
const agent = await tyxter.aiAgents.create(
  {
    name: 'Order support agent',
    provider: 'openai',
    model: 'gpt-4.1-mini',
    api_key: process.env.OPENAI_API_KEY!,
    system_prompt:
      'Answer customer order questions using only the automation input and escalate when unsure.',
  },
  { idempotencyKey: crypto.randomUUID() },
);
const automation = await tyxter.automations.create(
  { name: 'Order created follow-up' },
  { idempotencyKey: crypto.randomUUID() },
);
await tyxter.aiAgents.complete(
  agent.id,
  { messages: [{ role: 'user', content: 'Where is order ord_123?' }] },
  { idempotencyKey: crypto.randomUUID() },
);
const version = await tyxter.automations.createVersion(
  automation.id,
  {
    graph: {
      version: 'automation_graph_v1',
      nodes: [
        { id: 'webhook', type: 'webhook.trigger', config: { slug: 'orders-created' } },
        { id: 'agent', type: 'ai_agent.invoke', config: { ai_agent_id: agent.id } },
      ],
      edges: [{ id: 'webhook_agent', source: 'webhook', target: 'agent' }],
    },
  },
  { idempotencyKey: crypto.randomUUID() },
);
await tyxter.automations.publish(
  automation.id,
  { version_id: version.id },
  { idempotencyKey: crypto.randomUUID() },
);
await tyxter.automations.rotateWebhookSecret(automation.id, 'orders-created', {
  idempotencyKey: crypto.randomUUID(),
});
await tyxter.automations.invokeWebhook(
  'orders-created',
  { input: { order_id: 'ord_123' } },
  { idempotencyKey: crypto.randomUUID() },
);

// Saved audiences (R1)
const audience = await tyxter.audiences.create({
  name: 'Launch list',
  contact_ids: ['ct_1', 'ct_2'],
});
await tyxter.batches.create({
  channel: 'whatsapp',
  from: 'phone_number_id',
  template: { name: 'order_shipped', language: 'pt_BR' },
  audience_id: audience.id,
});

// Webhook event logs
const sandboxStatus = await tyxter.sandbox.quickstart();
// sandboxStatus.webhooks.inbound_event_type === 'message.received'
// sandboxStatus.capabilities.send_template_messages tells you whether a template
// send can succeed (scope + sender + at least one approved template), and
// sandboxStatus.templates.default_template suggests which one to use.
await tyxter.sandbox.inboundMessages.create({
  channel: 'whatsapp',
  from: '+5511999999999',
  to: 'phone_number_id',
  type: 'text',
  text: { body: 'Opening the sandbox service window' },
});
await tyxter.sandbox.templates.setStatus('tmpl_123', {
  status: 'rejected',
  rejection_reason: 'Sandbox policy fixture',
});

// Message list bodies are opt-in. Without include, payload and metadata are null;
// the tenant-safe inbound `media` descriptor is still present by default.
const failedMessages = await tyxter.messages.list({
  status: 'failed',
  include: 'payload',
});
// failedMessages.data[0].payload and .metadata contain the stored request data.
// A production inbound WhatsApp row can have sender.id === '' only when Meta
// withheld the customer's phone. Treat it as unresolved; never send to it.

await tyxter.webhookEvents.list({
  event_types: 'message.sent,message.failed',
  status: 'failed',
});
await tyxter.webhookEvents.retrieveListenEvent('outbox_event_id');

// General merchant payment request. The configured provider can return a hosted payment link.
const payment = await tyxter.payments.create(
  {
    amount_brl_centavos: 12990,
    description: 'Order #123',
    customer_name: 'Ana Silva',
    customer_tax_id: '12345678901',
    customer_phone: '+5511999999999',
    external_reference: 'ord_123',
  },
  { idempotencyKey: crypto.randomUUID() },
);
// payment.provider is the configured merchant provider, such as "iniciador" or "abacate_pay".
// payment.payment_link_url may be null while the provider worker is still generating the link.
// Pix providers can also return payment.pix_copy_paste and payment.pix_qr_code_base64.
// payment.fees is what accepting it cost: initiation.amount_brl is charged once, and
// completion.amount_brl is held until the outcome (completion.state). fees.simulated is true in
// sandbox, where nothing leaves the balance; fees is null for a payment accepted before fees applied.
const latestPayment = await tyxter.payments.retrieve(payment.id);
await tyxter.payments.requestApproval(
  latestPayment.id,
  { note: 'Customer requested a fresh approval URL' },
  { idempotencyKey: crypto.randomUUID() },
);

// Transparent Abacate Pix — the recommended merchant Pix path for WhatsApp conversations.
// First verify Abacate Pay is both connected and selected/default for this environment.
// GET /v1/provider-connections/status must report channels.payments.active_mode === "abacate_pay".
// This read requires provider_connections:read; without it the API returns 403 insufficient_scope.
// If readiness does not resolve to Abacate Pay, payment creation can fail with a provider
// selection/options mismatch error.
const providerStatus = await tyxter.providerConnections.status();
if (providerStatus.channels.payments.active_mode !== 'abacate_pay') {
  throw new Error('Connect and select Abacate Pay before requesting transparent Pix');
}

// POST /v1/payments creates provider work asynchronously.
const pixCreated = await tyxter.payments.create(
  {
    amount_brl_centavos: 12900,
    description: 'Order #123',
    external_reference: 'ord_123',
    provider_options: { abacate_pay: { charge_type: 'pix' } },
  },
  { idempotencyKey: crypto.randomUUID() },
);

let pixPayment = pixCreated;
for (let attempt = 0; attempt < 30 && !pixPayment.pix_copy_paste; attempt += 1) {
  if (
    pixPayment.status === 'failed' ||
    pixPayment.status === 'expired' ||
    pixPayment.status === 'cancelled'
  ) {
    throw new Error('Pix creation ended with ' + pixPayment.status);
  }
  await new Promise((resolve) => setTimeout(resolve, 1_000));
  pixPayment = await tyxter.payments.retrieve(pixCreated.id); // GET /v1/payments/{payment_id}
}

if (!pixPayment.pix_copy_paste) throw new Error('Pix code is still pending');

// This free-form WhatsApp text requires an open 24-hour customer-service window.
// Without one, POST /v1/messages rejects synchronously with service_window_required;
// use an approved template outside the window.
await tyxter.whatsapp.sendText({
  from: 'phone_number_id',
  to: '+5511999999999',
  body: 'Pay with Pix (copy and paste):\n' + pixPayment.pix_copy_paste,
});
// Subscribe to payment.paid for merchant PSP settlement on this payment request.
// It is not a Meta order-status signal.

Typing indicators

Use the Tyxter message ID at message.received.data.message_id when showing a typing indicator. The webhook envelope's top-level id names the event, while data.provider_message_id is Meta's reference; neither can target messages.typing(). Validate untyped webhook input before calling the SDK so a missing property cannot become the literal /v1/messages/undefined/typing path.

async function showTypingForInbound(event: { type?: unknown; data?: { message_id?: unknown } }) {
  if (event.type !== 'message.received') return;

  const messageId = event.data?.message_id;
  if (typeof messageId !== 'string' || messageId.trim() === '') {
    throw new Error('message.received is missing data.message_id');
  }

  await tyxter.messages.typing(messageId, { traceId: 'trc_reply_123' });
  // Generate and send the reply with tyxter.whatsapp.sendText(...) next.
}

Launch SDK Slice

The first locked SDK surface covers the core developer loop:

| Resource | Method | Endpoint | | ---------------------------------------------------- | ---------- | ------------------------------------------------------------------ | | account.retrieve | GET | /v1/account | | account.me | GET | /v1/me | | whatsapp / instagram / messages.create | POST | /v1/messages | | messages.list | GET | /v1/messages | | messages.retrieve | GET | /v1/messages/:message_id | | messages.requestTranscription | POST | /v1/messages/:message_id/transcription | | messages.retryTranscription | POST | /v1/messages/:message_id/transcription/retry | | messages.retrieveTranscription | GET | /v1/messages/:message_id/transcription | | messages.cancel | POST | /v1/messages/:message_id/cancel | | messages.typing | POST | /v1/messages/:message_id/typing | | media.list | GET | /v1/media | | media.createUpload | POST | /v1/media/uploads | | media.completeUpload | POST | /v1/media/uploads/:asset_id/complete | | media.storageUsage | GET | /v1/media/storage-usage | | media.retrieve | GET | /v1/media/:asset_id | | media.createDownloadUrl | GET | /v1/media/:asset_id/download-url | | media.delete | DELETE | /v1/media/:asset_id | | metaSignupSessions.create | POST | /v1/meta-signup-sessions | | metaSignupSessions.retrieve | GET | /v1/meta-signup-sessions/:session_id | | providerConnections.list | GET | /v1/provider-connections | | providerConnections.retrieve | GET | /v1/provider-connections/:connection_id | | providerConnections.salvy.register | POST | /v1/provider-connections/salvy | | providerConnections.salvy.rotate | POST | /v1/provider-connections/:connection_id/salvy/rotate | | providerConnections.salvy.refreshDiscovery | POST | /v1/provider-connections/:connection_id/salvy/discovery | | providerConnections.salvy.listNumbers | GET | /v1/provider-connections/:connection_id/salvy/numbers | | phoneNumbers.importSalvy | POST | /v1/phone-numbers/import-salvy | | phoneNumbers.completeSalvyRegistration | POST | /v1/phone-numbers/:phone_number_id/salvy/complete-registration | | phoneNumbers.convertToByon | POST | /v1/phone-numbers/:phone_number_id/convert-to-byon | | billing.phoneManagement.list | GET | /v1/billing/phone-management | | webhookEndpoints.create | POST | /v1/webhook-endpoints | | webhookEndpoints.list | GET | /v1/webhook-endpoints | | webhookEndpoints.retrieve | GET | /v1/webhook-endpoints/:webhook_endpoint_id | | webhookEndpoints.update | PATCH | /v1/webhook-endpoints/:webhook_endpoint_id | | webhookEndpoints.rotateSigningSecret | POST | /v1/webhook-endpoints/:webhook_endpoint_id/rotate-signing-secret | | webhookEndpoints.test | POST | /v1/webhook-endpoints/:webhook_endpoint_id/test | | webhookEndpoints.delete | DELETE | /v1/webhook-endpoints/:webhook_endpoint_id | | webhookEvents.list | GET | /v1/webhook-events | | webhookEvents.listen | GET | /v1/webhook-events/listen | | webhookEvents.retrieveListenEvent | GET | /v1/webhook-events/listen/:outbox_event_id | | webhookEvents.createListenSession | POST | /v1/webhook-events/listen-sessions | | webhookEvents.disableListenSession | DELETE | /v1/webhook-events/listen-sessions/:listen_session_id | | webhookEvents.retrieve | GET | /v1/webhook-events/:webhook_event_id | | webhookEvents.resend | POST | /v1/webhook-events/:webhook_event_id/resend | | webhookEvents.bulkResend | POST | /v1/webhook-events/bulk-resend | | sandbox.quickstart | GET | /v1/sandbox/quickstart | | sandbox.inboundMessages.create | POST | /v1/sandbox/inbound-messages | | sandbox.templates.setStatus | POST | /v1/sandbox/templates/:template_id/status | | usage.retrieve | GET | /v1/usage | | usage.listRecords | GET | /v1/usage/records | | payments.create | POST | /v1/payments | | payments.list | GET | /v1/payments | | payments.retrieve | GET | /v1/payments/:payment_id | | payments.cancel | POST | /v1/payments/:payment_id/cancel | | payments.requestApproval | POST | /v1/payments/:payment_id/request-approval | | aiAgents.create | POST | /v1/ai-agents | | aiAgents.list | GET | /v1/ai-agents | | aiAgents.retrieve | GET | /v1/ai-agents/:agent_id | | aiAgents.update | PATCH | /v1/ai-agents/:agent_id | | aiAgents.delete | DELETE | /v1/ai-agents/:agent_id | | aiAgents.listPromptVersions | GET | /v1/ai-agents/:agent_id/prompt-versions | | aiAgents.listResponseLogs | GET | /v1/ai-agents/:agent_id/response-logs | | aiAgents.complete | POST | /v1/ai-agents/:agent_id/completions | | automations.create | POST | /v1/automations | | automations.list | GET | /v1/automations | | automations.retrieve | GET | /v1/automations/:automation_id | | automations.update | PATCH | /v1/automations/:automation_id | | automations.delete | DELETE | /v1/automations/:automation_id | | automations.createVersion | POST | /v1/automations/:automation_id/versions | | automations.listVersions | GET | /v1/automations/:automation_id/versions | | automations.publish | POST | /v1/automations/:automation_id/publish | | automations.pause / automations.resume | POST | /v1/automations/:automation_id/pause, /resume | | automations.createRun / automations.listRuns | POST/GET | /v1/automations/:automation_id/runs | | automations.retrieveRun / automations.cancelRun | GET/POST | /v1/automation-runs/:run_id, /cancel | | automations.listRunSteps | GET | /v1/automation-runs/:run_id/steps | | automations.invokeWebhook | POST | /v1/automation-webhooks/:slug | | apiKeys.create | POST | /v1/api-keys | | apiKeys.list | GET | /v1/api-keys | | apiKeys.retrieve | GET | /v1/api-keys/:api_key_id | | apiKeys.rename | PATCH | /v1/api-keys/:api_key_id | | apiKeys.rotate | POST | /v1/api-keys/:api_key_id/rotate | | apiKeys.revoke | DELETE | /v1/api-keys/:api_key_id | | projects.create | POST | /v1/projects | | projects.list | GET | /v1/projects | | projects.retrieve | GET | /v1/projects/:project_id | | groups.retrieveEligibility (beta) | GET | /v1/groups/eligibility?phone_number_id= | | groups.createEligibilityCheck (beta) | POST | /v1/groups/eligibility-checks | | groups.create (beta) | POST | /v1/groups | | groups.list (beta) | GET | /v1/groups | | groups.retrieve (beta) | GET | /v1/groups/:group_id | | groups.delete (beta) | DELETE | /v1/groups/:group_id | | groups.resetInviteLink (beta) | POST | /v1/groups/:group_id/invite-link/reset | | sandbox.groups.simulateParticipant (beta, sandbox) | POST | /v1/sandbox/groups/:group_id/participants |

Endpoint paths and headers are pinned by resource tests. Wire request and response types are mirrored inside the SDK so customer installs need only this package; schema snapshots in the contract package are the breaking-change review point.

MessageReadSenderIdentity and every typed message.* webhook sender include profile_name: string | null. It is captured inbound WhatsApp profile evidence, not a verified identity or business display name. Message detail and messages.list({ include: 'payload' }) expose it when a resolved inbound WhatsApp sender has captured evidence; the default payload-less list, outbound and non-WhatsApp messages, phone-less rows, malformed capture, and redacted rows return null. Recipients and request identities never carry this field. Newly rendered webhook events include the field; already-stored historical webhook snapshots retain their stored body and can lack it. The two typed transcription envelope aliases include that historical snapshot variant, while MessageWebhookData itself keeps profile_name required.

The providerConnections.salvy methods use mandatory idempotencyKey arguments for registration, rotation, and discovery refresh. Registration and rotation return a 202 provider-connection receipt while the provider worker validates the sealed key. A 202 means that work was queued, so poll the connection until its operation is succeeded before reading the cached number list; the salvy response projection exposes the masked hint, polling mode, discovery timestamp, and latest operation outcome. salvy.listNumbers reads the cursor-paginated cached inventory and does not call Salvy. ProviderName, ProviderConnectionChannel, setup-session targets, and the Salvy request/response interfaces in contracts.ts mirror the public contract package.

Customer-owned Salvy phone foundation

For agent discovery, use providerConnections.salvy.* to register a customer account and refresh its cached inventory, then use phoneNumbers.importSalvy only to import one selected cached resource. phoneNumbers.importSalvy(input, idempotencyKey) sends POST /v1/phone-numbers/import-salvy; phoneNumbers.completeSalvyRegistration(phoneNumberId, input, idempotencyKey) sends POST /v1/phone-numbers/:phone_number_id/salvy/complete-registration; and phoneNumbers.convertToByon(phoneNumberId, idempotencyKey) sends POST /v1/phone-numbers/:phone_number_id/convert-to-byon. All three mutations require a non-empty Idempotency-Key. The import and completion responses are asynchronous phone receipts; conversion is the free exit from an existing customer-Salvy management attachment and preserves the same BYON phone and Meta link.

The inactive foundation can return 503 salvy_byok_unavailable from the import admission route even when the current plan reports salvy_byok_enabled: true; that plan field is not runtime admission evidence. Preserve the same idempotency key for a later retry, do not create an enrollment from cached discovery, and do not treat this response as an instruction to retry registration or conversion. billing.phoneManagement.list({ limit, starting_after }) sends GET /v1/billing/phone-management and is a cursor-paginated, read-only coverage projection: it never imports, enrolls, reconnects, or changes a phone.

webhookEndpoints.test(id, { idempotencyKey }) sends an empty, idempotent POST and returns the strict pending webhook_test receipt. It is the canonical fresh endpoint probe; read the returned event id through webhookEvents.retrieve for delivery status and attempt evidence.

Usage types keep Tyxter billing and Meta-direct estimates distinct. UsageRecordResponse.estimated_external_meta_cost is nullable and carries the verified event-time rate evidence when available; summary and bucket responses carry a separately marked aggregate plus covered/missing-reference quantities.

Webhook verification:

import { verifyWebhookSignature } from '@tyxter/sdk-js/webhook-verifier';

const ok = verifyWebhookSignature({
  secret: process.env.TYXTER_WEBHOOK_SECRET!,
  timestamp: req.headers['tyxter-webhook-timestamp'],
  signature: req.headers['tyxter-webhook-signature'],
  rawBody: req.rawBody,
});

Merchant payment webhook payloads are exported as a discriminated union, so handlers can narrow the complete event-time snapshot without recreating the wire shape:

import type { PaymentWebhookEnvelope } from '@tyxter/sdk-js';

function handlePayment(event: PaymentWebhookEnvelope) {
  switch (event.type) {
    case 'payment.created':
      // event.data.payment_link_url is null
      return;
    case 'payment.approval_available':
    case 'payment.link_generated':
      // event.data.payment_link_url is string
      return;
    case 'payment.paid':
      // terminal statuses are absorbing; ignore later earlier-state events
      return;
  }
}

CreditToppedUpWebhookEnvelope is the typed existing credit.topped_up snapshot. Its public data is topup_id, amount_brl, payment_method, optional provider, and balance_brl. provider is optional for historical events and must not be inferred; a promotion bonus has both fields set to 'promotion' and does not expose campaign or redemption details.

Meta policy warnings are an additive typed webhook export. They are evidence, not a blocked-send signal: violation_type is nullable/open and the data shape contains only the connection id, constant Meta provider, display name, warning type, and observation time — never WABA/phone ids, status, credentials, or the operator-only outbound count.

import type { ProviderConnectionPolicyWarningWebhookEnvelope } from '@tyxter/sdk-js';

function handlePolicyWarning(event: ProviderConnectionPolicyWarningWebhookEnvelope) {
  // Sends are not blocked by this warning. Surface Meta remediation and keep
  // handling ordinary send outcomes; continued violations may still lead Meta
  // to restrict or disable the account later.
  console.log(event.data.violation_type, event.data.observed_at);
}

Meta disable schedules are a separate additive typed webhook export. They are advisory evidence rather than a status change: waba_ban_date is a nullable ISO timestamp, and sends continue until Meta actually disables the account. The five-field data contract excludes WABA/phone ids, tenant scope, status/reason, restrictions, credentials, raw provider data, and operator-only evidence.

import type { ProviderConnectionDisableScheduledWebhookEnvelope } from '@tyxter/sdk-js';

function handleDisableSchedule(event: ProviderConnectionDisableScheduledWebhookEnvelope) {
  // Warn a human and review Account Quality; do not treat this as a blocked-send event.
  console.log(event.data.waba_ban_date, event.data.observed_at);
}

To receive this advisory, include provider_connection.disable_scheduled in the endpoint's subscribed_events.

WhatsApp Business group events (beta) are typed by GroupWebhookEnvelope (the six lifecycle outcomes, data GroupWebhookData) and GroupParticipantWebhookEnvelope (group.participant_joined / group.participant_removed, data GroupParticipantWebhookData). Lifecycle data is the group as Tyxter reads it when it sends the event or serves it on a listen read, plus the outcome's occurred_at, and includes invite_link, which lets anyone who has it join the group. On group.create_failed, group.delete_failed and group.invite_link_reset_failed, failure is instead the failure of the operation that failed, stored with the event when Tyxter recorded the outcome, so a later delete or reset does not change it; a reset that failed while the group was deleting carries its own reason. Participant data is the recorded join or removal: participant.wa_id, Tyxter's participant_count right after it applied the change, Meta's whole-second occurred_at, and initiated_by on a removal only.

import type { GroupParticipantWebhookEnvelope } from '@tyxter/sdk-js';

function handleParticipant(event: GroupParticipantWebhookEnvelope) {
  if (event.type === 'group.participant_removed') console.log(event.data.initiated_by);
  console.log(event.data.participant.wa_id, event.data.participant_count);
}

CreditHardBlockEngagedWebhookData, CreditHardBlockLiftedWebhookData and their matching WebhookEnvelope aliases type signed wallet lifecycle snapshots. Engagement adds a closed trigger union; both expose organization, four-decimal balance/floor and top-up URL. contracts-parity.ts checks schema parity.

Semantics

billing.balance() returns the required production_blocked fact for production API-key/MCP credit policy, including existing recovery exemptions, independently of the selected environment. Positive access recovery does not promise enough credit for the next rental fee or clear other sending gates. Its held_brl is credit held by outstanding production holds, already deducted from balance_brl; a hold is not spend. billing.listLedger() lists payment completion-fee hold and release entries beside debit and credit; only debit is spend, and payment_fee names the payment and fee reservation. PhoneRenewalResponse adds nullable provider renewal, scheduled 48h/24h warning and selected/end grace dates. Later invalidation does not erase captured historical dates. The status-derived history actions remain unchanged; warning dates are not delivery receipts and renewed retained rent can leave wallet debt. phoneNumbers.list() and phoneNumbers.retrieve() return PhoneNumberReadResponse, with required nullable renewal: PhoneRenewalSummary. BYON/sandbox return null; production Salvy unknown facts remain explicit in reasons/nulls. Committed release, grace and known insufficient credit take precedence over unknown state, so at_risk can include provider_timing_unknown. These two API reads retain phone_numbers:read and remain available during credit exhaustion. Provision/connect and other mutation responses, including old idempotency replays, keep their original shapes. contracts-parity.ts pins these shapes against contracts-api.

To discover risk without email, list every page by passing next_cursor as starting_after while has_more is true; inspect each phone.renewal, then retrieve the phone to refresh it. An active phone can already be at risk. evaluated_at is assessment time, separate from provider evidence freshness. For add_credit_or_enable_auto_topup, use existing scoped Billing controls or billing.packages.purchase / billing.autoTopup.update with billing:write and their documented inputs/idempotency. Confirm wallet production_blocked with billing:read; positive access recovery does not ensure next-fee coverage.

Remaining messaging allowance compares usage to the cap adjusted by the configured margin and sending-phone quality ratio. Linked Meta phones share portfolio usage but can report different estimates because their quality-adjusted caps differ. src/contracts-parity.test.ts pins this description. Quality-aware pacing adds no fields to public phone read types; the dashboard's additional pacing fields are BFF-only. Phone provision/connect separately return PhoneNumberMutationResponse with optional advisory warnings.

TyxterApiError.retryable exposes the optional error.retryable stop hint. For 402 credit_balance_exhausted, it is false: stop automatic retries until credit recovery. Older responses without the hint yield undefined, which is not permission to retry. The SDK makes one fetch per request and has no automatic retry loop; recovery does not clear other authorization or sending restrictions.

import { TyxterApiError } from '@tyxter/sdk-js';

try {
  await tyxter.messages.list();
} catch (error) {
  if (error instanceof TyxterApiError && error.retryable === false) {
    console.error('Pause automatic retries until credit recovery.', error.traceId);
  }
  throw error;
}
  • WhatsApp builders accept either the compatible string to value or a structured { countryCallingCode, nationalNumber } input and emit the exact public API shape. The string form follows the server's 8–15 ASCII digit legacy admission rule: optional leading +, ASCII spaces (U+0020), parentheses, dots, and hyphens only. The SDK does not normalize or rewrite it.

  • The SDK never logs API keys or webhook secrets.

  • Inbound WhatsApp image.id and audio.id values remain provider handles. Pass only the Tyxter media.asset_id from message reads or data.content.media.asset_id from message.received (mda_*) to MediaResource; createDownloadUrl() returns a fresh short-lived capability URL and never exposes Meta credentials or object-storage keys. For inbound audio, media.voice === true identifies a Meta voice note; false is ordinary audio and omission means the provider supplied no signal or the row predates this field.

  • Inbound audio transcription is opt-in per message. It runs asynchronously, does not delay playback-ready message.received, and can be observed by polling or through the typed MessageMediaTranscriptionWebhookEnvelope union: message.media_transcribed for success and message.media_transcription_failed for terminal failure. The failed event carries the same error_code as the transcript GET response and never carries speech, provider, model, or duration. Create replays only a pending or succeeded receipt. When a receipt has failed, call messages.retryTranscription(messageId, body, { idempotencyKey, traceId? }): the key is required, a same-key retry replays the stored 202, and the retry uses the same original inbound audio. It cannot replace an expired or structurally unavailable source. On transcription_retry_rate_limited, wait retryAfterMs (also available as body.retry_after_ms) and replay the same logical command with the same key; use a fresh key only for a distinct retry command.

  • Transcription accepts optional advisory prompt and keywords on create/retry. The server trims the prompt and rejects lengths above 1024 UTF-16 code units. Keywords allow at most 50 trimmed terms of 1–128 UTF-16 code units each; CR, LF, U+2028, U+2029 and angle brackets are rejected. Order, case and duplicates are preserved. Invalid fields return invalid_transcription_request. Create compares supplied normalized hints with the newest generation and returns transcription_hints_conflict if they differ. Retry inherits omitted hints; an empty prompt or keyword array clears that field. Same-key replay compares supplied normalized fields, distinguishing omission from clearing. Request hint fields are absent from transcript responses and webhooks and follow message retention and contact erasure.

  • The SDK owns its public TypeScript contract types so customer apps can install @tyxter/sdk-js without also depending on internal Tyxter packages. Those types must stay aligned with @tyxter/contracts-api and webhook types with @tyxter/contracts-webhooks before release.

  • Existing top-up reads may return provider: "promotion" and payment_method: "promotion" for a separately granted campaign bonus. This is additive response truth only: the SDK exposes no campaign resource and customer top-up creation remains limited to Pix or card.

  • PhoneNumberResponse keeps three distinct Meta-name facts: display_name remains customer-entered; verified_name is nullable Meta-verified display-name evidence; and name_review is null or the latest durable callback decision with requested_name, decision, reason, and reviewed_at. pending_name_review is separately null or { requested_name, status, observed_at }, the pending Graph health observation from a completed sweep. Its nullable status is an open provider vocabulary, and observed_at is the same freshness fact as meta_health_synced_at; null means no pending Graph observation, not approval. A successful complete sweep is authoritative and writes or clears that block; a completed callback can clear it immediately only when its requested name matches and its effective time is not older than observed_at. Other callbacks wait for the next sweep. Sandbox deterministically returns null, and the SDK does not fetch Meta while reading a phone resource.

  • projects.create forwards an optional idempotencyKey; same-key retries of the same parsed request replay the original create response, while a changed request receives idempotency_key_conflict. Project list and retrieve reads require the corresponding projects:read scope and never reveal foreign or archived projects.

  • groups.retrieveEligibility({ phone_number_id }) is the beta WhatsApp Business groups eligibility read (scope groups:read). It answers from Tyxter without reaching Meta: eligible for a sandbox number, and not_checked for a production number until an eligibility check has run for it, then the stored answer of the latest check. A phone number outside the key's environment is a 404 phone_number_not_found.

  • groups.createEligibilityCheck({ phone_number_id }, { idempotencyKey }) is the beta eligibility check (scope groups:write). It returns 202 with the stored answer and a new check_requested_at; a Tyxter worker then reads the facts from Meta (sandbox numbers answer eligible without a Meta call), and groups.retrieveEligibility shows the result once checked_at is later than check_requested_at. A check normally completes shortly after it is accepted, but no completion time is promised: if checked_at has not passed check_requested_at within your own timeout, that check did not complete, and a new check (with a new idempotency key) can be requested at any time. A malformed body is a 400 invalid_group_request.

  • groups.create({ phone_number_id, subject, description?, join_approval_mode? }, { idempotencyKey }) is the beta group create (scope groups:write). It returns 202 with the group as pending and its Tyxter id; a same-key retry returns the same group. A new group stays pending until a Tyxter worker resolves its create outcome; a create the worker finds it cannot send (for example the number was released meanwhile) ends failed with a failure.code saying why. A number whose stored eligibility answer is not_eligible is a 422 group_phone_number_not_eligible.

  • groups.list(query) and groups.retrieve(groupId) are the beta group reads (scope groups:read). retrieve takes the Tyxter group id, never Meta's, and percent-encodes it into the path, so a personal WhatsApp group invite link or app group id passed to it is a 422 personal_whatsapp_group_not_supported whose message links the page that explains the difference; any other unknown id is a 404 group_not_found. A raw HTTP call that pastes an invite link into the path unencoded matches no route and gets 404 route_not_found.

  • groups.delete(groupId, { idempotencyKey }) is the beta group delete (scope groups:write). It returns 202 with the group: an active group as deleting (a Tyxter worker then deletes it), a failed group as deleted, and a deleting or deleted group unchanged; a pending group is a 409 group_not_active. It keeps working when the groups feature family is disabled, and it encodes the id like retrieve.

  • groups.resetInviteLink(groupId, { idempotencyKey }) is the beta invite-link reset (scope groups:write). It returns 202 with the group still active and the link the reset will replace; while the group stays active, a Tyxter worker then stores the new invite_link (read it with retrieve), or records failure and keeps the old link. While the group is deleting, the reset's outcome leaves failure to the delete, and in production a reset not yet sent when the delete is accepted is not sent (reset again if the delete is refused). Any other status is a 409 group_not_active, the groups feature family gates it like create, and a retry with the same key never resets the link again. A new request while a reset worker is running returns 409 group_invite_link_reset_in_progress; retry after it finishes. A queued reset can still be superseded before a worker starts it.

  • Every group read carries participants (each wa_id, the participant's WhatsApp ID, and joined_at) and participant_count: the current members Tyxter recorded from the joins and removals Meta reports, the newest report about each participant deciding; a deleted group lists none. Reports are told apart by participant, action and Meta's time in whole seconds, so two joins (or two removals) in the same second count as one and a join and a removal in the same second go by arrival; a report Tyxter cannot match to a group, or that fails inside Tyxter, is not applied.

  • sandbox.groups.simulateParticipant(groupId, { wa_id, action }, { idempotencyKey }) is the beta sandbox participant simulation (scope groups:write, sandbox keys only; a production key is a 400 sandbox_group_participant_sandbox_only). It simulates the participant joining (join) or leaving (remove) an active group and returns 200 with the group. A join of a current member or a removal of a non-member changes nothing, so a retry never records the fact twice, with or without a key. A ninth joined participant is a 409 group_participant_limit_reached (Meta documents a cap of eight), and any status other than active is a 409 group_not_active. The groups feature family gates it like create. WhatsApp Business groups are business-owned groups created through Meta's Groups API, never personal WhatsApp groups (the /groups/not-personal-whatsapp-groups docs page explains the difference); what is offered today is listed on the /api-reference/groups docs page, and the /groups guide walks one group from eligibility to delete with these methods.

  • apiKeys.create accepts an optional project_id for an active sibling project in the source key's organization. Tyxter resolves the target environment of the source key's sandbox or production kind; it never accepts an environment id, crosses organizations or environment kinds, or lets a source key grant a scope it does not hold (including api_keys:admin). Omitting project_id preserves the source key's current project and environment.

  • ProviderConnectionPolicyWarningWebhookEnvelope is an additive, typed provider_connection.policy_warning contract. It is deliberately separate from suspension/reinstatement lifecycle handling: a warning does not state a connection status or block sends.

  • ProviderConnectionDisableScheduledWebhookEnvelope is an additive, typed provider_connection.disable_scheduled advisory contract. Its nullable waba_ban_date is schedule evidence, not a status change; sends continue until Meta actually disables the account.

  • ProviderConnectionResponse.waba_ban_date is the same nullable normalized schedule-date evidence on list, retrieve, and embedded payment-connection reads. It does not make a connected connection blocked; use status for current eligibility and treat null as an honestly missing/invalid Meta date.

  • ProviderConnectionResponse.last_policy_warning_type and last_policy_warning_at are nullable optional warning evidence on connection reads. The type is open because Meta owns its vocabulary; the fields do not imply a status or blocked-send change, and no outbound-count snapshot is in the SDK contract.

  • ProviderConnectionResponse.send_capability, send_block_codes, and send_capability_observed_at are the nullable optional compatibility view for the connection's primary WABA. waba_send_capabilities is the bounded, WABA-keyed view for sender WABAs observed through the connection (#807). null/absence is unobserved or unknown, never available; known blocks retain the three recognized codes in remediation priority. These are diagnostic fields only, never lifecycle status, and reactive checker attempt timestamps are not exposed through the SDK.

  • A WABA block records a terminal message error_code without changing the message/webhook shape: 141006 becomes meta_payment_method_required, 141008 becomes meta_waba_inactive, and 141011 becomes meta_messaging_permission_missing. A fresh stored block terminates before a call, so its provider_error is null. When a failed Meta send triggers reactive health confirmation, that terminal receipt retains the original bounded provider_error. Fix the WABA and wait for a positive capability observation before sending again rather than retrying the failed message unchanged.

  • Message error_code is intentionally an open string for additive delivery classifications. Meta 131049 is exposed as meta_marketing_frequency_cap; callers should treat it as recipient-scoped, avoid retrying that marketing message today, and leave the sending number active.

  • MessageReadSenderIdentity includes the output-only PhoneLessInboundSenderIdentity ({ type: "phone_e164", id: "" }) for a production inbound WhatsApp message whose customer phone Meta withheld. The empty id is unresolved and must not be used as a recipient or replaced with a synthetic identifier. Request identities and every recipient remain non-empty at runtime.

  • providerConnections.meta.exchangeOAuth() accepts a code-only body when a Meta browser relay omitted waba_id. Tyxter proceeds only if the token proves exactly one WhatsApp Business Account; otherwise callers receive meta_waba_missing and can retry the preserved pending setup after correcting Meta access. The SDK does not synthesize or expose candidate account IDs.

  • The webhook verifier performs constant-time HMAC comparison and rejects timestamps outside a 5-minute window by default to defeat replay attacks. Tests pin a vector matching the canonical sandbox message.received fixture in @tyxter/contracts-webhooks.

  • webhookEvents.listen is direct for sandbox keys. Production keys must first create a short-lived listen session, pass listen_session_id while polling, and disable the session when the validation run finishes. A create conflict exposes error.details.active_session.{id, expires_at, created_at}. Outside grace, only the owning key may reuse and poll that id. With 30 seconds or less remaining, only that owner may renew: renewal atomically disables the old session ID and the owner must switch polling to the returned 201 session ID. A sibling must not poll that id or loop POST, and waits for expiry or coordinates disable. This diagnostic lease does not promise delivery continuity.

  • The client targets fetch-API runtimes (Node 20+, browsers, Bun, Edge) and ships zero polyfills.

  • Media helpers create direct-upload sessions, list/delete assets, report storage usage, and can PUT bytes directly to the returned signed storage URL. Uploads default to single_use; pass lifecycle: "library" for media reused across messages or template-header broadcasts. The SDK never stores media bytes; Tyxter returns only metadata and asset_id references for message sends.

  • message.media.link is the URL media source. message.media.source is only for generated audio TTS, and SDK callers should prefer tyxter.whatsapp.sendAudioFromText for that shape.

  • sandbox.inboundMessages.create uses the sandbox inbound contract (from/to bare strings on the request). Retrieved messages return typed sender/recipient identities, expose settled attachments under media, and preserve the raw inbound body under payload for diagnostics.

  • sandbox.templates.setStatus is sandbox-only and forces terminal non-approved template states for local send-refusal tests.

Configuration

| Key | Default | Purpose | | ------------------------- | ------------------------ | ----------------------------------------------- | | apiKey (constructor) | — (required) | tx_live_* or tx_sandbox_* API key. | | baseUrl (constructor) | https://api.tyxter.com | Override for staging / self-hosted deployments. | | timeoutMs (constructor) | 30000 | Per-request timeout. |

Cross-module deps

None at runtime or package-install time. The SDK is intentionally self-contained for customer apps.

This package must not import from packages/modules/*, packages/platform/*, or packages/contracts/*. Contract drift is controlled by tests against the public contract package before release.

Test strategy

Unit tests under src/*.test.ts cover the verifier (signature, window, constant-time), client request construction, error narrowing, and compile-time parity with additive public API response fields.

pnpm --filter @tyxter/sdk-js test

License

MIT. The SDK is a thin, self-contained client over Tyxter's public API contracts — see LICENSE. The Tyxter platform itself is a separate commercial service and is not covered by this license.

payments.cancel(paymentId, { idempotencyKey?, traceId? }) queues hosted-checkout cancellation with an empty JSON object and returns the current PaymentResponse (HTTP 200). Poll payments.retrieve for provider-confirmed cancellation or paid status; a same-key replay returns the original receipt. Transparent Pix and unsupported rails return payment_cancellation_unsupported with error.feedback. The deterministic sandbox default supports the same queued operation. Refs #1225.