Python Library v3.00

tnzapi-python

tnzapi is TNZ's official Python helper library for sending SMS, Email, TTS, Voice, Fax, WhatsApp, RCS, and Workflow messages, and for managing your Addressbook and OptOut list, distributed via PyPI. Install it and skip writing your own HTTP client, request signing, and response parsing against the TNZ REST API. Send your first message in a few lines of Python.

What you can do

Send & receive messages:

  • Workflow: trigger multi-channel, no-code automation templates
  • SMS: two-way text messaging
  • Email: plain-text or HTML, with attachments
  • TTS: text-to-speech voice calls
  • Voice: pre-recorded audio calls
  • Fax: document delivery
  • WhatsApp: template messaging
  • RCS: Rich Communication Services

Manage your account:

  • Addressbook: contacts, groups, and relationships
  • OptOut: manage opted-out destinations
  • Webhooks: receive inbound delivery/reply events on your own server
  • Reports: poll message status, SMS replies, and received SMS
  • Actions: reschedule, abort, resubmit, or adjust pacing on an in-flight job, for any channel, from one place

Supported Python versions

This library supports the following Python versions:

  • Python 3.9
  • Python 3.10
  • Python 3.11
  • Python 3.12
  • Python 3.13
  • Python 3.14

Installation

Install the package from PyPI:

pip install tnzapi

Alternatively, you can browse the source code on GitHub. The repository includes samples/, covering Messaging, Addressbook, OptOut, Actions, Reports, and Webhooks usage, and demo/, a full API and web demo project (demo/api plus a shared demo/web frontend).

Continue to Getting Started to create your first TNZAPI client.

Getting Started

Create a TNZAPI client to start using the TNZ API.

Register an Account

If you don't already have a TNZ account, sign up here before continuing.

API Credentials

tnzapi authenticates every request with a JWT Auth Token.

