@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
tovalue 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.idandaudio.idvalues remain provider handles. Pass only the Tyxtermedia.asset_idfrom message reads ordata.content.media.asset_idfrommessage.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 === trueidentifies a Meta voice note;falseis 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 typedMessageMediaTranscriptionWebhookEnvelopeunion:message.media_transcribedfor success andmessage.media_transcription_failedfor terminal failure. The failed event carries the sameerror_codeas 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, callmessages.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. Ontranscription_retry_rate_limited, waitretryAfterMs(also available asbody.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
promptandkeywordson 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 returninvalid_transcription_request. Create compares supplied normalized hints with the newest generation and returnstranscription_hints_conflictif 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-jswithout also depending on internal Tyxter packages. Those types must stay aligned with@tyxter/contracts-apiand webhook types with@tyxter/contracts-webhooksbefore release.Existing top-up reads may return
provider: "promotion"andpayment_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.PhoneNumberResponsekeeps three distinct Meta-name facts:display_nameremains customer-entered;verified_nameis nullable Meta-verified display-name evidence; andname_reviewis null or the latest durable callback decision withrequested_name,decision,reason, andreviewed_at.pending_name_reviewis separately null or{ requested_name, status, observed_at }, the pending Graph health observation from a completed sweep. Its nullablestatusis an open provider vocabulary, andobserved_atis the same freshness fact asmeta_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 thanobserved_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.createforwards an optionalidempotencyKey; same-key retries of the same parsed request replay the original create response, while a changed request receivesidempotency_key_conflict. Project list and retrieve reads require the correspondingprojects:readscope and never reveal foreign or archived projects.groups.retrieveEligibility({ phone_number_id })is the beta WhatsApp Business groups eligibility read (scopegroups:read). It answers from Tyxter without reaching Meta:eligiblefor a sandbox number, andnot_checkedfor 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 a404 phone_number_not_found.groups.createEligibilityCheck({ phone_number_id }, { idempotencyKey })is the beta eligibility check (scopegroups:write). It returns202with the stored answer and a newcheck_requested_at; a Tyxter worker then reads the facts from Meta (sandbox numbers answereligiblewithout a Meta call), andgroups.retrieveEligibilityshows the result oncechecked_atis later thancheck_requested_at. A check normally completes shortly after it is accepted, but no completion time is promised: ifchecked_athas not passedcheck_requested_atwithin 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 a400 invalid_group_request.groups.create({ phone_number_id, subject, description?, join_approval_mode? }, { idempotencyKey })is the beta group create (scopegroups:write). It returns202with the group aspendingand its Tyxterid; a same-key retry returns the same group. A new group stayspendinguntil a Tyxter worker resolves its create outcome; a create the worker finds it cannot send (for example the number was released meanwhile) endsfailedwith afailure.codesaying why. A number whose stored eligibility answer isnot_eligibleis a422 group_phone_number_not_eligible.groups.list(query)andgroups.retrieve(groupId)are the beta group reads (scopegroups:read).retrievetakes 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 a422 personal_whatsapp_group_not_supportedwhose message links the page that explains the difference; any other unknown id is a404 group_not_found. A raw HTTP call that pastes an invite link into the path unencoded matches no route and gets404 route_not_found.groups.delete(groupId, { idempotencyKey })is the beta group delete (scopegroups:write). It returns202with the group: anactivegroup asdeleting(a Tyxter worker then deletes it), afailedgroup asdeleted, and adeletingordeletedgroup unchanged; apendinggroup is a409 group_not_active. It keeps working when thegroupsfeature family is disabled, and it encodes the id likeretrieve.groups.resetInviteLink(groupId, { idempotencyKey })is the beta invite-link reset (scopegroups:write). It returns202with the group stillactiveand the link the reset will replace; while the group staysactive, a Tyxter worker then stores the newinvite_link(read it withretrieve), or recordsfailureand keeps the old link. While the group isdeleting, the reset's outcome leavesfailureto 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 a409 group_not_active, thegroupsfeature family gates it likecreate, and a retry with the same key never resets the link again. A new request while a reset worker is running returns409 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(eachwa_id, the participant's WhatsApp ID, andjoined_at) andparticipant_count: the current members Tyxter recorded from the joins and removals Meta reports, the newest report about each participant deciding; adeletedgroup 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 (scopegroups:write, sandbox keys only; a production key is a400 sandbox_group_participant_sandbox_only). It simulates the participant joining (join) or leaving (remove) anactivegroup and returns200with 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 a409 group_participant_limit_reached(Meta documents a cap of eight), and any status other thanactiveis a409 group_not_active. Thegroupsfeature family gates it likecreate. WhatsApp Business groups are business-owned groups created through Meta's Groups API, never personal WhatsApp groups (the/groups/not-personal-whatsapp-groupsdocs page explains the difference); what is offered today is listed on the/api-reference/groupsdocs page, and the/groupsguide walks one group from eligibility to delete with these methods.apiKeys.createaccepts an optionalproject_idfor 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 (includingapi_keys:admin). Omittingproject_idpreserves the source key's current project and environment.ProviderConnectionPolicyWarningWebhookEnvelopeis an additive, typedprovider_connection.policy_warningcontract. It is deliberately separate from suspension/reinstatement lifecycle handling: a warning does not state a connection status or block sends.ProviderConnectionDisableScheduledWebhookEnvelopeis an additive, typedprovider_connection.disable_scheduledadvisory contract. Its nullablewaba_ban_dateis schedule evidence, not a status change; sends continue until Meta actually disables the account.ProviderConnectionResponse.waba_ban_dateis the same nullable normalized schedule-date evidence on list, retrieve, and embedded payment-connection reads. It does not make a connected connection blocked; usestatusfor current eligibility and treat null as an honestly missing/invalid Meta date.ProviderConnectionResponse.last_policy_warning_typeandlast_policy_warning_atare 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, andsend_capability_observed_atare the nullable optional compatibility view for the connection's primary WABA.waba_send_capabilitiesis 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_codewithout changing the message/webhook shape:141006becomesmeta_payment_method_required,141008becomesmeta_waba_inactive, and141011becomesmeta_messaging_permission_missing. A fresh stored block terminates before a call, so itsprovider_erroris null. When a failed Meta send triggers reactive health confirmation, that terminal receipt retains the original boundedprovider_error. Fix the WABA and wait for a positive capability observation before sending again rather than retrying the failed message unchanged.Message
error_codeis intentionally an openstringfor additive delivery classifications. Meta131049is exposed asmeta_marketing_frequency_cap; callers should treat it as recipient-scoped, avoid retrying that marketing message today, and leave the sending number active.MessageReadSenderIdentityincludes the output-onlyPhoneLessInboundSenderIdentity({ 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 omittedwaba_id. Tyxter proceeds only if the token proves exactly one WhatsApp Business Account; otherwise callers receivemeta_waba_missingand 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.receivedfixture in@tyxter/contracts-webhooks.webhookEvents.listenis direct for sandbox keys. Production keys must first create a short-lived listen session, passlisten_session_idwhile polling, and disable the session when the validation run finishes. A create conflict exposeserror.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; passlifecycle: "library"for media reused across messages or template-header broadcasts. The SDK never stores media bytes; Tyxter returns only metadata andasset_idreferences for message sends.message.media.linkis the URL media source.message.media.sourceis only for generated audio TTS, and SDK callers should prefertyxter.whatsapp.sendAudioFromTextfor that shape.sandbox.inboundMessages.createuses the sandbox inbound contract (from/tobare strings on the request). Retrieved messages return typedsender/recipientidentities, expose settled attachments undermedia, and preserve the raw inbound body underpayloadfor diagnostics.sandbox.templates.setStatusis 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 testLicense
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.