Export your Auth Token

  1. Login to the TNZ Dashboard
  2. Navigate to 'Users'
  3. Create a new user or select an existing one
  4. Enable API access (if it's not already enabled)
  5. Click on the 'API' tab
  6. Enable 'Auth Token' and create a new Auth Token
  7. Click the 'Copy' button to copy it to your clipboard

Pass it directly to the TNZAPI constructor:

from tnzapi import TNZAPI

client = TNZAPI(AuthToken="[Your Auth Token]")

Unlike some of TNZ's other SDKs, tnzapi doesn't have a separate "user" object you construct first: TNZAPI(**kwargs) accepts AuthToken (and, optionally, BaseURL) directly, and hands them to each facade (.Messaging, .Reports, .Actions, .Addressbook, .OptOut) the first time you access it. There's only the one construction pattern.

Refresh or invalidate your Auth Token

  1. Login to the TNZ Dashboard
  2. Navigate to 'Users'
  3. Click on your API user
  4. Click on the 'API' tab
  5. Click the refresh/recycle button in the Auth Token section
  6. Update your applications to use the new Auth Token

Refreshing invalidates the old token immediately. Any application still using it will start failing authentication until it's updated with the new one.

Every response exposes Result and ErrorMessage: check response.Result == "Success" before reading other response fields; on failure, response.ErrorMessage is a list[str] of human-readable error messages (always a list, even when empty). Result is a plain string, not an enum type, and takes one of four values: "Success", "Failed", "Unauthorized", "RecordNotFound".

if response.Result == "Success":
    print(f"Success - MessageID: {response.MessageID}")
else:
    for error in response.ErrorMessage:
        print(f"- Error={error}")

If you want to type-hint against a response's class (e.g. in a function signature), import it from tnzapi.models rather than building your own reference to it:

from tnzapi.models import SMSResponse

def handle(result: SMSResponse) -> None:
    ...

tnzapi.models re-exports every response type across .Messaging/.Reports/.Actions/.Addressbook/.OptOut under its plain name (SMSResponse, ContactResponse, OptOutListResponse, and so on). You still never construct these yourself: build requests with kwargs to SendMessage(...)/Create(...)/Set(...), and read what comes back.

A required ID parameter passed directly (MessageID to Status(...), ContactID to Detail(...), and so on) raises ValueError if it's missing or an empty string, rather than silently sending a request built around None. On Update(...)/Delete(...) calls in Addressbook and OptOut, that same ID parameter also accepts the response object a prior call returned, extracting the ID from it automatically; see those sections for examples. Messaging channels don't have this second form: pass a plain MessageID string to Status(...)/Reschedule(...)/Abort(...)/Resubmit(...)/Pacing(...), not the response SendMessage(...) returned.

Common Response Values

The SDK represents these as plain strings rather than defining enum types for them. Every channel's Status(...) and Action (Reschedule/Abort/Resubmit/Pacing) results share these values, referenced from each channel's own Response tables below rather than repeated in each one:

FieldValuesUsed on
JobStatus"Pending", "Delayed", "Completed", "CreditHold", "Unknown"The job-level JobStatus/Status field on every Status and Action response.
Status (per-recipient)"Success", "Failed", "Pending"The per-recipient Status field on each entry in a response's recipient list.
Type (per-recipient)"SMS", "Email", "Voice", "Fax", "WhatsApp", "RCS"The per-recipient Type field on each entry in a response's recipient list. TTS and Voice calls both report as "Voice": there's no separate TTS value.

List/nested response fields such as Recipients or Messages are plain dict objects, not typed classes, on every channel except one: access their fields with recipient["Status"] or recipient.get("Status"), not attribute access (recipient.Status will raise AttributeError). SMS is the exception: its Status(...)/Reply(...) Recipients (and their nested SMSReplies) are real dataclass instances, supporting both dict-style and attribute access; see SMS's own Response section for details. Every channel's own Response tables further down describe these dict (or, for SMS, dataclass) shapes field by field.

for recipient in response.Recipients:
    print(f"{recipient.get('Destination')}: {recipient.get('Status')}")

The Destination Model

Destination is one shared class used by every messaging channel (SMS, Email, Fax, TTS, Voice, WhatsApp, RCS, and Workflow), mirroring the Destinations array shape from the REST API docs field-for-field. Every channel accepts every field below, but the fields that actually matter differ by channel: ToNumber is effectively the universal default destination field for every channel except Email: Recipient and each channel's own "natural" field (MobilePhone for SMS/WhatsApp/RCS, MainPhone for TTS/Voice, FaxNumber for Fax) are treated as equivalent aliases of it for that channel. Email is the one channel with a genuinely different primary field, EmailAddress. Workflow is the one exception where ToNumber, MainPhone, and EmailAddress can all be set simultaneously and meaningfully, for omni-channel routing. See each channel's own Destination Fields table for the specifics.

Like the rest of this SDK, Destination's keyword arguments are PascalCase, matching the wire field names, not idiomatic Python snake_case. Pass a Destination instance anywhere a destination is expected: in a channel's Destinations=[...] list, or to AddDestination(...) on the builder. A plain string still works too, but its meaning depends on the class: the shorthand Destination("...") constructor always sets the generic Recipient field and lets the server infer the address type from channel context (the same behaviour on every channel). This is different from passing a raw string straight to AddDestination(...)/Destinations=[...] without wrapping it in Destination, where a channel-specific dict mapping applies instead (e.g. Email maps a bare string differently to everything else). Wrapping it in Destination normalises that away.

FieldTypeDescription
RecipientstrGeneric single-value shorthand; the server infers which address type it is from the channel you're sending on. Set by the shorthand single-string constructor.
ToNumberstrThe universal default destination field: every channel except Email treats it as the primary phone/address field. Also the destination for Workflow's toNumber shortcut.
MobilePhonestrSMS/WhatsApp/RCS's own natural field; treated as equivalent to ToNumber on those channels.
MainPhonestrTTS/Voice's own natural field; treated as equivalent to ToNumber on those channels. Also the destination for Workflow's mainPhone shortcut, a separate wire field from ToNumber.
EmailAddressstrEmail's primary field: the one genuine exception to ToNumber being the default. Can also be set on Workflow, alongside ToNumber/MainPhone, for omni-channel Workflow Templates.
FaxNumberstrFax's own natural field; treated as equivalent to ToNumber on that channel.
CompanystrPersonalisation token [[Company]].
AttentionstrPersonalisation token [[Attention]].
FirstNamestrPersonalisation token [[FirstName]].
LastNamestrPersonalisation token [[LastName]].
Custom1Custom9strPersonalisation tokens [[Custom1]][[Custom9]].
ContactIDstrSend to this addressbook contact instead of a raw destination.
GroupIDstrSend to every member of this addressbook group.
GroupCodestrAlternative group lookup by code.

A Destination instance validates field names at construction time: Destination(ToNumber="...", Bogus="...") raises TypeError, same as passing any unexpected keyword argument to a Python dataclass. An unknown key in a raw dict passed as a destination is rejected the same way: ValueError for AddDestination(...)/Set(Destinations=[...]), but Result="Failed" with an ErrorMessage (not an exception) when the unknown key reaches SendMessage(...) directly, consistent with how every other unknown-field case behaves for SendMessage(...) versus Set().

Shorthand constructor

Pass a single positional string to set only the generic Recipient field:

from tnzapi.core.destination import Destination

destination = Destination("+64211111111")

Full keyword-argument constructor

Set any combination of fields at once, including a specific address field like ToNumber directly instead of the generic Recipient shorthand:

from tnzapi.core.destination import Destination

destination = Destination(
    ToNumber="+64211111111",
    Company="Example Company",
    Attention="Accounts Payable",
    FirstName="Alice",
    LastName="Smith",
    Custom1="Invoice #1234"
)

Environment Variables

tnzapi can also be configured via environment variables (useful for CI, containers, or keeping credentials out of source control):

VariablePurposeDefault
TNZ_AUTH_TOKENFallback Auth Token used whenever AuthToken isn't passed explicitly. An explicit AuthToken always takes precedence.(none: raises ValueError("AuthToken is required") if still empty)
TNZ_API_URLOverrides the API base URL tnzapi sends requests to.https://api.tnz.co.nz/api/v3.00
TNZ_ALLOW_INSECURE_HTTPMust be the exact lowercase string "true" to allow requests over plain HTTP (useful when pointing TNZ_API_URL at a local/staging server without TLS). Any other value is treated as unset. By default tnzapi refuses to send the Bearer token over anything but HTTPS.(unset, HTTPS enforced)
from tnzapi import TNZAPI

# Picks up TNZ_AUTH_TOKEN automatically since no AuthToken is set explicitly
client = TNZAPI()

Setting environment variables

Windows

Current PowerShell session only:

$env:TNZ_AUTH_TOKEN = "your-auth-token"

Current Command Prompt (cmd.exe) session only:

set TNZ_AUTH_TOKEN=your-auth-token

Permanently, for your user account (visible in new terminals/processes after you open one):

[System.Environment]::SetEnvironmentVariable("TNZ_AUTH_TOKEN", "your-auth-token", "User")

Or via the GUI: Windows Settings → search "Environment Variables" → Edit environment variables for your account.

Linux

Current shell session only:

export TNZ_AUTH_TOKEN="your-auth-token"

Permanently, for your user account: add the export line above to ~/.bashrc (bash), ~/.zshrc (zsh), or ~/.profile, then start a new shell (or source the file).

For a systemd service, set it in the unit file instead:

[Service]
Environment="TNZ_AUTH_TOKEN=your-auth-token"
macOS

Current shell session only:

export TNZ_AUTH_TOKEN="your-auth-token"

Permanently, for your user account: add the export line above to ~/.zshrc (the default shell on modern macOS) or ~/.bash_profile, then start a new terminal (or source the file).

Same pattern applies to TNZ_API_URL and TNZ_ALLOW_INSECURE_HTTP. Just swap the variable name. Note that tnzapi reads OS-level environment variables only; it does not auto-load a .env file.

Actions & Reports

Every channel that supports an action exposes it two ways: directly on the channel itself (shown in that channel's own section, e.g. client.Messaging.SMS.Reschedule(...)), and through the channel-agnostic Actions facade, which takes a Channel= string instead:

response = client.Actions.Reschedule.SendRequest(
    Channel="sms",
    MessageID="ID123456",
    SendTime="2026-08-01T09:00"
)

client.Actions dispatches Abort, Reschedule, Resubmit, and Pacing requests to whichever channel you name; not every channel supports every action:

ChannelRescheduleAbortResubmitPacing
SMS
Email
TTS
Voice
Fax
WhatsApp
RCS
Workflow

Delivery reports work the same way, through a dedicated Reports facade: client.Reports.Status.Poll(...), client.Reports.SMSReceived.Poll(...), and client.Reports.SMSReply.Poll(...).

Workflow

Workflow triggers a pre-configured messaging strategy with a single API call. Build the strategy once in the Dashboard's drag-and-drop builder, then trigger it from your application using this library, with no code changes needed when the strategy itself changes. A Workflow Template can chain channels together: start with an SMS, fail over to an Email if there's no reply, then trigger a TTS call if the email goes unread. It can define fallback channels so a message keeps trying until it gets through, and fire custom webhooks at any stage.

Workflow is the only messaging module with no Message/TemplateID text content, no Status, no Received, and no client.Actions entry: just AddDestination and SendMessage (plus the inherited Set(...) builder shared by every messaging module).

Quick Example

response = client.Messaging.Workflow.SendMessage(
    WorkflowTemplateID="a1b2c3d4-e5f6-7890-1234-567890abcdef",
    Destination="+64211111111"
)

if response.Result == "Success":
    print(f"Success - MessageID: {response.MessageID}")

Parameters

ParameterTypeRequiredDescription
WorkflowTemplateIDstrYesID of the Workflow Template to trigger (built in the Dashboard).
Destinationslist[dict]No*One or more destinations. See Destination fields below.
DestinationstrNo*Single destination shorthand, e.g. "+64211111111".
ToNumberstrNo*Alternative single-destination field (phone number).
MainPhonestrNoAlternative single-destination field for a secondary/main phone number, distinct from ToNumber. Unlike ToNumber, setting only MainPhone does not satisfy the destination-presence check below. There is no equivalent top-level EmailAddress field. See the callout above.
ContactIDstrNoSingle addressbook contact to send to (alternative/addition to Destinations).
GroupIDstrNoSingle addressbook group to send to (alternative/addition to Destinations).
ReferencestrNoYour internal reference, returned in reports and webhooks.
SendTimestrNoSchedule delivery. Combine with Timezone.
TimezonestrNoTimezone name for SendTime, e.g. "Pacific/Auckland".
SubAccountstrNoSub-account code for billing separation.
DepartmentstrNoDepartment code.
MessageIDstrNoSupply your own message ID (otherwise auto-generated).
WebhookCallbackURLstrNoURL for delivery status callbacks.
WebhookCallbackFormatstrNoCallback format, e.g. "JSON".
NotificationTypestrNoNotification delivery mode.

*Set directly via Destinations, via SendMessage(...)'s own Destination/ToNumber keyword arguments, or via chained AddDestination(...) calls on client.Messaging.Workflow.Set(...), including AddDestination(ContactID=...)/AddDestination(GroupID=...), which append to Destinations. SendMessage(...) checks this client-side, before even checking WorkflowTemplateID: if none of Destinations/Destination/ToNumber is set, it immediately returns Result="Failed" with ErrorMessage=["Missing required field: Destinations, Destination, or ToNumber"]. Passing MainPhone, ContactID, or GroupID alone as a top-level SendMessage(...) keyword argument does not satisfy this check; route those through Destinations or AddDestination(...) instead.

Unlike every other channel, Workflow's parameters have no Files (attachments), no ReportTo, and no Mode field at all: there is no "Test" send mode for Workflow, every send is live.

Destination Fields (Destination)

Destination is shared across every channel; see the Destination Model reference in Getting Started for its full field list and construction patterns. Workflow is the one channel where ToNumber, MainPhone, FaxNumber, and EmailAddress can all be set on the same Destination at once, letting one Workflow Template route to whichever channel(s) it's configured for. Unknown keys are validated, not silently ignored: an unrecognised key in a destination dict or Destination object raises ValueError from Set()/AddDestination(), and causes SendMessage() to return Result="Failed".

FieldDescription
RecipientGeneric single-value shorthand; the server infers which address type it is from the channel(s) the Workflow Template routes to.
ToNumberPhone destination, e.g. "+64211111111". Can be set alongside MainPhone and EmailAddress on the same Destination for omni-channel Workflow Templates.
MobilePhoneAlternative phone destination field alongside ToNumber.
MainPhoneA separate wire field from ToNumber, letting a Workflow Template distinguish the two. Can be set alongside ToNumber and EmailAddress for omni-channel routing.
EmailAddressEmail destination. Can be set alongside ToNumber/MainPhone on the same Destination for omni-channel Workflow Templates. This is the only way to route a Workflow send to email: there is no top-level EmailAddress kwarg.
FaxNumberFax destination, read if the Workflow Template routes to a Fax channel. Can be set alongside ToNumber, MainPhone, and EmailAddress on the same Destination, following the same omni-channel pattern as EmailAddress.
CompanyPersonalisation token [[Company]].
AttentionPersonalisation token [[Attention]].
FirstNamePersonalisation token [[FirstName]].
LastNamePersonalisation token [[LastName]].
Custom1Custom9Personalisation tokens [[Custom1]][[Custom9]], passed through to whichever channel(s) the Workflow Template actually uses.
ContactIDAddressbook contact reference. Sends to that contact instead of raw addresses.
GroupIDAddressbook group reference. Sends to all members of that group.
GroupCodeAlternative group lookup by code.

If a destination isn't a known ContactID/GroupID, the API automatically creates (or updates) an Addressbook contact from whichever address/personalisation fields you supply alongside it.

Code Samples

Single destination shorthand

Trigger a Workflow Template for one phone destination with the fewest arguments possible.

response = client.Messaging.Workflow.SendMessage(
    WorkflowTemplateID="a1b2c3d4-e5f6-7890-1234-567890abcdef",
    Destination="+64211111111"
)

Via the builder

Chain Set(...) and AddDestination(...) when you need more control than the flat SendMessage(...) call above, e.g. scheduling.

response = (
    client.Messaging.Workflow.Set(
        WorkflowTemplateID="a1b2c3d4-e5f6-7890-1234-567890abcdef"
    )
    .AddDestination("+64211111111")
    .SendMessage()
)

Multiple recipients, via AddDestinations

AddDestinations([...]) adds several destinations in one call on the builder - each item can be a bare string, a dict, or a typed Destination, mixed freely in the same list. Unlike AddDestination(...) (singular), which only ever adds one destination per call and raises TypeError on a list, this is the chainable way to add several at once.

response = (
    client.Messaging.Workflow.Set(
        WorkflowTemplateID="a1b2c3d4-e5f6-7890-1234-567890abcdef"
    )
    .AddDestinations(["+64211111111", "+64222222222"])
    .SendMessage()
)

Bulk send with per-destination personalisation, via AddDestinations

Personalisation fields on each destination are passed through to whichever channel(s) the Workflow Template actually routes to.

from tnzapi.core.destination import Destination

response = (
    client.Messaging.Workflow.Set(
        WorkflowTemplateID="a1b2c3d4-e5f6-7890-1234-567890abcdef"
    )
    .AddDestinations([
        Destination(
            ToNumber="+64211111111",
            FirstName="Alice",
            Company="Example Company"
        ),
        Destination(
            ToNumber="+64222222222",
            FirstName="Bob",
            Company="Example Company"
        ),
    ])
    .SendMessage()
)

Destination with every channel address set

Build a Destination with ToNumber, MainPhone, and EmailAddress all set at once, plus personalisation fields. This is the pattern that makes Workflow genuinely omni-channel, and the only way to reach an email address on a Workflow send, since there's no top-level EmailAddress kwarg (see the callout above). The Template picks whichever destination(s) it needs from the same Destination.

from tnzapi.core.destination import Destination

destination = Destination(
    ToNumber="+64211111111",
    MainPhone="+6491112222",
    EmailAddress="test@example.com",
    FirstName="Alice",
    Custom1="Account #4432"
)

response = (
    client.Messaging.Workflow.Set(
        WorkflowTemplateID="a1b2c3d4-e5f6-7890-1234-567890abcdef"
    )
    .AddDestination(destination)
    .SendMessage()
)

Multiple destinations

Trigger the same Workflow Template for more than one destination in a single request, personalising each with its own Destination fields.

destinations = [
    Destination(
        ToNumber="+64211111111",
        FirstName="Alice",
        Company="Example Company",
        Custom1="Account #4432"
    ),
    Destination(
        ToNumber="+64222222222",
        FirstName="Bob",
        Company="Example Company",
        Custom1="Account #7788"
    ),
]

response = client.Messaging.Workflow.SendMessage(
    WorkflowTemplateID="a1b2c3d4-e5f6-7890-1234-567890abcdef",
    Destinations=destinations
)

Addressbook destination

Trigger a Workflow Template for an existing addressbook contact or group instead of a raw address.

response = (
    client.Messaging.Workflow
    .Set(WorkflowTemplateID="a1b2c3d4-e5f6-7890-1234-567890abcdef")
    .AddDestination(ContactID="[Contact ID]")
    .AddDestination(GroupID="[Group ID]")
    .SendMessage()
)

Scheduled send

Delay the trigger to a specific SendTime.

response = client.Messaging.Workflow.SendMessage(
    WorkflowTemplateID="a1b2c3d4-e5f6-7890-1234-567890abcdef",
    Destination="+64211111111",
    SendTime="2026-08-01T09:00:00",
    Timezone="Pacific/Auckland"
)

Response

Workflow has no Status, Received, or Action methods, and unlike WhatsApp/RCS there are no list-shaped fields here at all: SendMessage(...) returns exactly one response shape.

SendMessage(...) response

FieldTypeDescription
ResultstrSee Getting Started.
MessageIDstrThe ID of the Workflow run you just triggered.
ErrorMessagelist[str]See Getting Started.

SMS

Send text messages to one or more destinations via the TNZ REST API, with optional Voice/RCS/WhatsApp fallback. SMS supports two-way messaging: track delivery status in real time (see Poll for Status below), and receive replies from recipients (see Poll for Inbound SMS below, or the Webhooks section for a push-based alternative).

Message Body Tokens

Beyond the personalisation tokens in the Destination Fields table below, your Message body supports these special inline tokens:

  • [[Link:https://example.com/page]]: automatically shortens the URL and tracks click-through engagement (URL Shortener).
  • [[File1]]: inserts a link to the first file attached via AddAttachment(...)/Files ([[File2]], [[File3]], etc. for additional attachments) (File Link).
  • [[REPLY]]: inserts a tappable link recipients can use to reply, even from devices without native SMS reply support (Reply Link).
  • [[STOP]]: inserts an unsubscribe link that automatically opts the recipient out of future messages (Unsubscribe).
from tnzapi.core.send_mode import SendMode

response = client.Messaging.SMS.SendMessage(
    Destination="+64211111111",
    Message="View your invoice at [[Link:https://example.com/invoice/123]] or reply [[REPLY]]. Text [[STOP]] to opt out.",
    Mode=SendMode.Test  # Test mode
)

Quick Example

from tnzapi import TNZAPI
from tnzapi.core.send_mode import SendMode

client = TNZAPI(AuthToken="[Your Auth Token]")

response = client.Messaging.SMS.SendMessage(
    Destination="+64211111111",
    Message="Test SMS",
    Mode=SendMode.Test  # Test mode
)

if response.Result == "Success":
    print(f"Success - MessageID: {response.MessageID}")

Parameters

ParameterTypeRequiredDescription
MessagestrYes*Message body. Supports personalisation tokens [[FirstName]], [[Custom1]], etc.
TemplateIDstrYes*Pre-configured message template ID (alternative to Message).
Destinationstr | DestinationYes†Single destination shorthand: a plain string, e.g. "+64211111111", or a Destination instance for a single personalised send without building a Destinations list. See Destination fields below.
ToNumberstrYes†Alternative single-destination phone field, equivalent to Destination.
Destinationslist[Destination]Yes†One or more destinations. See Destination fields below.
ContactIDstrNoSingle addressbook contact to send to (alternative/addition to Destinations).
GroupIDstrNoSingle addressbook group to send to (alternative/addition to Destinations).
MessageIDstrNoSupply your own message ID (otherwise auto-generated).
ReferencestrNoYour internal reference, returned in reports and webhooks.
NotificationTypestrNoNotification delivery mode.
WebhookCallbackURLstrNoURL for delivery status callbacks.
WebhookCallbackFormatstrNoCallback format: "JSON"/"XML"/"POST"/"GET".
ReportTostrNoEmail address to receive delivery reports.
SendTimestrNoSchedule delivery. Combine with Timezone.
TimezonestrNoTimezone name for SendTime (e.g. "New Zealand", "Pacific/Auckland").
SubAccountstrNoSub-account code for billing separation.
DepartmentstrNoDepartment code.
FromNumberstrNoSender ID shown on the recipient's device.
SMSEmailReplystrNoEmail address to receive SMS replies.
CharacterConversionboolNoConvert characters outside the GSM character set automatically. Default False.
FallbackModestr | list[str]NoFallback channel(s) if SMS fails, tried in the order given, e.g. "Voice" or ["Voice", "WhatsApp"]. A list is joined into TNZ's wire format automatically ("Voice, WAPP") - "WhatsApp" is translated to its real wire token "WAPP" either way.
SMSCustomPageIDstrNoCustom landing page ID used for [[REPLY]] links.
ModestrNoSet "Test" to validate without sending. Default "Live".
Fileslist[dict]NoFiles sent via MessageLink, added with AddAttachment(Name, Data). NZ carriers don't support true MMS; reference the file in your message text with [[File1]] instead, and the recipient gets a link to it.

*Either Message or TemplateID must be provided.
†Set via Destination, ToNumber, or Destinations, via ContactID/GroupID, or via AddDestination(...) chained on Set(...).

Two calling styles: every field above can be passed directly as a keyword argument to SendMessage(...) in one call, or accumulated first via client.Messaging.SMS.Set(...)/AddDestination(...)/AddAttachment(...) chained calls and finished with a no-argument SendMessage(). Both styles accept the exact same field set: there's no reduced subset like some other TNZ SDKs. Set(...) raises ValueError on an unknown field name; SendMessage(...) instead returns Result="Failed".

Destination Fields (Destination)

Destination is shared across every channel; see the Destination Model reference in Getting Started for its full field list and construction patterns. SMS's primary destination field is ToNumber; Recipient and MobilePhone have the same effect. A Destination instance validates field names at construction time, and an unknown key in a destination dict is rejected the same way. See the Destination Model reference for details.

FieldDescription
ToNumberDestination phone number, e.g. "+64211111111".
RecipientGeneric destination address, sent as-is regardless of channel (same effect as ToNumber here). Set by Destination("+64211111111") or AddDestination("+64211111111").
MobilePhoneAlternative phone field, same effect as ToNumber for this channel.
MainPhoneAccepted but not read by SMS (used by TTS/Voice).
EmailAddressAccepted but not read by SMS (used by Email).
FaxNumberAccepted but not read by SMS (used by Fax).
FirstName / LastName / Company / AttentionPersonalisation tokens, e.g. [[FirstName]].
Custom1Custom9Arbitrary per-recipient personalisation values, [[Custom1]][[Custom9]].
ContactIDAddressbook contact reference: sends to that contact instead of a raw number.
GroupIDAddressbook group reference: sends to all members of that group.
GroupCodeAlternative group lookup by code (instead of GroupID).

Destination("+64211111111") sets only the generic Recipient field; the server infers it's a phone number from the SMS channel context. Pass keyword arguments (Destination(ToNumber="...", FirstName="...")) when you need a specific address field or personalisation set explicitly.

Code Samples

Single destination shorthand

The simplest way to send: pass a message and destination directly to SendMessage(...).

response = client.Messaging.SMS.SendMessage(
    Destination="+64211111111",
    Message="Office closed today.",
    Mode=SendMode.Test  # Test mode
)

Multiple destinations, via the builder

Use Set(...)/AddDestination(...) when you need multiple destinations, or want to mix raw numbers with existing Addressbook contacts and groups.

response = (
    client.Messaging.SMS
    .Set(
        Message="Test SMS",
        Reference="Test SMS - Builder sample",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestination("+64211111111")
    .AddDestination("+64222222222")
    # Addressbook lookups by ContactID/GroupID
    .AddDestination(ContactID="CCCCCCCC-BBBB-BBBB-CCCC-DDDDDDDDDDDD")
    .AddDestination(GroupID="GGGGGGGG-BBBB-BBBB-CCCC-DDDDDDDDDDDD")
    .SendMessage()
)

Multiple recipients, via AddDestinations

AddDestinations([...]) adds several destinations in one call on the builder - each item can be a bare string, a dict, or a typed Destination, mixed freely in the same list. Unlike AddDestination(...) (singular), which only ever adds one destination per call and raises TypeError on a list, this is the chainable way to add several at once.

response = (
    client.Messaging.SMS
    .Set(
        Message="Hi [[FirstName]]!",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestinations([
        "+64211111111",
        {"ToNumber": "+64222222222", "FirstName": "Alice"},
    ])
    .SendMessage()
)

Bulk send to multiple addressbook groups and contacts

Pass a list of Destination instances built from existing Addressbook groups and contacts directly to the Destinations kwarg to message everyone in them in one call.

from tnzapi.core.destination import Destination

destinations = [
    Destination(GroupID="GGGGGGGG-BBBB-BBBB-CCCC-DDDDDDDDDDDD"),
    Destination(GroupID="GGGGGGGG-BBBB-BBBB-CCCC-EEEEEEEEEEEE"),
    Destination(ContactID="CCCCCCCC-BBBB-BBBB-CCCC-DDDDDDDDDDDD"),
    Destination(ContactID="CCCCCCCC-BBBB-BBBB-CCCC-EEEEEEEEEEEE"),
]

response = client.Messaging.SMS.SendMessage(
    Message="Reminder: your subscription renews tomorrow.",
    Destinations=destinations,
    Mode=SendMode.Test  # Test mode
)

Bulk send with per-destination personalisation

Send one message to many destinations while personalising each copy with Destination keyword arguments instead of plain strings.

from tnzapi.core.destination import Destination

response = (
    client.Messaging.SMS
    .Set(
        Message="Hi [[FirstName]], your appointment is on [[Custom1]].",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestinations([
        Destination(
            ToNumber="+64211111111",
            FirstName="Alice",
            Custom1="Monday 3pm"
        ),
        Destination(
            ToNumber="+64222222222",
            FirstName="Bob",
            Custom1="Tuesday 10am"
        ),
    ])
    .SendMessage()
)

Send a file via MessageLink

NZ carriers don't support MMS. Attach a file with AddAttachment(...) - a local file path is read and base64-encoded automatically - then reference it in the message text with [[File1]]; the recipient gets an SMS with a link to the file instead of an inline attachment.

response = (
    client.Messaging.SMS
    .Set(
        Message="Here's the photo you requested: [[File1]]",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestination("+64211111111")
    .AddAttachment("path/to/photo.jpg")
    .SendMessage()
)

Multiple files, via AddAttachments

AddAttachments([...]) adds several attachments in one call - each item can be a path string, a dict, or a FileAttachment instance.

response = (
    client.Messaging.SMS
    .Set(
        Message="Here's what you requested: [[File1]] [[File2]]",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestination("+64211111111")
    .AddAttachments(["path/to/photo.jpg", "path/to/receipt.pdf"])
    .SendMessage()
)

Scheduled send with webhook callback

Combine SendTime/Timezone to delay delivery with WebhookCallbackURL to get notified the moment it completes, instead of polling Status(...).

response = client.Messaging.SMS.SendMessage(
    Message="Your reminder.",
    Destination="+64211111111",
    SendTime="2026-12-31T09:00:00",
    Timezone="Pacific/Auckland",
    WebhookCallbackURL="https://yourapp.example.com/webhooks/sms",
    WebhookCallbackFormat="JSON",
    Mode=SendMode.Test  # Test mode
)

Poll for status

Check delivery progress and per-recipient results any time after sending. Each entry in Recipients is a real object here (see note below), so plain attribute access works.

status = client.Messaging.SMS.Status(response.MessageID)

if status.Result == "Success":
    print(f"JobStatus: {status.JobStatus}")
    for recipient in status.Recipients:
        print(f" -> {recipient.Destination}: {recipient.Status} ({recipient.Result})")

        for reply in recipient.SMSReplies:
            print(f"    reply: {reply.MessageText}")

Poll for inbound SMS

Retrieve SMS replies received in the last TimePeriod minutes, as an alternative to configuring a webhook. Unlike Status(...)'s Recipients, Received(...)'s Messages are still plain dicts, so this sample uses dict access.

received = client.Messaging.SMS.Received(TimePeriod=1440)  # minutes

if received.Result == "Success":
    for message in received.Messages:
        print(f"From {message['From']}: {message['MessageText']}")

Poll for replies to a specific message

Reply(...) is a direct alias for Status(...) above, scoped and shaped identically, just named for this specific use case. client.Reports.SMSReply.Poll(MessageID) from the Reports facade also works and behaves identically, useful if the channel isn't a hardcoded literal at your call site.

replies = client.Messaging.SMS.Reply(response.MessageID)

if replies.Result == "Success":
    for recipient in replies.Recipients:
        for reply in recipient.SMSReplies:
            print(f"{recipient.Destination} replied: {reply.MessageText}")

Actions

SMS supports Reschedule and Abort (see the Getting Started capability table for all channels). Both are also reachable via the channel-agnostic client.Actions.Reschedule.SendRequest(Channel="sms", ...) facade. See Actions. An empty or missing MessageID raises ValueError; see Getting Started's note on required IDs.

Reschedule

Move a not-yet-sent message to a new SendTime.

response = client.Messaging.SMS.Reschedule(response.MessageID, SendTime="2026-12-31T12:00:00")

if response.Result == "Success":
    print(f"Action: {response.Action}, Status: {response.Status}")

Abort

Cancel a scheduled message before it sends.

client.Messaging.SMS.Abort(response.MessageID)

Response

SendMessage(...) response

FieldTypeDescription
ResultstrSee Getting Started.
ErrorMessagelist[str]See Getting Started.
MessageIDstrThe ID of the message you just sent.

Status(...)/Reply(...) response

FieldTypeDescription
MessageIDstrThe message this status is for.
JobStatusstrSee Getting Started's Common Response Enums.
JobNumstrTNZ's internal job number for this send.
AccountstrThe TNZ account that owns this job.
SubAccount / DepartmentstrEchoed from the original send.
ReferencestrEchoed from the Reference parameter.
CreatedTimeLocal / CreatedTimeUTC / CreatedTimeUTC_RFC3339strWhen the job was created, in local time, UTC, and RFC 3339 UTC.
DelayedTimeLocal / DelayedTimeUTC / DelayedTimeUTC_RFC3339strThe scheduled send time, if SendTime was set.
TimezonestrTimezone name used for scheduling.
CountintTotal recipients in the job.
CompleteintRecipients processed so far.
Success / FailedintRecipients successfully delivered / failed.
PricestrJob total cost.
TotalRecords / RecordsPerPage / PageCount / PageintPagination metadata for Recipients, controlled by Status(...)'s RecordsPerPage/Page parameters.
Recipientslist[dataclass]Per-recipient results. See table below.
Recipient object (each entry in Recipients)

SMS is the one channel where Recipients (and each recipient's nested SMSReplies) are real dataclass instances rather than plain dicts. Every other channel's Recipients/Messages stay plain dict lists, as described in Getting Started's Common Response Enums. Existing dict-style access still works unchanged (recipient["Status"], recipient.get("Status")), and attribute access (recipient.Status) works too, for either style. A field the API returns that isn't in the table below is preserved and still reachable via dict-style access; it just won't have a matching attribute.

FieldTypeDescription
TypestrRecipient channel type. See Getting Started's Common Response Enums.
DestSeqstrTNZ's internal sequence ID for this recipient within the job.
DestinationstrThe recipient's phone number.
ContactIDstrAddressbook contact reference, if sent via ContactID/GroupID.
StatusstrDelivery status for this recipient. See Getting Started's Common Response Enums.
ResultstrHuman-readable delivery result for this recipient.
SentTimeLocal / SentTimeUTC / SentTimeUTC_RFC3339strWhen the message was actually sent to this recipient.
Attention / Company / Custom1Custom9strEchoed personalisation fields. See Destination Fields above.
RemoteIDstrCarrier/network-assigned identifier for this delivery, if available.
PricestrPer-recipient cost.
SMSReplieslist[dataclass]Inbound replies from this recipient, same dict-plus-attribute access as Recipients above. See table below.
SMSReplies object (each entry in Recipients[].SMSReplies)
FieldTypeDescription
ReceivedIDstrUnique identifier for this reply.
ReceivedTimeLocal / ReceivedTimeUTC / ReceivedTimeUTC_RFC3339strWhen the reply was received.
TimezonestrTimezone name for ReceivedTimeLocal.
FromstrThe replying phone number.
MessageTextstrThe reply body.

Received(...) response

FieldTypeDescription
ResultstrSee Getting Started.
ErrorMessagelist[str]See Getting Started.
TotalRecords / RecordsPerPage / PageCount / PageintPagination metadata for Messages, controlled by Received(...)'s RecordsPerPage/Page parameters. Received(...) only ever returns the requested page. It never auto-walks every page for you.
Messageslist[dict]Inbound SMS messages. See table below.
Message dict (each entry in Messages)

Unlike Status(...)/Reply(...)'s Recipients above, each entry here is a plain dict, not a typed object: access fields with message["Field"]/message.get("Field"), not attribute access.

KeyDescription
ReceivedIDUnique identifier for this message.
MessageIDThe original outbound message this replies to, if determinable.
JobNumThe original send job's number, if applicable.
SubAccount / DepartmentEchoed billing codes from the original send.
ReceivedTimeLocal / ReceivedTimeUTC / ReceivedTimeUTC_RFC3339When the message was received, in three formats.
FromThe sender's phone number.
ContactIDAddressbook contact reference, if the sender matched one.
MessageTextThe message body.
TimezoneTimezone name for ReceivedTimeLocal.
VersionPayload format version.

Reschedule(...)/Abort(...) response

FieldTypeDescription
ResultstrSee Getting Started.
ErrorMessagelist[str]See Getting Started.
ActionResultstrHuman-readable result of the action.
MessageIDstrThe message this action was applied to.
JobNumstrThe job number this action was applied to.
StatusstrSee Getting Started's Common Response Enums.
ActionstrThe action performed, e.g. "Reschedule".

Email

TNZ's Email API sends plain-text or HTML emails. Provide your own HTML, or specify a TemplateID so your team can manage the design in the Dashboard's WYSIWYG editor without needing code changes. Attach files, and TNZ automatically tracks link clicks in the message body. Delivery status is available for every email you send, including invoices, newsletters, and alerts.

Quick Example

from tnzapi import TNZAPI
from tnzapi.core.send_mode import SendMode

client = TNZAPI(AuthToken="[Your Auth Token]")

response = client.Messaging.Email.SendMessage(
    FromEmail="from@test.com",        # Optional - leave blank to use your API username as sender
    EmailSubject="Test Email",
    MessagePlain="Test Email Body",
    Destination="email.one@test.com",
    Mode=SendMode.Test  # Test mode
)

if response.Result == "Success":
    print(f"Success - MessageID: {response.MessageID}")

Parameters

ParameterTypeRequiredDescription
MessagePlainstrYes*Plain-text body.
MessageHTMLstrYes*HTML body. Can be combined with MessagePlain for a multipart email.
TemplateIDstrYes*Pre-configured message template ID (alternative to MessagePlain/MessageHTML).
Destinationstr | DestinationYes†Single destination shorthand: a plain string, e.g. "email.one@test.com", or a Destination instance for a single personalised send without building a Destinations list. See Destination fields below.
EmailAddressstrYes†Alternative single-destination field, equivalent to Destination.
Destinationslist[Destination]Yes†One or more destinations. See Destination fields below.
ContactIDstrNoSingle addressbook contact to send to (alternative/addition to Destinations).
GroupIDstrNoSingle addressbook group to send to (alternative/addition to Destinations).
MessageIDstrNoSupply your own message ID (otherwise auto-generated).
ReferencestrNoYour internal reference, returned in reports and webhooks.
FromstrNoSender's friendly (display) name, as seen by the email recipient.
FromEmailstrNoSender address. Leave blank to use your API username.
CCEmailstrNoTracked CC address added to the email (chargeable, per recipient).
BCCEmailstrNoUntracked BCC address added to the email (chargeable, per recipient).
ReplyTostrNoReply-To address: replies from the recipient are sent here instead of FromEmail.
EmailSubjectstrNoSubject line for the email.
NotificationTypestrNoNotification delivery mode.
WebhookCallbackURLstrNoURL for delivery status callbacks.
WebhookCallbackFormatstrNoCallback format: "JSON"/"XML"/"POST"/"GET".
ReportTostrNoEmail address to receive delivery reports.
SendTimestrNoSchedule delivery. Combine with Timezone.
TimezonestrNoTimezone name for SendTime (e.g. "New Zealand", "Pacific/Auckland").
SubAccountstrNoSub-account code for billing separation.
DepartmentstrNoDepartment code.
ModestrNoSet "Test" to validate without sending. Default "Live".
Fileslist[dict]NoEmail attachments, added with AddAttachment(Name, Data).

*At least one of MessagePlain, MessageHTML, or TemplateID must be provided; MessagePlain and MessageHTML may be combined to send a multipart email.
†Set via Destination, EmailAddress, or Destinations, via ContactID/GroupID, or via AddDestination(...) chained on Set(...).

Two calling styles: every field above can be passed directly as a keyword argument to SendMessage(...) in one call, or accumulated first via client.Messaging.Email.Set(...)/AddDestination(...)/AddAttachment(...) chained calls and finished with a no-argument SendMessage(). Both styles accept the exact same field set. Set(...) raises ValueError on an unknown field name; SendMessage(...) instead returns Result="Failed".

Destination Fields (Destination)

Destination is shared across every channel; see the Destination Model reference in Getting Started for its full field list and construction patterns. Email's primary destination field is EmailAddress; Recipient has the same effect. A Destination instance validates field names at construction time, and an unknown key in a destination dict is rejected the same way. See the Destination Model reference for details.

FieldDescription
EmailAddressDestination email address, e.g. "email.one@test.com".
RecipientGeneric destination address, sent as-is regardless of channel (same effect as EmailAddress here). Set by Destination("email.one@test.com") or AddDestination("email.one@test.com").
ToNumberAccepted but not read by Email.
MobilePhoneAccepted but not read by Email.
MainPhoneAccepted but not read by Email.
FaxNumberAccepted but not read by Email.
FirstName / LastName / Company / AttentionPersonalisation tokens, e.g. [[FirstName]].
Custom1Custom9Arbitrary per-recipient personalisation values, [[Custom1]][[Custom9]].
ContactIDAddressbook contact reference: sends to that contact instead of a raw address.
GroupIDAddressbook group reference: sends to all members of that group.
GroupCodeAlternative group lookup by code (instead of GroupID).

Code Samples

Single destination, plain text

The simplest way to send: pass a plain-text body and destination directly to SendMessage(...).

response = client.Messaging.Email.SendMessage(
    FromEmail="from@test.com",
    EmailSubject="Test Email",
    MessagePlain="Test Email Body",
    Destination="email.one@test.com",
    Mode=SendMode.Test  # Test mode
)

Via the builder

Use Set(...)/AddDestination(...) when you're chaining several calls together, e.g. building up CCEmail, ReplyTo, or an attachment step by step.

response = (
    client.Messaging.Email
    .Set(
        EmailSubject="Test Email",
        MessagePlain="Test Email Body",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestination("email.one@test.com")
    .SendMessage()
)

Multiple recipients, via AddDestinations

AddDestinations([...]) adds several destinations in one call on the builder - each item can be a bare string, a dict, or a typed Destination, mixed freely in the same list. Unlike AddDestination(...) (singular), which only ever adds one destination per call and raises TypeError on a list, this is the chainable way to add several at once.

response = (
    client.Messaging.Email
    .Set(
        EmailSubject="Test Email",
        MessagePlain="Hi [[FirstName]]!",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestinations([
        "email.one@test.com",
        {"EmailAddress": "email.two@test.com", "FirstName": "Alice"},
    ])
    .SendMessage()
)

Bulk send with per-destination personalisation, via typed Destination

from tnzapi.core.destination import Destination

response = (
    client.Messaging.Email
    .Set(
        EmailSubject="Test Email",
        MessagePlain="Hi [[FirstName]], your appointment is on [[Custom1]].",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestinations([
        Destination(
            EmailAddress="email.one@test.com",
            FirstName="Alice",
            Custom1="Monday 3pm",
        ),
        Destination(
            EmailAddress="email.two@test.com",
            FirstName="Bob",
            Custom1="Tuesday 10am",
        ),
    ])
    .SendMessage()
)

HTML email

Email clients have inconsistent CSS support, so inline styles (rather than a <style> block) are the safest way to style an HTML body:

from tnzapi.core.destination import Destination

html_body = """
<div style='font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;'>
    <h2 style='color: #1057fc;'>Your Order Has Shipped!</h2>
    <p>Hi [[FirstName]],</p>
    <p>Great news! Your order <strong>#12345</strong> is on its way and should arrive within 2-3 business days.</p>
    <p style='text-align: center; margin: 24px 0;'>
        <a href='https://example.com/track/12345' style='background-color: #1057fc; color: #ffffff; padding: 12px 24px; border-radius: 4px; text-decoration: none;'>Track Your Order</a>
    </p>
    <p style='color: #787878; font-size: 12px;'>If you have any questions, reply to this email and we'll be happy to help.</p>
</div>"""

response = client.Messaging.Email.SendMessage(
    EmailSubject="Your order has shipped",
    MessageHTML=html_body,
    Destinations=[
        Destination(
            EmailAddress="email.one@test.com",
            FirstName="Alice"
        )
    ],
    Mode=SendMode.Test  # Test mode
)

With an attachment

A local file path is read and base64-encoded automatically - no manual base64 handling needed.

response = (
    client.Messaging.Email
    .Set(
        EmailSubject="Test Email",
        MessagePlain="See attached.",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestination("email.one@test.com")
    .AddAttachment("path/to/doc.pdf")
    .SendMessage()
)

Using the typed FileAttachment

tnzapi.core.file_attachment.FileAttachment mirrors Destination: a plain dataclass (Name, Data) usable with AddAttachment(...) or directly in a Files=[...] list. FileAttachment("path/to/doc.pdf") - a single positional argument, equivalently FileAttachment(FileName="path/to/doc.pdf") - reads the file and base64-encodes it automatically, deriving Name from the path's basename unless overridden.

Security note: Data is never filesystem-checked, under any circumstances - FileAttachment(Name=..., Data=<anything>) always stores Data exactly as given. This matters whenever Data comes from an external source (e.g. an HTTP request body) that must never be reinterpreted as a local file path.

from tnzapi.core.file_attachment import FileAttachment

response = (
    client.Messaging.Email
    .Set(
        EmailSubject="Test Email",
        MessagePlain="See attached.",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestination("email.one@test.com")
    .AddAttachment(
        FileAttachment("path/to/doc.pdf")
    )
    .SendMessage()
)

Multiple files, via AddAttachments

AddAttachments([...]) adds several attachments in one call - each item can be a path string, a dict, or a FileAttachment instance.

response = (
    client.Messaging.Email
    .Set(
        EmailSubject="Test Email",
        MessagePlain="See the attached documents.",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestination("email.one@test.com")
    .AddAttachments(["path/to/doc.pdf", "path/to/receipt.pdf"])
    .SendMessage()
)

Custom sender, reply-to, and CC

Override the sender identity and add a CC and Reply-To address.

response = client.Messaging.Email.SendMessage(
    EmailSubject="Your invoice is ready",
    MessagePlain="Please find your invoice attached.",
    From="Test Company Billing",
    FromEmail="billing@test.com",
    ReplyTo="accounts@test.com",
    CCEmail="manager@test.com",
    Destination="email.one@test.com",
    Mode=SendMode.Test  # Test mode
)

Bulk send to multiple addressbook groups and contacts

Pass a list of Destination instances built from existing Addressbook groups and contacts directly to the Destinations kwarg to message everyone in them in one call.

from tnzapi.core.destination import Destination

destinations = [
    Destination(GroupID="GGGGGGGG-BBBB-BBBB-CCCC-DDDDDDDDDDDD"),
    Destination(GroupID="GGGGGGGG-BBBB-BBBB-CCCC-EEEEEEEEEEEE"),
    Destination(ContactID="CCCCCCCC-BBBB-BBBB-CCCC-DDDDDDDDDDDD"),
    Destination(ContactID="CCCCCCCC-BBBB-BBBB-CCCC-EEEEEEEEEEEE"),
]

response = client.Messaging.Email.SendMessage(
    EmailSubject="Test Email",
    MessagePlain="Test Email Body",
    Destinations=destinations,
    Mode=SendMode.Test  # Test mode
)

Poll for status

Check delivery progress and per-recipient results any time after sending.

status = client.Messaging.Email.Status(response.MessageID)

if status.Result == "Success":
    print(f"JobStatus: '{status.JobStatus}', JobNum: '{status.JobNum}'")

    for recipient in status.Recipients:
        print(f" -> {recipient['Destination']}: {recipient['Status']} ({recipient['Result']})")

Actions

Email supports Reschedule, Abort, and Resubmit (see the Getting Started capability table for all channels). All three are also reachable via the channel-agnostic client.Actions.Reschedule.SendRequest(Channel="email", ...) facade. See Actions. An empty or missing MessageID raises ValueError; see Getting Started's note on required IDs.

Reschedule

Move a not-yet-sent email to a new SendTime.

response = client.Messaging.Email.Reschedule(response.MessageID, SendTime="2026-12-31T12:00:00")

if response.Result == "Success":
    print(f"Action: {response.Action}, Status: {response.Status}")

Abort

Cancel a scheduled email before it sends.

client.Messaging.Email.Abort(response.MessageID)

Resubmit

Resubmit a message for delivery at a new scheduled time, without rebuilding the whole request.

client.Messaging.Email.Resubmit(response.MessageID, SendTime="2026-12-31T13:00:00")

Response

SendMessage(...) response

FieldTypeDescription
ResultstrSee Getting Started.
ErrorMessagelist[str]See Getting Started.
MessageIDstrThe ID of the message you just sent.

Status(...) response

FieldTypeDescription
MessageIDstrThe message this status is for.
JobStatusstrSee Getting Started's Common Response Enums.
JobNumstrTNZ's internal job number for this send.
AccountstrThe TNZ account that owns this job.
SubAccount / DepartmentstrEchoed from the original send.
ReferencestrEchoed from the Reference parameter.
CreatedTimeLocal / CreatedTimeUTC / CreatedTimeUTC_RFC3339strWhen the job was created, in local time, UTC, and RFC 3339 UTC.
DelayedTimeLocal / DelayedTimeUTC / DelayedTimeUTC_RFC3339strThe scheduled send time, if SendTime was set.
TimezonestrTimezone name used for scheduling.
CountintTotal recipients in the job.
CompleteintRecipients processed so far.
Success / FailedintRecipients successfully delivered / failed.
PricestrJob total cost.
TotalRecords / RecordsPerPage / PageCount / PageintPagination metadata for Recipients, controlled by Status(...)'s RecordsPerPage/Page parameters.
Recipientslist[dict]Per-recipient results. See table below.
Recipient dict (each entry in Recipients)

Unlike some other TNZ SDKs, v3.00 Python returns each recipient as a plain dict, not a typed object: access fields with recipient["Field"]/recipient.get("Field"), not attribute access. See Getting Started's Common Response Enums for the shared note on this and the possible values of Type/Status.

KeyDescription
TypeRecipient channel type. See Getting Started's Common Response Enums.
DestSeqTNZ's internal sequence ID for this recipient within the job.
DestinationThe recipient's email address.
ContactIDAddressbook contact reference, if sent via ContactID/GroupID.
StatusDelivery status for this recipient. See Getting Started's Common Response Enums.
ResultHuman-readable delivery result for this recipient.
SentTimeLocal / SentTimeUTC / SentTimeUTC_RFC3339When the message was actually sent to this recipient.
Attention / Company / Custom1Custom9Echoed personalisation fields. See Destination Fields above.
RemoteIDCarrier/network-assigned identifier for this delivery, if available.
PricePer-recipient cost.

Reschedule(...)/Abort(...)/Resubmit(...) response

FieldTypeDescription
ResultstrSee Getting Started.
ErrorMessagelist[str]See Getting Started.
ActionResultstrHuman-readable result of the action.
MessageIDstrThe message this action was applied to.
JobNumstrThe job number this action was applied to.
StatusstrSee Getting Started's Common Response Enums.
ActionstrThe action performed, e.g. "Resubmit".

TTS (Text-to-Speech)

Transform written text into clear, natural-sounding voice calls. Ideal for automated alerts, appointment reminders, or customer surveys. Route callers or capture responses with interactive keypad menus, and rely on built-in answering machine detection (AnswerPhoneMode) to deliver an alternate message when a voicemail picks up instead of a person.

Quick Example

from tnzapi.core.send_mode import SendMode

response = client.Messaging.TTS.SendMessage(
    MessageToPeople="Hello, this is a call from test. This is relevant information.",
    Destination="+64211111111",
    Mode=SendMode.Test  # Test mode
)

if response.Result == "Success":
    print(f"Success - MessageID: {response.MessageID}")

Parameters

ParameterTypeRequiredDescription
MessageToPeoplestrYes*Text read aloud to a human answering the call.
TemplateIDstrYes*Pre-configured message template ID (alternative to MessageToPeople).
DestinationstrYes†Single destination shorthand, e.g. "+64211111111".
ToNumberstrYes†Alternative single-destination field.
Destinationslist[dict]Yes†One or more destinations. See Destination fields below.
ContactIDstrNoSingle addressbook contact to send to (alternative/addition to Destinations).
GroupIDstrNoSingle addressbook group to send to (alternative/addition to Destinations).
ReferencestrNoYour internal reference, returned in reports and webhooks.
MessageToAnswerPhonesstrNoAlternative text read when an answering machine picks up.
AnswerPhoneModestrNoHow to handle an answering machine: "NDAS", "NDAF", "DAS", or "DAF". Leave unset to use the account's default handling.
Keypadslist[dict]NoKeypad menu options. See Keypad fields below.
KeypadOptionRequiredboolNoForce the caller to press a key before the call proceeds. Default False.
CallRouteMessageOnWrongKeystrNoMessage played if an invalid key is pressed.
CallRouteMessageToPeoplestrNoMessage played before routing to an operator.
CallRouteMessageToOperatorsstrNoMessage played to the operator receiving the routed call.
NumberOfOperatorsintNoNumber of simultaneous operators for keypad-routed calls. Also settable in-flight via Pacing(...).
RetryAttemptsintNoNumber of retry attempts on no-answer/busy.
RetryPeriodintNoMinutes between retry attempts.
CallerIDstrNoCaller ID shown to the recipient (E.164, must be whitelisted under your account).
VoicestrNoTTS voice to use: "Female1", "Male1", "Nicole", "Russell", "Amy", "Brian", or "Emma". Leave unset to use the account's default voice.
OptionsstrNoAdditional provider-specific options (survey recording, DTMF capture, etc).
SendTimestrNoSchedule delivery. Combine with Timezone.
TimezonestrNoTimezone name for SendTime, e.g. "New Zealand", "Pacific/Auckland".
SubAccountstrNoSub-account code for billing separation.
DepartmentstrNoDepartment code.
MessageIDstrNoSupply your own message ID (otherwise auto-generated).
ReportTostrNoEmail address to receive delivery reports.
WebhookCallbackURLstrNoURL for delivery status callbacks.
WebhookCallbackFormatstrNoCallback format.
NotificationTypestrNoNotification delivery mode.
ModestrNoSet "Test" to validate without sending. Default "Live".

*Either MessageToPeople or TemplateID must be provided.
†Provide at least one destination via Destination (single-string shorthand), ToNumber (alternative single-destination field), Destinations (a list, built up with AddDestination(...)), or ContactID/GroupID. Destination/ToNumber here are request-level shorthand for a single destination, equivalent to a one-item Destinations list; see Destination fields below for the fuller per-item field set available inside Destinations.

Direct kwargs: client.Messaging.TTS.SendMessage(...) accepts every field in the table above directly as a keyword argument: there's no separate builder-only subset. For Destinations/Keypads, either pass fully-formed lists directly, or build them up first with AddDestination(...)/AddKeypad(...) and then call SendMessage() (with no arguments, or with the remaining fields). AddDestination(...) accepts a Destination instance, a plain string, a list of either, or ContactID=/GroupID= keyword arguments.

Keypad Fields (Keypads list items)

Built via AddKeypad(Tone, Play=None, RouteNumber=None, PlaySection=None), or supplied directly as dicts in Keypads.

FieldTypeDescription
ToneintThe DTMF digit this entry responds to (0–9).
PlaystrMessage read aloud when this key is pressed.
RouteNumberstrPhone number to route the call to when this key is pressed.
PlaySectionstrWhere in the call flow this keypad applies: "Main" (the main MessageToPeople), "AnswerPhone" (simulates an answering-machine pickup and plays MessageToAnswerPhones), or "WrongKey" (plays CallRouteMessageOnWrongKey).

Destination Fields (Destination)

Destination is shared across every channel; see the Destination Model reference in Getting Started for its full field list and construction patterns. TTS's primary destination field is ToNumber: the bare-string shorthand passed to AddDestination(...) sets it. MainPhone and Recipient have the same effect when set explicitly. MobilePhone/EmailAddress/FaxNumber are accepted but genuinely not read by TTS. Destination dicts/objects are validated: an unknown key raises ValueError from AddDestination(...), or produces Result="Failed" from SendMessage(...); see the Destination Model reference for details.

FieldDescription
ToNumberDestination phone number, e.g. "+64211111111". TTS's primary destination field, set by the bare-string shorthand passed to AddDestination("+64211111111").
RecipientGeneric destination address, sent as-is regardless of channel. Same effect as ToNumber here.
MainPhoneAlternative phone field, same effect as ToNumber for TTS.
MobilePhoneAccepted but not read by TTS.
EmailAddressAccepted but not read by TTS.
FaxNumberAccepted but not read by TTS.
ContactIDAddressbook contact reference. Sends to that contact instead of a raw number.
GroupIDAddressbook group reference. Sends to all members of that group.
GroupCodeAlternative group lookup by code (instead of GroupID).
FirstName / LastName / Company / AttentionPersonalisation tokens, e.g. [[FirstName]].
Custom1Custom9Arbitrary per-recipient personalisation values, [[Custom1]][[Custom9]].

Code Samples

Single destination shorthand

The simplest way to send: pass the spoken text and destination directly to SendMessage(...).

response = client.Messaging.TTS.SendMessage(
    MessageToPeople="Hello, this is a call from test. This is relevant information.",
    Destination="+64211111111",
    Mode=SendMode.Test  # Test mode
)

With a keypad menu, via AddDestination/AddKeypad

Add interactive keypad options so callers can route themselves to a different number by pressing a key.

response = (
    client.Messaging.TTS
    .Set(
        MessageToPeople="Press 1 for sales, press 2 for support.",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestination("+64211111111")
    .AddKeypad(
        Tone=1,
        Play="Connecting you to sales now.",
        PlaySection="Main",
        RouteNumber="+64211112222"
    )
    .SendMessage()
)

Keypad menu, via AddKeypads

AddKeypads([...]) adds several keypad entries in one call - each item is a plain dict of the same fields AddKeypad(...) takes.

response = (
    client.Messaging.TTS
    .Set(
        MessageToPeople="Press 1 for sales, press 2 for support.",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestination("+64211111111")
    .AddKeypads([
        {"Tone": 1, "RouteNumber": "+64211112222"},
        {"Tone": 2, "RouteNumber": "+64211113333"},
    ])
    .SendMessage()
)

Multi-option keypad menu

Offer several keys at once: route two of them to different numbers, play a short message on another without routing, and replay the original message on a fourth.

response = (
    client.Messaging.TTS
    .Set(
        MessageToPeople="Press 1 for sales, press 2 for support, press 3 to hear our address, or press 9 to hear this message again.",
        KeypadOptionRequired=True,
        CallRouteMessageOnWrongKey="Sorry, that key isn't recognised. Please try again.",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestination("+64211111111")
    .AddKeypad(Tone=1, RouteNumber="+64211112222")
    .AddKeypad(Tone=2, RouteNumber="+64211113333")
    .AddKeypad(Tone=3, Play="We're located at 123 Example Street, Auckland.")
    .AddKeypad(Tone=9, PlaySection="Main")
    .SendMessage()
)

Multiple destinations, via AddDestinations

AddDestinations([...]) adds several destinations in one call on the builder - each item can be a bare string, a dict, or a typed Destination, mixed freely in the same list. Unlike AddDestination(...) (singular), which only ever adds one destination per call and raises TypeError on a list, this is the chainable way to add several at once.

response = (
    client.Messaging.TTS
    .Set(MessageToPeople="Hello, this is a call from test.")
    .AddDestinations(["+64211111111", "+64222222222"])
    .SendMessage()
)

Multiple destinations

Call more than one number in a single request by calling AddDestination(...) repeatedly.

response = (
    client.Messaging.TTS
    .Set(
        MessageToPeople="Hello, this is a call from test.",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestination("+64211111111")
    .AddDestination("+64222222222")
    .SendMessage()
)

With personalisation

Build a Destination with MainPhone and personalisation fields set, for use with [[FirstName]]-style tokens in MessageToPeople.

from tnzapi.core.destination import Destination

response = (
    client.Messaging.TTS
    .Set(
        MessageToPeople="Hello [[FirstName]], this is a reminder about your appointment on [[Custom1]].",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestinations([
        Destination(
            MainPhone="+64211111111",
            FirstName="Alice",
            Custom1="Monday 3pm"
        ),
        Destination(
            MainPhone="+64222222222",
            FirstName="Bob",
            Custom1="Tuesday 10am"
        ),
    ])
    .SendMessage()
)

Scheduled send

Delay the call to a specific SendTime.

response = client.Messaging.TTS.SendMessage(
    MessageToPeople="Your reminder call.",
    Destination="+64211111111",
    SendTime="2026-08-01T09:00:00",
    Timezone="Pacific/Auckland",
    Mode=SendMode.Test  # Test mode
)

Retry attempts, caller ID, and reporting

Configure automatic retries on no-answer or busy, set the caller ID shown to recipients, choose a voice, and email a report once the job completes.

response = client.Messaging.TTS.SendMessage(
    MessageToPeople="Hello, this is a call from test. This is relevant information.",
    Destination="+64211111111",
    CallerID="+6499999999",
    Voice="Emma",
    RetryAttempts=3,
    RetryPeriod=1,
    ReportTo="report@example.com",
    Mode=SendMode.Test  # Test mode
)

Poll for status

Check call progress and per-recipient results any time after sending.

status = client.Messaging.TTS.Status(MessageID=response.MessageID)

if status.Result == "Success":
    print(f"JobStatus: '{status.JobStatus}', JobNum: '{status.JobNum}'")

    for recipient in status.Recipients:
        print(f" -> {recipient['Destination']}: {recipient['Status']} ({recipient['Result']})")

Actions

TTS supports Reschedule, Abort, Resubmit, and Pacing (see the Getting Started capability table for all channels). Each is available directly on client.Messaging.TTS as shown below, and also reachable via Actions's client.Actions.<Verb>.SendRequest(Channel="tts", ...), which works the same way across every channel. An empty or missing MessageID raises ValueError; see Getting Started's note on required IDs.

Reschedule

Move a not-yet-placed call to a new SendTime.

response = client.Messaging.TTS.Reschedule(
    MessageID=response.MessageID,
    SendTime="2026-12-31T12:00:00"
)

Abort

Cancel a scheduled call before it's placed.

client.Messaging.TTS.Abort(MessageID=response.MessageID)

Resubmit

Re-send a call at a new scheduled time without rebuilding the whole request.

client.Messaging.TTS.Resubmit(MessageID=response.MessageID, SendTime="2026-08-01T10:00:00")

Pacing (adjust simultaneous operators)

Change how many calls run at once on an in-progress keypad-routed job.

client.Messaging.TTS.Pacing(MessageID=response.MessageID, NumberOfOperators=1)

Response

SendMessage(...) response

FieldTypeDescription
ResultstrSee Getting Started.
ErrorMessagelist[str]See Getting Started.
MessageIDstrThe ID of the call you just placed.

Status(...) response

FieldTypeDescription
ResultstrSee Getting Started.
MessageIDstrThe call this status is for.
JobStatusstrSee Getting Started's Common Response Enums.
JobNumstrTNZ's internal job number for this send.
AccountstrThe TNZ account that owns this job.
SubAccount / DepartmentstrEchoed from the original send.
ReferencestrEchoed from the Reference parameter.
CreatedTimeLocal / CreatedTimeUTC / CreatedTimeUTC_RFC3339strWhen the job was created, in three formats.
DelayedTimeLocal / DelayedTimeUTC / DelayedTimeUTC_RFC3339strThe scheduled send time, if SendTime was set.
TimezonestrTimezone name used for scheduling.
CountintTotal recipients in the job.
CompleteintRecipients processed so far.
Success / FailedintCalls successfully completed / failed.
PricestrJob total cost.
TotalRecords / RecordsPerPage / PageCount / PageintPagination metadata for Recipients, controlled by Status(...)'s RecordsPerPage/Page parameters.
Recipientslist[dict]Per-recipient results. See table below.
ErrorMessagelist[str]See Getting Started.
Recipient dict (each entry in Recipients)

Unlike some other TNZ SDKs, v3.00 Python's Recipients entries are plain dicts, not typed objects: access fields via recipient["Field"] or recipient.get("Field"), not attribute access. See Getting Started for the fuller explanation.

KeyDescription
TypeRecipient channel type. See Getting Started's Common Response Enums. TTS calls report as "Voice".
DestSeqTNZ's internal sequence ID for this recipient within the job.
DestinationThe recipient's phone number.
ContactIDAddressbook contact reference, if sent via ContactID/GroupID.
StatusSee Getting Started's Common Response Enums.
ResultHuman-readable call result for this recipient.
SentTimeLocal / SentTimeUTC / SentTimeUTC_RFC3339When the call was actually placed to this recipient, in three formats.
Attention / Company / Custom1Custom9Echoed personalisation fields. See Destination Fields above.
RemoteIDCarrier/network-assigned identifier for this call, if available.
PricePer-recipient cost.

Reschedule(...)/Abort(...)/Resubmit(...)/Pacing(...) response

FieldTypeDescription
ResultstrSee Getting Started.
ActionResultstrHuman-readable result of the action.
MessageIDstrThe call this action was applied to.
JobNumstrThe job number this action was applied to.
StatusstrSee Getting Started's Common Response Enums.
ActionstrThe action performed, e.g. "Pacing".
ErrorMessagelist[str]See Getting Started.

Voice (Pre-Recorded Audio)

Send pre-recorded audio files as voice calls. This suits alerts where a human voice matters, or information that's easier to hear than read. Manage calls to individuals or large groups, with retry attempts for failed calls (RetryAttempts/RetryPeriod), answering machine detection, and interactive keypad responses.

Voice shares its keypad shape and AnswerPhoneMode values with TTS. The request/builder API is identical: the only structural difference is that Voice has no Voice field, because it plays pre-recorded audio rather than synthesizing speech. Audio content is carried directly in the message fields (MessageToPeople, MessageToAnswerPhones, the CallRouteMessage* fields, and keypad Play), not a separate attachment or file upload: there is no Files/VoiceFiles field on this channel.

MessageToPeople, MessageToAnswerPhones, CallRouteMessageOnWrongKey, CallRouteMessageToPeople, CallRouteMessageToOperators, and AddKeypad(..., Play=...) all accept a local file path directly (the file is read and base64-encoded automatically), a tnzapi.core.file_attachment.FileAttachment instance (.Data is used, .Name is ignored - these fields have no filename concept), or a pre-encoded base64 string, passed through unchanged. Unlike TTS's same-named fields (plain spoken text), these are always audio.

Quick Example

from tnzapi.core.send_mode import SendMode

response = client.Messaging.Voice.SendMessage(
    Destination="+64211111111",
    TemplateID="[Your Template ID]",
    Mode=SendMode.Test  # Test mode
)

if response.Result == "Success":
    print(f"Success - MessageID: {response.MessageID}")

Parameters

ParameterTypeRequiredDescription
TemplateIDstrYes*Pre-configured audio template ID, typically built in the Dashboard.
MessageToPeoplestrYes*Audio played to a person answering the call (alternative to TemplateID): a local file path, a FileAttachment, or base64-encoded WAV/MP3 audio directly.
DestinationstrYes†Single destination shorthand, e.g. "+64211111111".
ToNumberstrYes†Alternative single-destination field.
Destinationslist[dict]Yes†One or more destinations. See Destination fields below.
ContactIDstrNoSingle addressbook contact to send to (alternative/addition to Destinations).
GroupIDstrNoSingle addressbook group to send to (alternative/addition to Destinations).
ReferencestrNoYour internal reference, returned in reports and webhooks.
MessageToAnswerPhonesstrNoAudio played if an answering machine picks up instead of a person.
AnswerPhoneModestrNoHow to handle an answering machine: "NDAS", "NDAF", "DAS", or "DAF". Leave unset to use the account's default handling.
Keypadslist[dict]NoKeypad menu options, same shape as TTS's. See Keypad fields below.
KeypadOptionRequiredboolNoForce the caller to press a key before the call proceeds. Default False.
CallRouteMessageOnWrongKeystrNoAudio played if an invalid key is pressed.
CallRouteMessageToPeoplestrNoAudio played before routing the call to an operator.
CallRouteMessageToOperatorsstrNoAudio played to the operator receiving the routed call.
NumberOfOperatorsintNoNumber of simultaneous operators for keypad-routed calls. Also settable in-flight via Pacing(...).
RetryAttemptsintNoNumber of retry attempts on no-answer/busy.
RetryPeriodintNoMinutes between retry attempts.
CallerIDstrNoCaller ID shown to the recipient (E.164, must be whitelisted under your account).
OptionsstrNoAdditional provider-specific options.
SendTimestrNoSchedule delivery. Combine with Timezone.
TimezonestrNoTimezone name for SendTime, e.g. "New Zealand", "Pacific/Auckland".
SubAccountstrNoSub-account code for billing separation.
DepartmentstrNoDepartment code.
MessageIDstrNoSupply your own message ID (otherwise auto-generated).
ReportTostrNoEmail address to receive delivery reports.
WebhookCallbackURLstrNoURL for delivery status callbacks.
WebhookCallbackFormatstrNoCallback format.
NotificationTypestrNoNotification delivery mode.
ModestrNoSet "Test" to validate without sending. Default "Live".

*Either TemplateID or MessageToPeople must be provided: a plain Voice send typically relies on TemplateID (built in the Dashboard) for its pre-recorded audio instead of supplying base64 data inline.
†Provide at least one destination via Destination (single-string shorthand), ToNumber (alternative single-destination field), Destinations (a list, built up with AddDestination(...)), or ContactID/GroupID. Destination/ToNumber here are request-level shorthand for a single destination, equivalent to a one-item Destinations list; see Destination fields below for the fuller per-item field set available inside Destinations.
‡Accepts a local file path (read and base64-encoded automatically), a FileAttachment instance, or a pre-encoded base64 string directly. See the call-flow sample below.

Direct kwargs: client.Messaging.Voice.SendMessage(...) accepts every field in the table above directly as a keyword argument: there's no separate builder-only subset. For Destinations/Keypads, either pass fully-formed lists directly, or build them up first with AddDestination(...)/AddKeypad(...) and then call SendMessage() (with no arguments, or with the remaining fields). AddDestination(...) accepts a Destination instance, a plain string, a list of either, or ContactID=/GroupID= keyword arguments.

Keypad Fields (Keypads list items)

Built via AddKeypad(Tone, Play=None, RouteNumber=None, PlaySection=None), or supplied directly as dicts in Keypads.

FieldTypeDescription
ToneintThe DTMF digit this entry responds to (0–9).
PlaystrAudio played when this key is pressed, before any routing: a local file path, a FileAttachment, or base64-encoded WAV/MP3 audio directly.
RouteNumberstrPhone number to route the call to when this key is pressed.
PlaySectionstrWhere in the call flow this keypad applies: "Main" (the main MessageToPeople), "AnswerPhone" (simulates an answering-machine pickup and plays MessageToAnswerPhones), or "WrongKey" (plays CallRouteMessageOnWrongKey).

Destination Fields (Destination)

Destination is shared across every channel; see the Destination Model reference in Getting Started for its full field list and construction patterns. Voice's primary destination field is ToNumber: the bare-string shorthand passed to AddDestination(...) sets it. MainPhone and Recipient have the same effect when set explicitly. MobilePhone/EmailAddress/FaxNumber are accepted but genuinely not read by Voice. Destination dicts/objects are validated: an unknown key raises ValueError from AddDestination(...), or produces Result="Failed" from SendMessage(...); see the Destination Model reference for details.

FieldDescription
ToNumberDestination phone number, e.g. "+64211111111". Voice's primary destination field, set by the bare-string shorthand passed to AddDestination("+64211111111").
RecipientGeneric destination address, sent as-is regardless of channel. Same effect as ToNumber here.
MainPhoneAlternative phone field, same effect as ToNumber for Voice.
MobilePhoneAccepted but not read by Voice (used by SMS/WhatsApp/RCS).
EmailAddressAccepted but not read by Voice (used by Email).
FaxNumberAccepted but not read by Voice (used by Fax).
ContactIDAddressbook contact reference. Sends to that contact instead of a raw number.
GroupIDAddressbook group reference. Sends to all members of that group.
GroupCodeAlternative group lookup by code (instead of GroupID).
FirstName / LastName / Company / AttentionPersonalisation tokens, e.g. [[FirstName]].
Custom1Custom9Arbitrary per-recipient personalisation values, [[Custom1]][[Custom9]].

Code Samples

Single destination shorthand

The simplest way to send: pass a destination directly to SendMessage(...). The pre-recorded audio comes from TemplateID, typically built in the Dashboard.

response = client.Messaging.Voice.SendMessage(
    Destination="+64211111111",
    TemplateID="[Your Template ID]",
    Mode=SendMode.Test  # Test mode
)

From a file path, with a keypad menu

A local audio file path is read and base64-encoded automatically - no manual base64 handling needed. Add interactive keypad options so callers can route themselves to a different number by pressing a key.

response = (
    client.Messaging.Voice
    .Set(
        MessageToPeople="path/to/audio.wav",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestination("+64211111111")
    .AddKeypad(
        Tone=1,
        RouteNumber="+64211112222",
        PlaySection="Main"
    )
    .SendMessage()
)

Using the typed FileAttachment

tnzapi.core.file_attachment.FileAttachment mirrors Destination: a plain dataclass (Name, Data). MessageToPeople=FileAttachment("path/to/audio.wav") is equivalent to MessageToPeople="path/to/audio.wav" above - .Data is used, .Name is ignored, since this field has no filename concept.

from tnzapi.core.file_attachment import FileAttachment

response = (
    client.Messaging.Voice
    .Set(
        MessageToPeople=FileAttachment("path/to/audio.wav")
    )
    .AddDestination("+64211111111")
    .SendMessage()
)

Call flow with pre-recorded audio and keypad-triggered playback

Set audio for every stage of the call: the main message, the answering machine fallback, the routing announcements, and a keypad menu where key 1 routes to another number and key 2 plays its own audio clip. MessageToPeople, MessageToAnswerPhones, the CallRouteMessage* fields, and keypad Play all accept a local file path (read and base64-encoded automatically), a FileAttachment, or a pre-encoded base64 string.

response = (
    client.Messaging.Voice
    .Set(
        MessageToPeople="path/to/audio.wav",
        MessageToAnswerPhones="path/to/audio.wav",
        AnswerPhoneMode="DAS",
        CallRouteMessageToPeople="path/to/please-hold.wav",
        CallRouteMessageToOperators="path/to/incoming-call.wav",
        CallRouteMessageOnWrongKey="path/to/invalid-option.wav",
        KeypadOptionRequired=True,
        NumberOfOperators=2,
        Mode=SendMode.Test  # Test mode
    )
    .AddDestination("+64211111111")
    .AddKeypad(
        Tone=1,
        RouteNumber="+64211112222",
        PlaySection="Main"
    )
    .AddKeypad(
        Tone=2,
        Play="path/to/opening-hours.wav"
    )
    .SendMessage()
)

Multiple destinations, via AddDestinations

AddDestinations([...]) adds several destinations in one call on the builder - each item can be a bare string, a dict, or a typed Destination, mixed freely in the same list. Unlike AddDestination(...) (singular), which only ever adds one destination per call and raises TypeError on a list, this is the chainable way to add several at once.

response = (
    client.Messaging.Voice
    .Set(MessageToPeople="path/to/audio.wav")
    .AddDestinations(["+64211111111", "+64222222222"])
    .SendMessage()
)

Keypad menu, via AddKeypads

AddKeypads([...]) adds several keypad entries in one call - each item is a plain dict of the same fields AddKeypad(...) takes.

response = (
    client.Messaging.Voice
    .Set(
        MessageToPeople="path/to/press-1-for-sales-2-for-support.wav",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestination("+64211111111")
    .AddKeypads([
        {"Tone": 1, "RouteNumber": "+64211112222"},
        {"Tone": 2, "RouteNumber": "+64211113333"},
    ])
    .SendMessage()
)

Multiple destinations

Call more than one number in a single request by calling AddDestination(...) repeatedly.

response = (
    client.Messaging.Voice
    .Set(
        TemplateID="[Your Template ID]",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestination("+64211111111")
    .AddDestination("+64222222222")
    .SendMessage()
)

With personalisation

Build a Destination with MainPhone and personalisation fields set. These are echoed back in the recipient's Status result even though Voice's own pre-recorded audio can't substitute merge tags into speech.

from tnzapi.core.destination import Destination

response = (
    client.Messaging.Voice
    .Set(
        TemplateID="[Your Template ID]",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestinations([
        Destination(
            MainPhone="+64211111111",
            FirstName="Alice",
            Custom1="Account #4432"
        ),
        Destination(
            MainPhone="+64222222222",
            FirstName="Bob",
            Custom1="Account #7788"
        ),
    ])
    .SendMessage()
)

Scheduled send

Delay the call to a specific SendTime.

response = client.Messaging.Voice.SendMessage(
    Destination="+64211111111",
    TemplateID="[Your Template ID]",
    SendTime="2026-08-01T09:00:00",
    Timezone="Pacific/Auckland",
    Mode=SendMode.Test  # Test mode
)

Retry attempts, caller ID, and billing codes

Configure retry behaviour for calls that go unanswered or reach a busy signal, the caller ID shown to the recipient, and the sub-account/department used for billing and reporting.

response = client.Messaging.Voice.SendMessage(
    Destination="+64211111111",
    TemplateID="[Your Template ID]",
    RetryAttempts=3,
    RetryPeriod=5,
    CallerID="+6499999999",
    SubAccount="SALES",
    Department="OUTBOUND",
    ReportTo="reports@example.com",
    Mode=SendMode.Test  # Test mode
)

Poll for status

Check call progress and per-recipient results any time after sending.

status = client.Messaging.Voice.Status(MessageID=response.MessageID)

if status.Result == "Success":
    print(f"JobStatus: '{status.JobStatus}', JobNum: '{status.JobNum}'")

    for recipient in status.Recipients:
        print(f" -> {recipient['Destination']}: {recipient['Status']} ({recipient['Result']})")

Actions

Voice supports Reschedule, Abort, Resubmit, and Pacing (see the Getting Started capability table for all channels). Each is available directly on client.Messaging.Voice as shown below, and also reachable via Actions's client.Actions.<Verb>.SendRequest(Channel="voice", ...), which works the same way across every channel. An empty or missing MessageID raises ValueError; see Getting Started's note on required IDs.

Reschedule

Move a not-yet-placed call to a new SendTime.

response = client.Messaging.Voice.Reschedule(
    MessageID=response.MessageID,
    SendTime="2026-12-31T12:00:00"
)

Abort

Cancel a scheduled call before it's placed.

client.Messaging.Voice.Abort(MessageID=response.MessageID)

Resubmit

Resubmit a call for redelivery at a new scheduled time, without rebuilding the whole request.

client.Messaging.Voice.Resubmit(MessageID=response.MessageID, SendTime="2026-08-01T10:00:00")

Pacing (adjust simultaneous operators)

Change how many calls run at once on an in-progress keypad-routed job.

client.Messaging.Voice.Pacing(MessageID=response.MessageID, NumberOfOperators=1)

Response

SendMessage(...) response

FieldTypeDescription
ResultstrSee Getting Started.
ErrorMessagelist[str]See Getting Started.
MessageIDstrThe ID of the call you just placed.

Status(...) response

FieldTypeDescription
ResultstrSee Getting Started.
MessageIDstrThe call this status is for.
JobStatusstrSee Getting Started's Common Response Enums.
JobNumstrTNZ's internal job number for this send.
AccountstrThe TNZ account that owns this job.
SubAccount / DepartmentstrEchoed from the original send.
ReferencestrEchoed from the Reference parameter.
CreatedTimeLocal / CreatedTimeUTC / CreatedTimeUTC_RFC3339strWhen the job was created, in three formats.
DelayedTimeLocal / DelayedTimeUTC / DelayedTimeUTC_RFC3339strThe scheduled send time, if SendTime was set.
TimezonestrTimezone name used for scheduling.
CountintTotal recipients in the job.
CompleteintRecipients processed so far.
Success / FailedintCalls successfully completed / failed.
PricestrJob total cost.
TotalRecords / RecordsPerPage / PageCount / PageintPagination metadata for Recipients, controlled by Status(...)'s RecordsPerPage/Page parameters.
Recipientslist[dict]Per-recipient results. See table below.
ErrorMessagelist[str]See Getting Started.
Recipient dict (each entry in Recipients)

Unlike some other TNZ SDKs, v3.00 Python's Recipients entries are plain dicts, not typed objects: access fields via recipient["Field"] or recipient.get("Field"), not attribute access. See Getting Started for the fuller explanation.

KeyDescription
TypeRecipient channel type. See Getting Started's Common Response Enums. Voice calls report as "Voice", same as TTS.
DestSeqTNZ's internal sequence ID for this recipient within the job.
DestinationThe recipient's phone number.
ContactIDAddressbook contact reference, if sent via ContactID/GroupID.
StatusSee Getting Started's Common Response Enums.
ResultHuman-readable call result for this recipient.
SentTimeLocal / SentTimeUTC / SentTimeUTC_RFC3339When the call was actually placed to this recipient, in three formats.
Attention / Company / Custom1Custom9Echoed personalisation fields. See Destination Fields above.
RemoteIDCarrier/network-assigned identifier for this call, if available.
PricePer-recipient cost.

Reschedule(...)/Abort(...)/Resubmit(...)/Pacing(...) response

FieldTypeDescription
ResultstrSee Getting Started.
ActionResultstrHuman-readable result of the action.
MessageIDstrThe call this action was applied to.
JobNumstrThe job number this action was applied to.
StatusstrSee Getting Started's Common Response Enums.
ActionstrThe action performed, e.g. "Pacing". All four action methods share this one response type.
ErrorMessagelist[str]See Getting Started.

Fax

TNZ's Fax API sends documents from your application to any fax machine worldwide, without physical fax hardware. Track the delivery status of each fax to confirm it arrived.

Unlike SMS/RCS/WhatsApp, Fax has no free-text message field. Content comes entirely from an attachment.

Quick Example

from tnzapi import TNZAPI
from tnzapi.core.send_mode import SendMode

client = TNZAPI(AuthToken="[Your Auth Token]")

response = (
    client.Messaging.Fax
    .Set(
        Mode=SendMode.Test  # Test mode
    )
    .AddDestination("+6491111111")
    .AddAttachment("path/to/doc.pdf")
    .SendMessage()
)

if response.Result == "Success":
    print(f"Success - MessageID: {response.MessageID}")

Parameters

ParameterTypeRequiredDescription
Fileslist[dict]Yes*The document(s) to fax, added with AddAttachment(Name, Data). Fax has no message-text body field; the document itself is the payload.
TemplateIDstrYes*Pre-configured fax template ID (alternative to Files).
Destinationstr | DestinationYes†Single destination shorthand: a plain string, e.g. "+6491111111", or a Destination instance for a single personalised send without building a Destinations list. See Destination fields below.
ToNumberstrYes†Alternative single-destination field, equivalent to Destination.
Destinationslist[Destination]Yes†One or more destinations. See Destination fields below.
ContactIDstrNoSingle addressbook contact to send to (alternative/addition to Destinations).
GroupIDstrNoSingle addressbook group to send to (alternative/addition to Destinations).
MessageIDstrNoSupply your own message ID (otherwise auto-generated).
ReferencestrNoYour internal reference, returned in reports and webhooks.
NotificationTypestrNoNotification delivery mode.
WebhookCallbackURLstrNoURL for delivery status callbacks.
WebhookCallbackFormatstrNoCallback format: "JSON"/"XML"/"POST"/"GET".
ReportTostrNoEmail address to receive delivery reports.
SendTimestrNoSchedule delivery. Combine with Timezone.
TimezonestrNoTimezone name for SendTime (e.g. "New Zealand", "Pacific/Auckland").
SubAccountstrNoSub-account code for billing separation.
DepartmentstrNoDepartment code.
CSIDstrNoCalled Subscriber ID string shown on the recipient fax machine/header.
ResolutionstrNoFax output resolution, e.g. "Fine".
WatermarkFolderstrNoFolder containing the watermark image/template to stamp onto pages.
WatermarkFirstPagestrNoWatermark text/token stamped onto the first page only.
WatermarkAllPagesstrNoWatermark text/token stamped onto every page, e.g. "Page [[PageNumber]]".
RetryAttemptsintNoNumber of retry attempts on send failure (busy/no answer/fax error).
RetryPeriodintNoMinutes to wait between retry attempts.
ModestrNoSet "Test" to validate without sending. Default "Live".

*Either Files or TemplateID must be provided.
†Set via Destination, ToNumber, or Destinations, via ContactID/GroupID, or via AddDestination(...) chained on Set(...).

Two calling styles: every field above can be passed directly as a keyword argument to SendMessage(...) in one call, or accumulated first via client.Messaging.Fax.Set(...)/AddDestination(...)/AddAttachment(...) chained calls and finished with a no-argument SendMessage(). Both styles accept the exact same field set. Set(...) raises ValueError on an unknown field name; SendMessage(...) instead returns Result="Failed".

Destination Fields (Destination)

Destination is shared across every channel; see the Destination Model reference in Getting Started for its full field list and construction patterns. Fax's primary destination field is ToNumber; FaxNumber and Recipient have the same effect. A Destination instance validates field names at construction time, and an unknown key in a destination dict is rejected the same way. See the Destination Model reference for details.

FieldDescription
ToNumberDestination fax number, e.g. "+6491232345".
RecipientGeneric destination address, sent as-is regardless of channel (same effect as ToNumber here). Set by Destination("+6491111111") or AddDestination("+6491111111").
FaxNumberAlternative phone field, same effect as ToNumber for this channel.
MobilePhoneAccepted but not read by Fax (used by SMS/WhatsApp/RCS).
MainPhoneAccepted but not read by Fax (used by TTS/Voice).
EmailAddressAccepted but not read by Fax (used by Email).
ContactIDAddressbook contact reference: sends to that contact instead of a raw number.
GroupIDAddressbook group reference: sends to all members of that group.
GroupCodeAlternative group lookup by code (instead of GroupID).
CompanyNot rendered into a merge tag (Fax has no message body). Still accepted on the Destination and echoed back in status reports for your own reference.
AttentionNot rendered into a merge tag (Fax has no message body). Still accepted on the Destination and echoed back in status reports for your own reference.
FirstNameNot rendered into a merge tag (Fax has no message body). Still accepted on the Destination and echoed back in status reports for your own reference.
LastNameNot rendered into a merge tag (Fax has no message body). Still accepted on the Destination and echoed back in status reports for your own reference.
Custom1Custom9Not rendered into a merge tag (Fax has no message body). Still accepted on the Destination and echoed back in status reports for your own reference.

Fax has no message body to personalise, so Company/Attention/FirstName/LastName/Custom1Custom9 above aren't rendered into [[FirstName]]-style merge tags the way they are on other channels. The Destination class still carries all of them (it's one shared class across every channel, not Fax-specific), and Fax still accepts and echoes them back for your own reference. See the reference-fields sample below.

Code Samples

Single destination with an attachment

The simplest way to send: a local file path is read and base64-encoded automatically - no manual base64 handling needed.

response = (
    client.Messaging.Fax
    .Set(
        Mode=SendMode.Test  # Test mode
    )
    .AddDestination("+6491111111")
    .AddAttachment("path/to/doc.pdf")
    .SendMessage()
)

Using the typed FileAttachment

tnzapi.core.file_attachment.FileAttachment mirrors Destination: a plain dataclass (Name, Data) usable with AddAttachment(...) or directly in a Files=[...] list. Useful when you want to build the attachment separately from the AddAttachment(...) call, e.g. to reuse it across multiple sends. FileAttachment("path/to/doc.pdf") - a single positional argument, equivalently FileAttachment(FileName="path/to/doc.pdf") - reads the file and base64-encodes it automatically, deriving Name from the path's basename unless overridden.

Security note: Data is never filesystem-checked, under any circumstances - FileAttachment(Name=..., Data=<anything>) always stores Data exactly as given. This matters whenever Data comes from an external source (e.g. an HTTP request body) that must never be reinterpreted as a local file path.

from tnzapi.core.file_attachment import FileAttachment

response = (
    client.Messaging.Fax
    .Set(
        Mode=SendMode.Test  # Test mode
    )
    .AddDestination("+6491111111")
    .AddAttachment(
        FileAttachment("path/to/doc.pdf")
    )
    .SendMessage()
)

Multiple pages, via AddAttachments

AddAttachments([...]) adds several attachments (e.g. multiple pages sent as separate files) in one call - each item can be a path string, a dict, or a FileAttachment instance.

response = (
    client.Messaging.Fax
    .Set()
    .AddDestination("+6491111111")
    .AddAttachments(["path/to/page1.pdf", "path/to/page2.pdf"])
    .SendMessage()
)

Multiple destinations

Send the same document to more than one fax number in a single request.

from tnzapi.core.destination import Destination

destinations = [
    Destination("+6491111111"),
    Destination("+6492222222"),
]

response = client.Messaging.Fax.SendMessage(
    Destinations=destinations,
    Files=[{"Name": "My Document.pdf", "Data": "[base64-encoded-file-data]"}],
    Mode=SendMode.Test  # Test mode
)

Multiple recipients, via AddDestinations

AddDestinations([...]) adds several destinations in one call on the builder - each item can be a bare string, a dict, or a typed Destination, mixed freely in the same list. Unlike AddDestination(...) (singular), which only ever adds one destination per call and raises TypeError on a list, this is the chainable way to add several at once.

response = (
    client.Messaging.Fax
    .Set()
    .AddAttachment("path/to/doc.pdf")
    .AddDestinations(["+6491111111", "+6492222222"])
    .SendMessage()
)

With reference fields

Build a Destination with FaxNumber and reference fields set. Fax has no message body to personalise, so these are for reference and reporting rather than merge tags.

from tnzapi.core.destination import Destination

response = client.Messaging.Fax.SendMessage(
    Destinations=[
        Destination(
            FaxNumber="+6491111111",
            Attention="Accounts Payable",
            Custom1="Invoice #1234"
        ),
    ],
    Files=[{"Name": "My Document.pdf", "Data": "[base64-encoded-file-data]"}],
    Mode=SendMode.Test  # Test mode
)

Bulk send with per-destination reference fields, via AddDestinations

Fax has no message body to personalise, but reference fields are still accepted per-destination and echoed back in status reports.

from tnzapi.core.destination import Destination

response = (
    client.Messaging.Fax
    .Set()
    .AddAttachment("path/to/doc.pdf")
    .AddDestinations([
        Destination(
            FaxNumber="+6491111111",
            Attention="Accounts Payable",
            Custom1="Invoice #1234"
        ),
        Destination(
            FaxNumber="+6492222222",
            Attention="Purchasing",
            Custom1="Invoice #1235"
        ),
    ])
    .SendMessage()
)

Scheduled send

Delay delivery to a specific SendTime.

response = client.Messaging.Fax.SendMessage(
    Destination="+6491111111",
    Files=[{"Name": "My Document.pdf", "Data": "[base64-encoded-file-data]"}],
    SendTime="2026-12-31T09:00:00",
    Timezone="Pacific/Auckland",
    Mode=SendMode.Test  # Test mode
)

Setting resolution and CSID

Set the output resolution and the CSID (station identifier) printed in the header of the received fax.

response = client.Messaging.Fax.SendMessage(
    Destination="+6491111111",
    Files=[{"Name": "My Document.pdf", "Data": "[base64-encoded-file-data]"}],
    Resolution="Fine",
    CSID="MY COMPANY",
    Mode=SendMode.Test  # Test mode
)

Adding a watermark

Apply an image from a watermark folder to the first page only, or to every page, of the outgoing fax.

response = client.Messaging.Fax.SendMessage(
    Destination="+6491111111",
    Files=[{"Name": "My Document.pdf", "Data": "[base64-encoded-file-data]"}],
    WatermarkFolder="Confidential",
    WatermarkFirstPage="confidential-stamp.png",
    Mode=SendMode.Test  # Test mode
)

Configuring retry behaviour

Set how many times to retry a busy or unanswered fax number, and how many minutes to wait between attempts.

response = client.Messaging.Fax.SendMessage(
    Destination="+6491111111",
    Files=[{"Name": "My Document.pdf", "Data": "[base64-encoded-file-data]"}],
    RetryAttempts=3,
    RetryPeriod=5,
    Mode=SendMode.Test  # Test mode
)

Poll for status

Check delivery progress and per-recipient results any time after sending.

status = client.Messaging.Fax.Status(response.MessageID)

if status.Result == "Success":
    print(f"JobStatus: '{status.JobStatus}', JobNum: '{status.JobNum}'")

    for recipient in status.Recipients:
        print(f" -> {recipient['Destination']}: {recipient['Status']} ({recipient['Result']})")

Actions

Fax supports Reschedule, Abort, and Resubmit (see the Getting Started capability table for all channels). All three are also reachable via the channel-agnostic client.Actions.Reschedule.SendRequest(Channel="fax", ...) facade. See Actions. An empty or missing MessageID raises ValueError; see Getting Started's note on required IDs.

Reschedule

Move a not-yet-sent fax to a new SendTime.

response = client.Messaging.Fax.Reschedule(response.MessageID, SendTime="2026-12-31T12:00:00")

Abort

Cancel a scheduled fax before it sends.

client.Messaging.Fax.Abort(response.MessageID)

Resubmit

Resend a fax at a new scheduled time without rebuilding the whole request.

client.Messaging.Fax.Resubmit(response.MessageID, SendTime="2026-12-31T13:00:00")

Response

SendMessage(...) response

FieldTypeDescription
ResultstrSee Getting Started.
ErrorMessagelist[str]See Getting Started.
MessageIDstrThe ID of the fax you just sent.

Status(...) response

FieldTypeDescription
MessageIDstrThe fax this status is for.
JobStatusstrSee Getting Started's Common Response Enums.
JobNumstrTNZ's internal job number for this send.
AccountstrThe TNZ account that owns this job.
SubAccount / DepartmentstrEchoed from the original send.
ReferencestrEchoed from the Reference parameter.
CreatedTimeLocal / CreatedTimeUTC / CreatedTimeUTC_RFC3339strWhen the job was created, in local time, UTC, and RFC 3339 UTC.
DelayedTimeLocal / DelayedTimeUTC / DelayedTimeUTC_RFC3339strThe scheduled send time, if SendTime was set.
TimezonestrTimezone name used for scheduling.
CountintTotal recipients in the job.
CompleteintRecipients processed so far.
Success / FailedintRecipients successfully delivered / failed.
PricestrJob total cost.
TotalRecords / RecordsPerPage / PageCount / PageintPagination metadata for Recipients, controlled by Status(...)'s RecordsPerPage/Page parameters.
Recipientslist[dict]Per-recipient results. See table below.
Recipient dict (each entry in Recipients)

Unlike some other TNZ SDKs, v3.00 Python returns each recipient as a plain dict, not a typed object: access fields with recipient["Field"]/recipient.get("Field"), not attribute access. See Getting Started's Common Response Enums for the shared note on this and the possible values of Type/Status.

KeyDescription
TypeRecipient channel type. See Getting Started's Common Response Enums.
DestSeqTNZ's internal sequence ID for this recipient within the job.
DestinationThe recipient's fax number.
ContactIDAddressbook contact reference, if sent via ContactID/GroupID.
StatusDelivery status for this recipient. See Getting Started's Common Response Enums.
ResultHuman-readable delivery result for this recipient.
SentTimeLocal / SentTimeUTC / SentTimeUTC_RFC3339When the fax was actually sent to this recipient.
Attention / Company / Custom1Custom9Echoed reference fields. See Destination Fields above.
RemoteIDCarrier/network-assigned identifier for this delivery, if available.
PricePer-recipient cost.

Reschedule(...)/Abort(...)/Resubmit(...) response

FieldTypeDescription
ResultstrSee Getting Started.
ErrorMessagelist[str]See Getting Started.
ActionResultstrHuman-readable result of the action.
MessageIDstrThe fax this action was applied to.
JobNumstrThe job number this action was applied to.
StatusstrSee Getting Started's Common Response Enums.
ActionstrThe action performed, e.g. "Resubmit".

WhatsApp

Send text and files over WhatsApp, and receive replies back (see Poll for status / inbound messages below).

Send WhatsApp template messages, with optional fallback to another channel, via the TNZ REST API. WhatsApp requires all three of Message, TemplateID, and FromNumber (not either/or like SMS). Find your Template ID in the Dashboard.

Quick Example

from tnzapi.core.send_mode import SendMode

response = client.Messaging.WhatsApp.SendMessage(
    TemplateID="123e4567-e89b-12d3-a456-426614174000",
    Message="Your order has shipped!",
    FromNumber="+6495006000",
    Destination="+64211111111",
    Mode=SendMode.Test  # Test mode
)

if response.Result == "Success":
    print(f"Success - MessageID: {response.MessageID}")

Parameters

ParameterTypeRequiredDescription
TemplateIDstrYesPre-approved WhatsApp template ID, required alongside Message and FromNumber, unlike SMS/RCS's either/or.
MessagestrYesMessage body. Supports personalisation tokens [[FirstName]], [[Custom1]], etc. Must match the content of the approved TemplateID template.
FromNumberstrYesYour registered WhatsApp sender number, shown on the recipient's device.
Destinationslist[dict]Yes†One or more destinations. See Destination fields below.
DestinationstrYes†Single destination shorthand, e.g. "+64211111111".
ToNumberstrYes†Alternative single-destination field.
ContactIDstrNoSingle addressbook contact to send to (alternative/addition to Destinations).
GroupIDstrNoSingle addressbook group to send to (alternative/addition to Destinations).
ReferencestrNoYour internal reference, returned in reports and webhooks.
FallbackModestr | list[str]NoFallback channel(s) if WhatsApp delivery fails, tried in the order given, e.g. "SMS" or ["SMS", "RCS"]. A list is joined into TNZ's wire format automatically ("SMS, RCS").
SendTimestrNoSchedule delivery. Combine with Timezone.
TimezonestrNoTimezone name for SendTime, e.g. "Pacific/Auckland".
SubAccountstrNoSub-account code for billing separation.
DepartmentstrNoDepartment code.
MessageIDstrNoSupply your own message ID (otherwise auto-generated).
WebhookCallbackURLstrNoURL for delivery status callbacks.
WebhookCallbackFormatstrNoCallback format, e.g. "JSON".
NotificationTypestrNoNotification delivery mode.
Fileslist[dict]NoMedia attachments. See AddAttachment(Name, Data) below.
ModestrNoSet "Test" to validate without sending. Default "Live".

†Set directly via Destinations, via SendMessage(...)'s own Destination/ToNumber/ContactID/GroupID keyword arguments, or via chained AddDestination(...) calls on client.Messaging.WhatsApp.Set(...).

Unlike SMS/Email/Fax/TTS/Voice, WhatsApp has no ReportTo field: there is no delivery-report-by-email option for WhatsApp.

Destination Fields (Destination)

Destination is shared across every channel; see the Destination Model reference in Getting Started for its full field list and construction patterns. WhatsApp reads ToNumber/MobilePhone as the destination. Unknown keys are validated, not silently ignored: an unrecognised key in a destination dict or Destination object raises ValueError from Set()/AddDestination(), and causes SendMessage() to return Result="Failed".

FieldDescription
RecipientGeneric single-value shorthand for the destination phone number, e.g. "+64211111111". Read by WhatsApp the same way as ToNumber/MobilePhone.
ToNumberDestination phone number, e.g. "+64211111111". Read by WhatsApp.
MobilePhoneAlternative phone destination field alongside ToNumber. Read by WhatsApp.
MainPhoneAccepted but not read by WhatsApp (used by TTS/Voice).
EmailAddressAccepted but not read by WhatsApp (used by Email).
FaxNumberAccepted but not read by WhatsApp (used by Fax).
CompanyPersonalisation token [[Company]].
AttentionPersonalisation token [[Attention]].
FirstNamePersonalisation token [[FirstName]].
LastNamePersonalisation token [[LastName]].
Custom1Custom9Personalisation tokens [[Custom1]][[Custom9]].
ContactIDAddressbook contact reference. Sends to that contact instead of a raw number.
GroupIDAddressbook group reference. Sends to all members of that group.
GroupCodeAlternative group lookup by code.

Code Samples

Single destination shorthand

The simplest way to send: pass the message, template, sender, and destination directly to SendMessage(...). Remember Message, TemplateID, and FromNumber are all required.

response = client.Messaging.WhatsApp.SendMessage(
    TemplateID="123e4567-e89b-12d3-a456-426614174000",
    Message="Your order has shipped!",
    FromNumber="+6495006000",
    Destination="+64211111111",
    Mode=SendMode.Test  # Test mode
)

With SMS fallback, via the builder

Chain Set(...) and AddDestination(...) to add a fallback channel for recipients that can't be reached on WhatsApp.

response = (
    client.Messaging.WhatsApp
    .Set(
        TemplateID="123e4567-e89b-12d3-a456-426614174000",
        Message="Your order has shipped!",
        FromNumber="+6495006000",
        FallbackMode="SMS",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestination("+64211111111")
    .SendMessage()
)

Multiple recipients, via AddDestinations

AddDestinations([...]) adds several destinations in one call on the builder - each item can be a bare string, a dict, or a typed Destination, mixed freely in the same list. Unlike AddDestination(...) (singular), which only ever adds one destination per call and raises TypeError on a list, this is the chainable way to add several at once.

response = (
    client.Messaging.WhatsApp
    .Set(
        TemplateID="123e4567-e89b-12d3-a456-426614174000",
        Message="Hi [[FirstName]]!",
        FromNumber="+6495006000"
    )
    .AddDestinations([
        "+64211111111",
        {"ToNumber": "+64222222222", "FirstName": "Bob"},
    ])
    .SendMessage()
)

Multiple destinations

Send the same templated message to more than one destination in a single request, personalising each with its own Destination fields.

from tnzapi.core.destination import Destination

response = (
    client.Messaging.WhatsApp
    .Set(
        TemplateID="123e4567-e89b-12d3-a456-426614174000",
        Message="Hi [[FirstName]], your order #[[Custom1]] has shipped!",
        FromNumber="+6495006000",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestinations([
        Destination(
            ToNumber="+64211111111",
            FirstName="Alice",
            Custom1="4432"
        ),
        Destination(
            ToNumber="+64222222222",
            FirstName="Bob",
            Custom1="7788"
        ),
    ])
    .SendMessage()
)

With personalisation

Build a single Destination with ToNumber and personalisation fields set, for use with [[FirstName]]-style tokens in the message.

response = (
    client.Messaging.WhatsApp
    .Set(
        TemplateID="123e4567-e89b-12d3-a456-426614174000",
        Message="Hi [[FirstName]], your order #[[Custom1]] has shipped!",
        FromNumber="+6495006000",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestination(
        Destination(
            ToNumber="+64211111111",
            FirstName="Alice",
            Custom1="4432"
        )
    )
    .SendMessage()
)

With an attachment

A local file path is read and base64-encoded automatically - no manual base64 handling needed.

response = (
    client.Messaging.WhatsApp
    .Set(
        TemplateID="123e4567-e89b-12d3-a456-426614174000",
        Message="Here's your invoice.",
        FromNumber="+6495006000",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestination("+64211111111")
    .AddAttachment("path/to/doc.pdf")
    .SendMessage()
)

Using the typed FileAttachment

tnzapi.core.file_attachment.FileAttachment mirrors Destination: a plain dataclass (Name, Data) usable with AddAttachment(...) or directly in a Files=[...] list. FileAttachment("path/to/doc.pdf") - a single positional argument, equivalently FileAttachment(FileName="path/to/doc.pdf") - reads the file and base64-encodes it automatically, deriving Name from the path's basename unless overridden.

Security note: Data is never filesystem-checked, under any circumstances - FileAttachment(Name=..., Data=<anything>) always stores Data exactly as given. This matters whenever Data comes from an external source (e.g. an HTTP request body) that must never be reinterpreted as a local file path.

from tnzapi.core.file_attachment import FileAttachment

response = (
    client.Messaging.WhatsApp
    .Set(
        TemplateID="123e4567-e89b-12d3-a456-426614174000",
        Message="Here's your invoice.",
        FromNumber="+6495006000",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestination("+64211111111")
    .AddAttachment(
        FileAttachment("path/to/doc.pdf")
    )
    .SendMessage()
)

Multiple files, via AddAttachments

AddAttachments([...]) adds several attachments in one call - each item can be a path string, a dict, or a FileAttachment instance.

response = (
    client.Messaging.WhatsApp
    .Set(
        TemplateID="123e4567-e89b-12d3-a456-426614174000",
        Message="See the attached documents.",
        FromNumber="+6495006000"
    )
    .AddDestination("+64211111111")
    .AddAttachments(["path/to/doc.pdf", "path/to/receipt.pdf"])
    .SendMessage()
)

Poll for status / inbound messages

Check delivery progress, or retrieve inbound replies received in the last TimePeriod minutes, any time after sending.

status = client.Messaging.WhatsApp.Status(response.MessageID)

received = client.Messaging.WhatsApp.Received(TimePeriod=1440)
if received.Result == "Success":
    for message in received.Messages:
        print(f" => From: '{message.get('From')}', MessageText: '{message.get('MessageText')}'")

Actions

WhatsApp supports Reschedule and Abort (see the Getting Started capability table for all channels). Both are also reachable through the shared Actions module via client.Actions.Reschedule.SendRequest(Channel="whatsapp", ...) and client.Actions.Abort.SendRequest(Channel="whatsapp", ...). This is useful if your code already routes actions by channel name rather than calling client.Messaging.WhatsApp directly. An empty or missing MessageID raises ValueError; see Getting Started's note on required IDs.

Reschedule

Move a not-yet-sent message to a new SendTime.

response = client.Messaging.WhatsApp.Reschedule(
    response.MessageID,
    SendTime="2026-12-31T12:00:00"
)

Abort

Cancel a scheduled message before it sends.

client.Messaging.WhatsApp.Abort(response.MessageID)

Response

Status(...).Recipients and Received(...).Messages are plain dict lists, not typed objects: access entries with recipient["Key"]/recipient.get("Key") rather than attribute access. See Getting Started for more on this.

SendMessage(...) response

FieldTypeDescription
ResultstrSee Getting Started.
MessageIDstrThe ID of the message you just sent.
ErrorMessagelist[str]See Getting Started.

Status(...) response

FieldTypeDescription
ResultstrSee Getting Started.
MessageIDstrThe message this status is for.
JobStatusstrSee Getting Started's Common Response Enums.
JobNumstrTNZ's internal job number for this send.
AccountstrThe TNZ account that owns this job.
SubAccount / DepartmentstrEchoed from the original send.
ReferencestrEchoed from the Reference parameter.
CreatedTimeLocal / CreatedTimeUTC / CreatedTimeUTC_RFC3339strWhen the job was created, in three formats.
DelayedTimeLocal / DelayedTimeUTC / DelayedTimeUTC_RFC3339strThe scheduled send time, if SendTime was set.
TimezonestrTimezone name used for scheduling.
CountintTotal recipients in the job.
CompleteintRecipients processed so far.
Success / FailedintRecipients successfully delivered / failed.
PricestrJob total cost.
TotalRecords / RecordsPerPage / PageCount / PageintPagination metadata for Recipients.
Recipientslist[dict]Per-recipient results, as plain dicts. See table below.
ErrorMessagelist[str]See Getting Started.
Recipient dict (each entry in Recipients)

Each entry is an untyped dict parsed straight from the API's JSON response. These are the keys you can expect to find; see Getting Started for why the SDK doesn't validate or type this shape.

KeyDescription
TypeSee Getting Started's Common Response Enums.
DestSeqTNZ's internal sequence ID for this recipient within the job.
DestinationThe recipient's phone number.
ContactIDAddressbook contact reference, if sent via ContactID/GroupID.
StatusSee Getting Started's Common Response Enums.
ResultHuman-readable delivery result for this recipient.
SentTimeLocal / SentTimeUTCWhen the message was actually sent to this recipient.
Attention / Company / Custom1Custom9Echoed personalisation fields. See Destination Fields above.
RemoteIDCarrier/network-assigned identifier for this delivery, if available.
PricePer-recipient cost.
SMSRepliesInbound replies from this recipient, as a list of dicts. See table below. The key is named SMSReplies even on WhatsApp; it isn't a mistake in this doc.
Reply dict (each entry in SMSReplies)
KeyDescription
ReceivedIDUnique identifier for this reply.
ReceivedTimeLocal / ReceivedTimeUTCWhen the reply was received.
TimezoneTimezone name for ReceivedTimeLocal.
FromThe replying phone number.
MessageTextThe reply body.

Received(...) response

FieldTypeDescription
ResultstrSee Getting Started.
TotalRecords / RecordsPerPage / PageCount / PageintPagination metadata for Messages.
Messageslist[dict]Inbound WhatsApp messages, as plain dicts. See table below.
ErrorMessagelist[str]See Getting Started.
Message dict (each entry in Messages)
KeyDescription
ReceivedIDUnique identifier for this message.
MessageIDThe original outbound message this replies to, if determinable.
JobNumThe original send job's number, if applicable.
SubAccount / DepartmentEchoed billing codes from the original send.
ReceivedTimeLocal / ReceivedTimeUTC / ReceivedTimeUTC_RFC3339When the message was received, in three formats.
FromThe sender's phone number.
ContactIDAddressbook contact reference, if the sender matched one.
MessageTextThe message body.
TimezoneTimezone name for ReceivedTimeLocal.
VersionPayload format version.

Reschedule(...)/Abort(...) response

FieldTypeDescription
ResultstrSee Getting Started.
ActionResultstrHuman-readable result of the action.
MessageIDstrThe message this action was applied to.
JobNumstrThe job number this action was applied to.
StatusstrSee Getting Started's Common Response Enums.
ActionstrThe action performed, e.g. "Reschedule".
ErrorMessagelist[str]See Getting Started.

RCS

RCS (Rich Communication Services) sends richer messages than SMS, including media attachments (see Files below). Like SMS, Message/TemplateID are either/or (not both required, unlike WhatsApp).

Quick Example

from tnzapi.core.send_mode import SendMode

response = client.Messaging.RCS.SendMessage(
    Message="Test RCS message",
    Destination="+64211111111",
    Mode=SendMode.Test  # Test mode
)

if response.Result == "Success":
    print(f"Success - MessageID: {response.MessageID}")

Parameters

ParameterTypeRequiredDescription
MessagestrYes*Message body. Supports personalisation tokens [[FirstName]], [[Custom1]], etc.
TemplateIDstrYes*Pre-configured message template ID (alternative to Message).
Destinationslist[dict]Yes†One or more destinations. See Destination fields below.
DestinationstrYes†Single destination shorthand, e.g. "+64211111111".
ToNumberstrYes†Alternative single-destination field.
ContactIDstrNoSingle addressbook contact to send to (alternative/addition to Destinations).
GroupIDstrNoSingle addressbook group to send to (alternative/addition to Destinations).
ReferencestrNoYour internal reference, returned in reports and webhooks.
FromNumberstrNoSender ID shown on the recipient's device.
FallbackModestr | list[str]NoFallback channel(s) if RCS delivery fails, tried in the order given, e.g. "SMS" or ["SMS", "WhatsApp"]. A list is joined into TNZ's wire format automatically ("SMS, WAPP") - "WhatsApp" is translated to its real wire token "WAPP" either way.
SendTimestrNoSchedule delivery. Combine with Timezone.
TimezonestrNoTimezone name for SendTime, e.g. "Pacific/Auckland".
SubAccountstrNoSub-account code for billing separation.
DepartmentstrNoDepartment code.
MessageIDstrNoSupply your own message ID (otherwise auto-generated).
WebhookCallbackURLstrNoURL for delivery status callbacks.
WebhookCallbackFormatstrNoCallback format, e.g. "JSON".
NotificationTypestrNoNotification delivery mode.
SMSEmailReplystrNoEmail address to receive SMS fallback replies.
CharacterConversionboolNoConvert characters outside the GSM character set automatically. Default False. RCS shares SMS's message-building pipeline server-side, so this has the same effect as it does for SMS.
Fileslist[dict]NoMedia attachments. See AddAttachment(Name, Data) below.
ModestrNoSet "Test" to validate without sending. Default "Live".

*Either Message or TemplateID must be provided.
†Set directly via Destinations, via SendMessage(...)'s own Destination/ToNumber/ContactID/GroupID keyword arguments, or via chained AddDestination(...) calls on client.Messaging.RCS.Set(...).

Unlike SMS, RCS has no ReportTo field and no SMSCustomPageID field. Unlike WhatsApp, RCS performs no client-side required-field validation: SendMessage(...) always makes the HTTP call, and any missing/invalid fields are caught server-side, returned in response.ErrorMessage.

Destination Fields (Destination)

Destination is shared across every channel; see the Destination Model reference in Getting Started for its full field list and construction patterns. RCS reads ToNumber/MobilePhone as the destination. Unknown keys are validated, not silently ignored: an unrecognised key in a destination dict or Destination object raises ValueError from Set()/AddDestination(), and causes SendMessage() to return Result="Failed".

FieldDescription
RecipientGeneric single-value shorthand for the destination phone number, e.g. "+64211111111". Read by RCS the same way as ToNumber/MobilePhone.
ToNumberDestination phone number, e.g. "+64211111111". Read by RCS.
MobilePhoneAlternative phone destination field, also read by RCS.
MainPhoneAccepted but not read by RCS (used by TTS/Voice).
EmailAddressAccepted but not read by RCS (used by Email).
FaxNumberAccepted but not read by RCS (used by Fax).
CompanyPersonalisation token [[Company]].
AttentionPersonalisation token [[Attention]].
FirstNamePersonalisation token [[FirstName]].
LastNamePersonalisation token [[LastName]].
Custom1Custom9Arbitrary per-destination personalisation values, [[Custom1]][[Custom9]].
ContactIDAddressbook contact reference. Sends to that contact instead of a raw number.
GroupIDAddressbook group reference. Sends to all members of that group.
GroupCodeAlternative group lookup by code.

Code Samples

Single destination shorthand

The simplest way to send: pass a message and destination directly to SendMessage(...).

response = client.Messaging.RCS.SendMessage(
    Message="Test RCS message",
    Destination="+64211111111",
    Mode=SendMode.Test  # Test mode
)

Via the builder, with a custom sender ID

Chain Set(...) and AddDestination(...) to set fields the flat SendMessage(...) call above doesn't need, such as a custom FromNumber sender ID.

response = (
    client.Messaging.RCS
    .Set(
        Message="Test RCS message",
        FromNumber="61410023004",   # Sender ID, E.164 without leading '+'
        Mode=SendMode.Test  # Test mode
    )
    .AddDestination("+64211111111")
    .SendMessage()
)

With SMS fallback

Set FallbackMode, tried if RCS delivery fails.

response = (
    client.Messaging.RCS
    .Set(
        Message="Test RCS message",
        FallbackMode="SMS",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestination("+64211111111")
    .SendMessage()
)

With an attachment

A local file path is read and base64-encoded automatically - no manual base64 handling needed. tnzapi.core.file_attachment.FileAttachment mirrors Destination: a plain dataclass (Name, Data) usable with AddAttachment(...) or directly in a Files=[...] list.

Security note: Data is never filesystem-checked, under any circumstances - FileAttachment(Name=..., Data=<anything>) always stores Data exactly as given. This matters whenever Data comes from an external source (e.g. an HTTP request body) that must never be reinterpreted as a local file path. The reverse risk applies to the path-string forms: FileAttachment("path/to/doc.pdf"), a bare string passed to AddAttachments([...])/Files=[...], and Voice's own audio fields (see Voice) all read that path from disk and base64-encode it. Never pass user-controlled input as that value without validating it against an allow-list or a dedicated upload directory first - an unvalidated path risks arbitrary file exfiltration from the server.

from tnzapi.core.file_attachment import FileAttachment

response = (
    client.Messaging.RCS
    .Set(
        Message="See the attached document.",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestination("+64211111111")
    .AddAttachment(
        FileAttachment("path/to/doc.pdf")
    )
    .SendMessage()
)

Multiple recipients, via AddDestinations

AddDestinations([...]) adds several destinations in one call on the builder - each item can be a bare string, a dict, or a typed Destination, mixed freely in the same list. Unlike AddDestination(...) (singular), which only ever adds one destination per call and raises TypeError on a list, this is the chainable way to add several at once.

response = (
    client.Messaging.RCS
    .Set(
        Message="Hi [[FirstName]]!",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestinations([
        "+64211111111",
        {"ToNumber": "+64222222222", "FirstName": "Bob"},
    ])
    .SendMessage()
)

Multiple files, via AddAttachments

AddAttachments([...]) adds several attachments in one call - each item can be a path string, a dict, or a FileAttachment instance.

response = (
    client.Messaging.RCS
    .Set(
        Message="See the attached documents.",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestination("+64211111111")
    .AddAttachments(["path/to/doc.pdf", "path/to/receipt.pdf"])
    .SendMessage()
)

Multiple destinations

Send the same message to more than one destination in a single request, personalising each with its own Destination fields.

from tnzapi.core.destination import Destination

destinations = [
    Destination(
        ToNumber="+64211111111",
        FirstName="Alice",
        Custom1="4432"
    ),
    Destination(
        ToNumber="+64222222222",
        FirstName="Bob",
        Custom1="7788"
    ),
]

response = client.Messaging.RCS.SendMessage(
    Message="Hi [[FirstName]], your order #[[Custom1]] has shipped!",
    Destinations=destinations,
    Mode=SendMode.Test  # Test mode
)

With personalisation

Build a single Destination with ToNumber and personalisation fields set, for use with [[FirstName]]-style tokens in the message.

response = (
    client.Messaging.RCS
    .Set(
        Message="Hi [[FirstName]], your order #[[Custom1]] has shipped!",
        Mode=SendMode.Test  # Test mode
    )
    .AddDestinations([
        Destination(
            ToNumber="+64211111111",
            FirstName="Alice",
            Custom1="4432"
        ),
        Destination(
            ToNumber="+64222222222",
            FirstName="Bob",
            Custom1="7788"
        ),
    ])
    .SendMessage()
)

Scheduled send

Delay delivery to a specific SendTime.

response = client.Messaging.RCS.SendMessage(
    Message="Your reminder.",
    Destination="+64211111111",
    SendTime="2026-08-01T09:00:00",
    Timezone="Pacific/Auckland",
    Mode=SendMode.Test  # Test mode
)

Poll for status / inbound messages

Check delivery progress, or retrieve inbound replies received in the last TimePeriod minutes, any time after sending.

status = client.Messaging.RCS.Status(response.MessageID)

received = client.Messaging.RCS.Received(TimePeriod=1440)
if received.Result == "Success":
    for message in received.Messages:
        print(f" => From: '{message.get('From')}', MessageText: '{message.get('MessageText')}'")

Actions

RCS supports Reschedule and Abort (see the Getting Started capability table for all channels). Both are also reachable through the shared Actions module via client.Actions.Reschedule.SendRequest(Channel="rcs", ...) and client.Actions.Abort.SendRequest(Channel="rcs", ...). This is useful if your code already routes actions by channel name rather than calling client.Messaging.RCS directly. An empty or missing MessageID raises ValueError; see Getting Started's note on required IDs.

Reschedule

Move a not-yet-sent message to a new SendTime.

response = client.Messaging.RCS.Reschedule(
    response.MessageID,
    SendTime="2026-12-31T12:00:00"
)

Abort

Cancel a scheduled message before it sends.

client.Messaging.RCS.Abort(response.MessageID)

Response

Status(...).Recipients and Received(...).Messages are plain dict lists, not typed objects: access entries with recipient["Key"]/recipient.get("Key") rather than attribute access. See Getting Started for more on this.

SendMessage(...) response

FieldTypeDescription
ResultstrSee Getting Started.
MessageIDstrThe ID of the message you just sent.
ErrorMessagelist[str]See Getting Started. This is where server-side validation errors (e.g. a missing Message/TemplateID) surface, since RCS has no client-side pre-check.

Status(...) response

FieldTypeDescription
ResultstrSee Getting Started.
MessageIDstrThe message this status is for.
JobStatusstrSee Getting Started's Common Response Enums.
JobNumstrTNZ's internal job number for this send.
AccountstrThe TNZ account that owns this job.
SubAccount / DepartmentstrEchoed from the original send.
ReferencestrEchoed from the Reference parameter.
CreatedTimeLocal / CreatedTimeUTC / CreatedTimeUTC_RFC3339strWhen the job was created, in three formats.
DelayedTimeLocal / DelayedTimeUTC / DelayedTimeUTC_RFC3339strThe scheduled send time, if SendTime was set.
TimezonestrTimezone name used for scheduling.
CountintTotal recipients in the job.
CompleteintRecipients processed so far.
Success / FailedintRecipients successfully delivered / failed.
PricestrJob total cost.
TotalRecords / RecordsPerPage / PageCount / PageintPagination metadata for Recipients.
Recipientslist[dict]Per-recipient results, as plain dicts. See table below.
ErrorMessagelist[str]See Getting Started.
Recipient dict (each entry in Recipients)

Each entry is an untyped dict parsed straight from the API's JSON response. These are the keys you can expect to find; see Getting Started for why the SDK doesn't validate or type this shape.

KeyDescription
TypeSee Getting Started's Common Response Enums.
DestSeqTNZ's internal sequence ID for this recipient within the job.
DestinationThe recipient's phone number.
ContactIDAddressbook contact reference, if sent via ContactID/GroupID.
StatusSee Getting Started's Common Response Enums.
ResultHuman-readable delivery result for this recipient.
SentTimeLocal / SentTimeUTCWhen the message was actually sent to this recipient.
Attention / Company / Custom1Custom9Echoed personalisation fields. See Destination Fields above.
RemoteIDCarrier/network-assigned identifier for this delivery, if available.
PricePer-recipient cost.
SMSRepliesInbound replies from this recipient, as a list of dicts. See table below. The key is named SMSReplies even on RCS; it isn't a mistake in this doc.
Reply dict (each entry in SMSReplies)
KeyDescription
ReceivedIDUnique identifier for this reply.
ReceivedTimeLocal / ReceivedTimeUTCWhen the reply was received.
TimezoneTimezone name for ReceivedTimeLocal.
FromThe replying phone number.
MessageTextThe reply body.

Received(...) response

FieldTypeDescription
ResultstrSee Getting Started.
TotalRecords / RecordsPerPage / PageCount / PageintPagination metadata for Messages.
Messageslist[dict]Inbound RCS messages, as plain dicts. See table below.
ErrorMessagelist[str]See Getting Started.
Message dict (each entry in Messages)
KeyDescription
ReceivedIDUnique identifier for this message.
MessageIDThe original outbound message this replies to, if determinable.
JobNumThe original send job's number, if applicable.
SubAccount / DepartmentEchoed billing codes from the original send.
ReceivedTimeLocal / ReceivedTimeUTC / ReceivedTimeUTC_RFC3339When the message was received, in three formats.
FromThe sender's phone number.
ContactIDAddressbook contact reference, if the sender matched one.
MessageTextThe message body.
TimezoneTimezone name for ReceivedTimeLocal.
VersionPayload format version.

Reschedule(...)/Abort(...) response

FieldTypeDescription
ResultstrSee Getting Started.
ActionResultstrHuman-readable result of the action.
MessageIDstrThe message this action was applied to.
JobNumstrThe job number this action was applied to.
StatusstrSee Getting Started's Common Response Enums.
ActionstrThe action performed, e.g. "Reschedule".
ErrorMessagelist[str]See Getting Started.

Reports

client.Reports is a cross-channel convenience wrapper over the same polling methods each channel already exposes directly under client.Messaging.<Channel> (e.g. SMS's Status(...)/Received(...), or TTS's Status(...)). Use it when the channel isn't known until runtime, e.g. you stored a MessageID and Channel pair together in your own database and want to poll status without a big if/elif over every channel in your own code. If you already know the channel at the point you're writing the code, calling it directly on client.Messaging.<Channel> is simpler.

client.Reports exposes three properties, each returning a fresh request object on every access: .Status, .SMSReceived, and .SMSReply.

Methods

MethodSignatureChannelsReturns
Status.PollPoll(Channel, MessageID, RecordsPerPage=20, Page=1)sms, email, fax, tts, voice, whatsapp, rcsThat channel's own status response, e.g. the same response Channel="sms"'s Status(...) returns.
SMSReceived.PollPoll(TimePeriod=None, DateFrom=None, DateTo=None, RecordsPerPage=20, Page=1)SMS onlySame response as client.Messaging.SMS.Received(...)
SMSReply.PollPoll(MessageID, RecordsPerPage=20, Page=1)SMS onlySame response as SMS.Status(...) (see note below)

Channel names are matched case-insensitively. Workflow is not included: it has no Status(...) method to dispatch to. Passing an unknown or unsupported channel to Status.Poll(...) doesn't raise an exception; it returns a Result="Failed" response (see Response below).

Code Samples

Poll for status, channel resolved at runtime

The channel doesn't have to be a literal in your code; here it's read back out of a dictionary you might have stored alongside the MessageID yourself.

stored_job = {"Channel": "sms", "MessageID": "ID123456"}

response = client.Reports.Status.Poll(
    Channel=stored_job["Channel"],
    MessageID=stored_job["MessageID"]
)

if response.Result == "Success":
    print(f"JobStatus: {response.JobStatus}")
    for recipient in response.Recipients:
        print(f" -> {recipient['Destination']}: {recipient['Status']} ({recipient['Result']})")

Poll for inbound SMS

Retrieve SMS replies received in a given date range, as an alternative to configuring a webhook.

response = client.Reports.SMSReceived.Poll(
    DateFrom="2026-07-01 00:00:00",
    DateTo="2026-08-01 00:00:00"
)

if response.Result == "Success":
    for message in response.Messages:
        print(f"From {message['From']}: {message['MessageText']}")

Poll for replies to a specific message

SMSReply.Poll(...) is a thin alias over SMS's own Status(...): there is no separate reply-shaped response type in v300. Replies show up on Recipients[].SMSReplies, exactly as they would if you'd called client.Reports.Status.Poll(Channel="sms", ...) or client.Messaging.SMS.Status(...) directly. client.Messaging.SMS.Reply(...) is the same alias, one level closer to the channel; reach for the Reports-facade version here only if the channel isn't a hardcoded literal at your call site.

response = client.Reports.SMSReply.Poll(MessageID="ID123456")

if response.Result == "Success":
    for recipient in response.Recipients:
        for reply in (recipient.SMSReplies or []):
            print(f"{recipient.Destination} replied: {reply.MessageText}")

Response

Every response has a Result field; check response.Result == "Success" before reading other fields, and read response.ErrorMessage on failure. See Getting Started.

Status.Poll(...)

Returns exactly what the resolved channel's own Status(...) returns: same shape, same fields, nothing added or removed. Rather than duplicate that field table here (and risk it drifting out of sync), see the channel's own Response section, e.g. SMS for Channel="sms" or TTS for Channel="tts".

SMSReceived.Poll(...)

Returns the same response as client.Messaging.SMS.Received(...). See SMS's Response section for the full field table.

SMSReply.Poll(...)

Returns the same response as Status.Poll(Channel="sms", ...) and client.Messaging.SMS.Status(...)/Reply(...) above. There is no distinct "reply" response shape; see SMS's Response section, and look at Recipients[].SMSReplies specifically. As documented there, SMS is the one channel where these are typed objects, not plain dicts.

Unknown channel

The only response shape unique to this facade, returned when Status.Poll(...) is given a Channel it doesn't recognise (including "workflow", which is never valid here).

FieldTypeDescription
ResultstrAlways "Failed".
ErrorMessagelist[str]e.g. ["Unknown or unsupported channel for Status: carrierpigeon"].

Actions

client.Actions is a cross-channel dispatcher over the same action methods each channel already exposes directly under client.Messaging.<Channel> (e.g. SMS's Reschedule(...)/Abort(...), or TTS's Pacing(...)). Use it when the channel is a runtime variable rather than a value you hardcode, for the same reason as Reports above: a job's channel stored alongside its MessageID in your own system, one shared "cancel this job" code path across every channel, and so on. If you already know the channel at the point you're writing the code, calling it directly on client.Messaging.<Channel> is simpler.

client.Actions exposes four properties, each returning a fresh request object on every access: .Abort, .Reschedule, .Resubmit, and .Pacing. Each takes a Channel and MessageID, matched case-insensitively, plus whatever extra parameter that action needs.

Actions Support by Channel

Not every action is valid on every channel. Abort and Reschedule work everywhere; Resubmit and Pacing are narrower. This mirrors the per-channel support in Getting Started's Actions Support by Channel table.

ChannelAbortRescheduleResubmitPacing
SMS
Email
TTS
Voice
Fax
WhatsApp
RCS

Workflow doesn't appear in this table: it has no action methods to dispatch to, on this facade or on client.Messaging.Workflow directly.

Methods

MethodSignatureChannels
Abort.SendRequestSendRequest(Channel, MessageID)sms, email, fax, tts, voice, whatsapp, rcs
Reschedule.SendRequestSendRequest(Channel, MessageID, SendTime)sms, email, fax, tts, voice, whatsapp, rcs
Resubmit.SendRequestSendRequest(Channel, MessageID, SendTime)email, fax, tts, voice only
Pacing.SendRequestSendRequest(Channel, MessageID, NumberOfOperators)tts, voice only

Abort and Reschedule accept any of the 7 channels above; anything else (a typo, or "workflow") fails with ErrorMessage "Unknown or unsupported channel for <Action>: <Channel>". Resubmit and Pacing work differently. There's no separate "unknown channel" check for them, just a single allow-list gate: any Channel outside their own shorter list (a channel that's valid elsewhere, like sms for Pacing, or a genuine typo) fails with the same "<Action> is not supported for channel: <Channel>" message. There's no distinct "unknown channel" wording for these two. All four failures return the same error response shape; see Response below.

Code Samples

Abort a job, channel resolved at runtime

stored_job = {"Channel": "fax", "MessageID": "ID123456"}

response = client.Actions.Abort.SendRequest(
    Channel=stored_job["Channel"],
    MessageID=stored_job["MessageID"]
)

if response.Result == "Success":
    print(f"Action: {response.Action}, Status: {response.Status}")

Reschedule a job

response = client.Actions.Reschedule.SendRequest(
    Channel="sms",
    MessageID="ID123456",
    SendTime="2026-08-01T09:00:00"
)

if response.Result == "Success":
    print(f"Action: {response.Action}, Status: {response.Status}")

Resubmit a failed job

Resubmit only supports email/fax/tts/voice; sms/whatsapp/rcs aren't resubmittable.

response = client.Actions.Resubmit.SendRequest(
    Channel="fax",
    MessageID="ID123456",
    SendTime="2026-08-01T09:00:00"
)

if response.Result == "Success":
    print(f"Action: {response.Action}, Status: {response.Status}")

Adjust pacing

response = client.Actions.Pacing.SendRequest(
    Channel="tts",
    MessageID="ID123456",
    NumberOfOperators=10
)

if response.Result == "Success":
    print(f"Action: {response.Action}, Status: {response.Status}")

Calling an action on a channel it doesn't support

Pacing only makes sense for tts/voice: there's no such thing as "operator pacing" for an SMS job. Calling it with Channel="sms" doesn't raise an exception; it comes back as a normal Result="Failed" response, so you can handle it the same way as any other failure.

response = client.Actions.Pacing.SendRequest(
    Channel="sms",
    MessageID="ID123456",
    NumberOfOperators=10
)

if response.Result != "Success":
    print(response.ErrorMessage)
    # ["Pacing is not supported for channel: sms"]

Response

Every response has a Result field; check response.Result == "Success" before reading other fields, and read response.ErrorMessage on failure. See Getting Started.

Success

All four methods return the resolved channel's own action-result response on success, e.g. the same response shape Channel="sms" returns from its own action methods. The shape is identical across all seven channels, so it's shown once here rather than seven times; see each channel's own Response section (e.g. SMS, TTS) if you want it documented alongside that channel's other responses.

FieldTypeDescription
Resultstr"Success" or "Failed". See Getting Started.
ActionResultstrHuman-readable result of the action.
MessageIDstrThe message this action was applied to.
JobNumstrThe job number this action was applied to.
StatusstrThe job's status after the action, e.g. "Pending", "Delayed", "Completed".
ActionstrThe action performed, e.g. "Reschedule".
ErrorMessagelist[str]Empty on success. See Getting Started.

Failure

Returned instead of a channel-specific result whenever Channel isn't recognised at all, or isn't in the calling action's own allow-list (the Resubmit/Pacing case above). This shape is unique to client.Actions; the direct client.Messaging.<Channel> calls don't have an equivalent, since the channel is always valid by construction there.

FieldTypeDescription
ResultstrAlways "Failed".
ErrorMessagelist[str]e.g. ["Unknown or unsupported channel for Abort: carrierpigeon"] or ["Pacing is not supported for channel: sms"].

Inbound Webhooks

TNZ's webhooks notify your application when a message completes sending, or when you receive an inbound SMS, instead of you polling Status(...)/Received(...) on a timer. This keeps delivery receipts and customer replies flowing to your system without the extra request load that polling adds.

Webhooks are inbound: TNZ's servers POST these payloads to your own server (configure WebhookCallbackURL on a send, or your Sender's Dashboard settings for SMS-reply/result reporting). The SDK doesn't call anything for this; instead, tnzapi.webhooks provides typed dataclass shapes you can construct from an incoming request body in your own webhook receiver endpoint.

Security note: TNZ does not sign or otherwise authenticate these POSTs: no HMAC or shared-secret header is provided. Anyone who discovers your WebhookCallbackURL can post a payload to it. Don't expose a receiver like the ones below on a public route without adding your own verification, e.g. a random, hard-to-guess path segment or query token, an IP allowlist for TNZ's sending range, or routing the callback through your own authenticated proxy.

Payload Fields

ResultWebhookPayload and InboundSMSWebhookPayload share the exact same field set, both dataclasses subclassing a shared private base. Every field on both classes is a plain str, defaulting to None. They're kept as two distinct classes purely so a receiver's type hints communicate which kind of event it's handling, even though the wire shape happens to be identical today.

FieldTypeDescription
VersionstrWebhook payload format version.
SenderstrYour TNZ Sender ID.
APIKeystrYour API key, included for correlation/validation.
TypestrMessage channel type (e.g. "SMS").
DestinationstrThe recipient (or, for inbound SMS, the sender) address/number.
ContactIDstrAddressbook contact reference, if the destination matched one.
ReceivedIDstrIdentifier for this specific inbound/result event.
MessageIDstrThe original outbound message this event relates to.
SubAccountstrSub-account code, echoed from the original send.
DepartmentstrDepartment code, echoed from the original send.
JobNumberstrJob number for the send batch.
SentTimeLocal / SendTimeUTC / SentTimeUTC_RFC3339strEvent timestamp in local time, UTC, and RFC3339 UTC respectively. Note the middle field is named SendTimeUTC, not SentTimeUTC (a real inconsistency versus its SentTime* neighbours), reproduced here exactly as the SDK defines it.
StatusstrDelivery/message status.
ResultstrResult code/description for this event.
MessagestrThe message text (the reply body for inbound SMS).
PricestrSee the Price note below.
DetailstrAdditional detail text for this event.
URLstrRelated URL, if applicable.

Both classes are plain @dataclasses with no tolerance for unexpected keys: constructing one from a payload that includes a field neither class declares raises TypeError, unlike the tolerant field-by-field parsing this SDK's own HTTP responses go through internally. If TNZ ever adds a new field to the wire payload before this SDK is updated to match, filter the parsed body down to known keys yourself before constructing the dataclass, e.g. {k: v for k, v in body.items() if k in ResultWebhookPayload.__dataclass_fields__}.

Code Samples

Delivery result webhook

Handle the payload TNZ posts when a message completes sending, whatever the outcome, as an alternative to polling Status(...). RequestBody is the already-JSON-decoded body your web framework gave you, e.g. Flask's request.get_json() or Django's json.loads(request.body).

from tnzapi.webhooks import ResultWebhookPayload

payload = ResultWebhookPayload(**RequestBody)

print(f"{payload.Type} to {payload.Destination}: {payload.Status} ({payload.Result})")

Inbound SMS webhook

Handle the payload TNZ posts when a recipient replies to an SMS, as an alternative to polling Received(...).

from tnzapi.webhooks import InboundSMSWebhookPayload

payload = InboundSMSWebhookPayload(**RequestBody)

print(f"Inbound SMS from {payload.Destination}: {payload.Message}")

Flask receiver example

Wiring both payload types into two routes of a small Flask app. This SDK has no Flask dependency itself; any framework that hands you the parsed JSON body works the same way.

from flask import Flask, request
from tnzapi.webhooks import ResultWebhookPayload, InboundSMSWebhookPayload

app = Flask(__name__)

@app.route("/webhooks/tnz/result", methods=["POST"])
def tnz_result_webhook():
    body = request.get_json(silent=True)
    if body is None:
        return "", 400
    payload = ResultWebhookPayload(**body)
    print(f"{payload.MessageID} is now {payload.Status}")
    return "", 204

@app.route("/webhooks/tnz/inbound-sms", methods=["POST"])
def tnz_inbound_sms_webhook():
    body = request.get_json(silent=True)
    if body is None:
        return "", 400
    payload = InboundSMSWebhookPayload(**body)
    print(f"Reply from {payload.Destination}: {payload.Message}")
    return "", 204

Note: Price is annotated str on both payload types, but ResultWebhookPayload/InboundSMSWebhookPayload are plain @dataclasses with no validation or type coercion: the annotation is a type hint only, not enforced at runtime. TNZ's webhook callback may send Price as either a JSON string or a JSON number; if it arrives as a number, payload.Price will actually hold an int/float at runtime despite the annotation, since ResultWebhookPayload(**body) assigns whatever value body["Price"] is verbatim. Normalise it explicitly if you need a consistent type, e.g. Decimal(str(payload.Price)).

Addressbook

Centralise your contacts with the Addressbook: a single source of truth that simplifies your integration and enables data-rich personalisation across every messaging channel. Manage contacts, groups, and contact–group relationships; keep contact data synchronised with your CRM, HR system, or spreadsheets; organise contacts into groups to message thousands with a single GroupID; and reduce payload size by referencing a ContactID/GroupID instead of sending full recipient details on every send. The same ContactID/GroupID values documented here work directly as ContactID=/GroupID= arguments to AddDestination(...) on any messaging channel, e.g. SMS. Personalisation fields (FirstName, Company, Custom1Custom4) work as [[FirstName]]-style merge tags in your message body, and contacts/groups created here are also usable directly in the TNZ Dashboard. Groups have no custom fields of their own; personalisation always comes from the contact.

client.Addressbook exposes four properties, each returning a fresh request object on every access: .Contact, .Group, .ContactGroup, and .GroupContact. This is flat: contact–group relationships are managed through the separate ContactGroup/GroupContact properties directly off client.Addressbook, not through nested chaining off Contact or Group themselves.

Contact

Fields

FieldTypeDescription
ExTypestrExternal system type tag, for correlating this contact with your own CRM/system.
ExIDstrExternal system ID, for correlating this contact with your own CRM/system.
ViewBystrWho can view this contact in the Dashboard: "Account", "SubAccount", "Department", or "No". Plain string; Python has no equivalent of Enums.ViewEditByOptions.
EditBystrWho can edit this contact in the Dashboard, same values as ViewBy.
AccessControlstr"Limited" or "Granted".
AttentionstrPersonalisation token [[Attention]].
Titlestre.g. "Mr", "Dr".
CompanystrPersonalisation token [[Company]].
RecipDepartmentstrThe contact's department at their company. Not related to your TNZ Department code.
FirstNamestrPersonalisation token [[FirstName]].
LastNamestrPersonalisation token [[LastName]].
PositionstrJob title.
StreetAddress / Suburb / City / State / Country / PostcodestrPostal address fields.
MainPhonestrPrimary phone number.
AltPhone1AltPhone8strUp to 8 additional phone numbers.
MobilePhonestrMobile number, used as the SMS/WhatsApp/RCS destination when sending via ContactID.
FaxNumberstrFax destination.
EmailAddressstrEmail destination.
WebAddressstrWebsite URL.
Custom1Custom4strPersonalisation tokens [[Custom1]][[Custom4]].
NotesstrFree-text notes. Not exposed as a personalisation token.

There's no DirectPhone field here: tnzapi's contact fields stop at MainPhone and AltPhone1AltPhone8.

Create

Pass fields as keyword arguments directly to Create(...), or build one first with Set(...)/Build() if you want to construct the request ahead of time and pass it as model=. Both are equivalent; the samples below use plain keyword arguments, matching the rest of this SDK. model= also accepts a response object from an earlier call (e.g. Detail(...)), useful for cloning a contact: only the fields the request actually declares are copied across, so read-only fields like ContactID/Owner/timestamps are dropped automatically rather than causing an error.

response = client.Addressbook.Contact.Create(
    Attention="API Test",
    FirstName="API",
    LastName="Test",
    MobilePhone="+64211231234",
    EmailAddress="test@example.com",
    MainPhone="+6491112222"
)

if response.Result == "Success":
    print(f"Created ContactID={response.ContactID}")

Detail

Look up a contact's stored fields by ContactID. Note the method name is singular Detail(...) here, not Details(...), a real naming inconsistency versus OptOut's Details(...) elsewhere in this SDK, worth double-checking if you're switching between the two. An empty or missing ContactID raises ValueError rather than silently sending a broken request.

details = client.Addressbook.Contact.Detail(response.ContactID)

if details.Result == "Success":
    print(f"{details.FirstName} {details.LastName}, {details.EmailAddress}")

Update and Delete

Change or remove a contact. Update(...)/Delete(...) take either a plain ContactID string or the response object a prior call returned, extracting the ID automatically in the latter case. Update(...) is a partial PATCH: only the fields you pass are changed.

updated = client.Addressbook.Contact.Update(response, Company="Example Company")

client.Addressbook.Contact.Delete(response)

# A plain ContactID string still works too, e.g. one stored from an earlier session
client.Addressbook.Contact.Update(response.ContactID, Company="Example Company")

Search and List

Find contacts by any combination of EmailAddress (full match) or MobilePhone/MainPhone/Attention/FirstName/LastName/Company (partial match), or page through your full contact list. Both default to RecordsPerPage=20, Page=1 and only cover the requested page. This SDK never auto-walks every page on your behalf.

results = client.Addressbook.Contact.Search(
    FirstName="Alice",
    Company="Example Company",
    RecordsPerPage=100,
    Page=1
)

if results.Result == "Success":
    for contact in results.Contacts:
        print(f"{contact.get('ContactID')}: {contact.get('FirstName')} {contact.get('LastName')}")

page = client.Addressbook.Contact.List(RecordsPerPage=100, Page=1)

if page.Result == "Success":
    for contact in page.Contacts:
        print(f"{contact.get('ContactID')}: {contact.get('FirstName')} {contact.get('LastName')}")

Group

Fields

FieldTypeDescription
GroupNamestrDisplay name for the group.
SubAccountstrSub-account code.
DepartmentstrDepartment code.
ViewEditBystrWho can view and edit this group in the Dashboard: "Account", "SubAccount", "Department", or "No". Unlike Contact, Group has one combined permission rather than separate ViewBy/EditBy fields.
AccessControlstr"Limited" or "Granted".

Create

Same keyword-argument pattern as Contact. GroupCode is server-assigned; it's returned on the response, not something you set.

response = client.Addressbook.Group.Create(
    GroupName="API Test Group",
    SubAccount="SALES",
    ViewEditBy="SubAccount"
)

if response.Result == "Success":
    print(f"Created GroupID={response.GroupID}, GroupCode={response.GroupCode}")

Detail, Update, Delete, and List

Manage a group the same way as a contact: look up, rename, remove, or page through all groups. Like Contact, the lookup method is singular Detail(...), and Update(...)/Delete(...) accept either a GroupID string or the response object itself.

details = client.Addressbook.Group.Detail(response.GroupID)
client.Addressbook.Group.Update(response, GroupName="Renamed Group")
client.Addressbook.Group.Delete(response)

page = client.Addressbook.Group.List(RecordsPerPage=100, Page=1)

Contact ↔ Group relationships

ContactGroup (contact's-side view) and GroupContact (group's-side view) both manage the same underlying membership, and both support the same four operations: List(...), Create(...), Delete(...), and Detail(...). Three things worth noting: the method names are Create/Delete, not Add/Remove; neither class has a Set(...)/Build() builder, both take their arguments as plain positional/keyword parameters; and neither accepts a response object in place of an ID the way Contact/Group do, every ContactID/GroupID here must be a plain string (an empty or missing one raises ValueError).

ContactGroup.Create(ContactID, GroupID) and GroupContact.Create(GroupID, ContactID) dispatch to the exact same underlying endpoint; only the Python argument order differs (which ID comes first), matching whichever side reads more naturally at your call site. The same is true of their respective Delete(...) methods.

# Groups a contact belongs to
groups = client.Addressbook.ContactGroup.List(contactID)

# Add a contact to a group, from the contact's side
add_result = client.Addressbook.ContactGroup.Create(contactID, groupID)

if add_result.Result == "Success":
    print(f"Added to group: {add_result.Group.get('GroupName')}")

# Look up a single contact-group relation
relation = client.Addressbook.ContactGroup.Detail(contactID, groupID)

# Remove a contact from a group
client.Addressbook.ContactGroup.Delete(contactID, groupID)

# Contacts belonging to a group
contacts = client.Addressbook.GroupContact.List(groupID)

if contacts.Result == "Success":
    for contact in contacts.Contacts:
        print(f"{contact.get('FirstName')} {contact.get('LastName')}")

# Add a contact to a group, from the group's side (same wire endpoint as above)
group_add_result = client.Addressbook.GroupContact.Create(groupID, contactID)

if group_add_result.Result == "Success":
    contact = group_add_result.Contact
    print(f"Added {contact.get('FirstName')} {contact.get('LastName')} to group")

# Remove a contact from a group, from the group's side
client.Addressbook.GroupContact.Delete(groupID, contactID)

# Look up a single group-contact relation
group_relation = client.Addressbook.GroupContact.Detail(groupID, contactID)

Note: Detail(...) on both ContactGroup and GroupContact has no dedicated wire endpoint behind it. It's synthesised client-side: internally it calls List(...) for one page and scans the results for a matching GroupID/ContactID, returning Result="RecordNotFound" if it isn't on that page. It deliberately does not auto-paginate to keep searching: that could mean an unbounded number of HTTP calls for a contact or group with many memberships. If you suspect the match is on a later page, inspect List(...)'s PageCount yourself and pass a larger RecordsPerPage or a specific Page. This is also why Detail(...)'s default RecordsPerPage is 100, not the 20 used everywhere else in this SDK: a larger default page reduces the odds of a false RecordNotFound on a contact/group with many memberships.

Response

Every Addressbook result carries Result and ErrorMessage: check Result == "Success" before reading other fields; see Getting Started. List-style results additionally carry TotalRecords/RecordsPerPage/PageCount/Page (all int) for pagination. As with the rest of this SDK, list/nested fields below are plain dict objects, not typed classes: access them with contact.get("FirstName") or contact["FirstName"], not attribute access.

Contact.Create(...)/Detail(...)/Update(...)/Delete(...) response

FieldTypeDescription
ResultstrSee Getting Started.
ErrorMessagelist[str]See Getting Started.
ContactIDstrThe contact's ID.
OwnerstrThe TNZ user who owns this contact.
CreatedTimeLocal / CreatedTimeUTC / CreatedTimeUTC_RFC3339strWhen the contact was created, in local time, UTC, and RFC3339 UTC respectively.
UpdatedTimeLocal / UpdatedTimeUTC / UpdatedTimeUTC_RFC3339strWhen the contact was last updated.
TimezonestrTimezone the local timestamps above are expressed in.
Groupslist[dict]The groups this contact belongs to.
every Contact field abovestrEchoed back, e.g. FirstName, EmailAddress, Custom1Custom4. See the Fields table above.

Contact.Search(...)/List(...) response

FieldTypeDescription
ResultstrSee Getting Started.
ErrorMessagelist[str]See Getting Started.
Contactslist[dict]The matching contacts for this page, each dict shaped like Contact.Detail(...)'s result fields.

Group.Create(...)/Detail(...)/Update(...)/Delete(...) response

FieldTypeDescription
ResultstrSee Getting Started.
ErrorMessagelist[str]See Getting Started.
GroupIDstrThe group's ID.
GroupCodestrServer-assigned lookup code. Read-only; there's no matching field in the Fields table above to set it.
GroupName / SubAccount / Department / ViewEditBy / AccessControlstrEchoed back. See the Fields table above.
OwnerstrThe TNZ user who owns this group.
CreatedTimeLocal / CreatedTimeUTC / CreatedTimeUTC_RFC3339strWhen the group was created. Unlike Contact, Group has no Updated* timestamps.
TimezonestrTimezone the local timestamp above is expressed in.

Group.List(...) response

FieldTypeDescription
ResultstrSee Getting Started.
ErrorMessagelist[str]See Getting Started.
Groupslist[dict]The groups for this page, each dict shaped like Group.Detail(...)'s result fields.

ContactGroup.List(...) response

FieldTypeDescription
ResultstrSee Getting Started.
ErrorMessagelist[str]See Getting Started.
ContactdictThe contact whose groups you're listing.
Groupslist[dict]The groups this contact belongs to.

GroupContact.List(...) response

FieldTypeDescription
ResultstrSee Getting Started.
ErrorMessagelist[str]See Getting Started.
GroupdictThe group whose members you're listing.
Contactslist[dict]The contacts belonging to this group.

ContactGroup.Create(...)/Delete(...)/Detail(...) response

FieldTypeDescription
ResultstrSee Getting Started. Detail(...) can also return "RecordNotFound". See the note above.
ErrorMessagelist[str]See Getting Started.
ContactdictThe contact side of this relation.
GroupdictThe group side of this relation.

GroupContact.Create(...)/Delete(...)/Detail(...) response

FieldTypeDescription
ResultstrSee Getting Started. Detail(...) can also return "RecordNotFound". See the note above.
ErrorMessagelist[str]See Getting Started.
GroupdictThe group side of this relation.
ContactdictThe contact side of this relation.

These last two result types carry no pagination fields, unlike the List results above; each always describes exactly one contact–group relation.

OptOut

Programmatically manage contacts who have unsubscribed, so you can meet your obligations under anti-spam regulations. Opt-outs are managed on a SubAccount, Department, and message-type (DestType) basis: a contact can opt out of SMS marketing while still receiving important Email alerts, since each channel is suppressed independently. Access it directly as client.OptOut: there's no intermediate Configuration facade in this SDK.

Once a destination is opted out, sending to it doesn't fail outright: the API accepts the request, but delivery is blocked and the response reports a "Destination is blacklisted" result. This gives you a clear audit trail rather than a hard error. Use List(...) below to retrieve the full opt-out list for auditing, reporting, or syncing with your own CRM.

DestType is a plain string field, not an enum type: Python has no equivalent of Enums.OptOutDestType. Accepted values, matched case-insensitively, are fax, text, sms, email, speech, and voice; sms is accepted as an alias of text, and voice as an alias of speech. Comma-join multiple values in a single string to apply to more than one channel at once, e.g. "SMS,Email"; whitespace around each comma-separated part is tolerated on the way in, and stripped before the value is sent to the API. Any value outside this list, on its own or as part of a comma-joined string, fails validation client-side with Result="Failed" before any request is sent.

Fields

FieldTypeDescription
DestTypestrRequired. The channel this entry applies to. See the accepted values above.
DestinationstrThe destination to suppress, e.g. "+6421003004" or an email address. Use Destination or ContactID, not both.
ContactIDstrOpt out an addressbook contact instead of a raw destination. Use Destination or ContactID, not both.
SubAccountstrScope this entry to a sub-account. Empty applies to all sub-accounts.
DepartmentstrScope this entry to a department. Empty applies to all departments.
StopMessagestrThe opt-out phrase detected, e.g. "Stop sending me these messages".
NotesstrFree-text notes.

Code samples

Create a single OptOut entry

Suppress future sends to one destination on a specific channel. DestType and either Destination or ContactID are required; Create(...) validates both client-side before sending anything.

response = client.OptOut.Create(
    DestType="SMS",
    Destination="+6421003004",
    Notes="Requested via support call"
)

if response.Result == "Success":
    print(f"Created OptOut ID={response.ID}")

Opt out an addressbook contact

Suppress an addressbook contact by ContactID instead of a raw destination. Set ContactID or Destination, not both.

response = client.OptOut.Create(
    DestType="Email",
    ContactID=contactID
)

Opt out multiple destinations at once

CreateBatch(DestType, Destination=None, Destinations=None, ContactID=None, ContactIDs=None, SubAccount=None, Department=None) takes explicit parameters rather than a builder or model, a different calling convention from every other method on this page. It requires DestType plus at least one of Destination, Destinations, ContactID, or ContactIDs; SubAccount/Department apply the same scoping as on a single entry, but there's no StopMessage/Notes on the batch call.

batch_response = client.OptOut.CreateBatch(
    DestType="SMS",
    Destinations=["+6421003004", "+6421003005"]
)

if batch_response.Result == "Success":
    print(f"Batch opt-out created: ID={batch_response.ID}")

Note: CreateBatch(...) returns a single response object (the same shape as Create(...)/Details(...), with one ID field), not a list of one entry per destination. If you need the individual opt-out records the batch created, look them up afterwards with List(...) filtered by DestType/ContactID, or by the destinations you originally submitted.

Update

Change the notes or scoping on an existing entry. Update(...) takes either a plain OptOutID string or the response object a prior call returned, extracting its ID field automatically in the latter case. This is a partial PATCH: only the fields you pass are changed, and unlike Create(...), DestType is only validated if you actually supply it in the call. DestType and Destination/ContactID can be changed the same way, though moving an entry to a different destination is unusual; deleting and re-creating is more common for that case.

updated = client.OptOut.Update(response, Notes="Confirmed via follow-up call")

# A plain OptOutID string still works too, e.g. one stored from an earlier session
client.OptOut.Update(response.ID, Notes="Confirmed via follow-up call")

Details, Delete, and List

Look up a single entry, remove an opt-out, or page through the full suppression list, filtering by channel with a plain DestType string. The lookup method here is plural Details(...), a real naming inconsistency versus Addressbook's singular Detail(...) elsewhere in this SDK, worth double-checking if you're switching between the two. Details(...) takes a plain OptOutID string only (an empty or missing one raises ValueError); Delete(...) accepts either the string or the response object, same as Update(...) above.

details = client.OptOut.Details(response.ID)
client.OptOut.Delete(response)

list_result = client.OptOut.List(DestType="SMS", TimePeriod=30)

if list_result.Result == "Success":
    for entry in list_result.OptOuts:
        print(f"{entry.get('Destination')}, {entry.get('DestType')}")

List(...)'s signature is List(TimePeriod=None, DestType=None, ContactID=None, Page=1, RecordsPerPage=100). Two details are easy to miss if you're used to the rest of this SDK: Page comes before RecordsPerPage in the parameter order, and the default RecordsPerPage is 100, not the 20 used by every other paginated method. Both differences matter if you're calling positionally rather than by keyword.

Response

Every OptOut result carries Result and ErrorMessage: check Result == "Success" before reading other fields; see Getting Started.

Create(...)/Details(...)/Update(...)/Delete(...)/CreateBatch(...) response

FieldTypeDescription
ResultstrSee Getting Started.
ErrorMessagelist[str]See Getting Started.
IDstrThis entry's ID. Note the field is plain ID, not OptOutID.
DestType / Destination / ContactID / SubAccount / Department / StopMessage / NotesstrEchoed back. See the Fields table above.
OriginalMessagestrThe original inbound message that triggered the opt-out, when it was created automatically from a [[STOP]]-style reply rather than via this API.
CreatedTimeLocal / CreatedTimeUTC / CreatedTimeUTC_RFC3339strWhen the entry was created, in local time, UTC, and RFC3339 UTC respectively.
UpdatedTimeLocal / UpdatedTimeUTC / UpdatedTimeUTC_RFC3339strWhen the entry was last updated.
TimezonestrTimezone the local timestamps above are expressed in.

CreateBatch(...) returns this same response shape. See the note above the code sample above; it is not a list of the destinations submitted.

List(...) response

FieldTypeDescription
ResultstrSee Getting Started.
ErrorMessagelist[str]See Getting Started.
TotalRecords / RecordsPerPage / PageCount / PageintPagination.
OptOutslist[dict]The matching entries for this page, each dict shaped like Details(...)'s result fields above. As elsewhere in this SDK, these are plain dicts: use entry.get("Destination"), not attribute access.