Skip to main content

TNZ REST API Helper Library for Python

Project description

tnzapi

tnzapi is the official Python helper library for the TNZ Group REST API — send SMS, Email, Fax, TTS (text-to-speech), Voice, WhatsApp, RCS and Workflow messages, manage opt-outs, retrieve delivery reports, amend in-flight jobs, and manage your address book, all through a small, consistent Python surface over the v3.00 JSON API.

Documentation

The documentation for the TNZ API can be found here.

Full parameter reference for this library is under docs/ — one page per messaging channel plus Addressbook, Opt-Out Management, and Webhooks, including every field, type, and destination shape not covered by this README's quick examples.

Versions

tnzapi uses a modified version of Semantic Versioning for all changes. See this document for details. See CHANGELOG.md for what's changed release to release.

Supported Python Versions

This library supports the following Python implementations:

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

Installation

Install from PyPi using pip, a package manager for Python.

pip install tnzapi

Don't have pip installed? Try installing it, by running this from the command line:

$ curl https://raw.github.com/pypa/pip/master/contrib/get-pip.py | python

python setup.py install

You may need to run the above commands with sudo.

Configuration

Every v3.00 facade (Messaging, Reports, Actions, Addressbook, OptOut) authenticates with a Bearer AuthToken — pass it directly to the TNZAPI constructor, or leave it out and set it via environment variable instead:

from tnzapi import TNZAPI

client = TNZAPI(
    AuthToken="[Your Auth Token]"
)
Environment variable Purpose
TNZ_AUTH_TOKEN Used when AuthToken isn't passed to the TNZAPI constructor.
TNZ_API_URL Overrides the default API base URL (https://api.tnz.co.nz/api/v3.00) — useful for pointing at a local/staging server.
TNZ_ALLOW_INSECURE_HTTP Set to true to allow a non-HTTPS TNZ_API_URL (local dev only). By default, any base URL that isn't https:// is rejected so the Bearer token is never sent over plain HTTP.

Quick Start

from tnzapi import TNZAPI

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

response = client.Messaging.SMS.SendMessage(
    Message="Test SMS Message click [[Reply]] to opt out",
    Destination="+64211231234",
    Reference="Test"
)

if response.Result == "Success":
    print(response.MessageID)
else:
    print(response.ErrorMessage)

Messaging

Parameter naming: every request-class method (Status, Reschedule, Received, List, etc.) takes PascalCase keyword arguments (MessageID=, RecordsPerPage=, ...), matching the field names used throughout this SDK's DTOs. The previous snake_case names (message_id=, records_per_page=, ...) still work and will keep working - they're translated internally with a DeprecationWarning - but new code should use the PascalCase form.

Every messaging channel supports two equivalent calling styles:

  • Flat kwargs — pass every field to SendMessage(...) in one call.
  • Builder — chain Set(...)/AddDestination(...)/AddAttachment(...) calls and finish with SendMessage() (no arguments). Build() returns the underlying request DTO without sending, if you want to inspect or reuse it.

SMS

# Flat kwargs
response = client.Messaging.SMS.SendMessage(
    Message="Test SMS Message click [[Reply]] to opt out",
    Destinations=[{"ToNumber": "+64211231234"}],
    Reference="Test"
)

# Builder
response = (
    client.Messaging.SMS
    .Set(
        Message="Test SMS Message click [[Reply]] to opt out",
        Reference="Test"
    )
    .AddDestination("+64211231234")
    .SendMessage()
)

Poll for replies to a sent message:

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}")

Full reference: docs/sms.md

Email

# Flat kwargs
response = client.Messaging.Email.SendMessage(
    EmailSubject="Test Email",
    MessagePlain="Hi world!",
    Destinations=[{"EmailAddress": "recipient@example.com"}]
)

# Builder
response = (
    client.Messaging.Email
    .Set(
        EmailSubject="Test Email",
        MessagePlain="Hi world!"
    )
    .AddDestination("recipient@example.com")
    .SendMessage()
)

Full reference: docs/email.md

Fax

# Flat kwargs
response = client.Messaging.Fax.SendMessage(
    Destinations=[{"ToNumber": "+6491232345"}],
    Files=[{"Name": "Document.pdf", "Data": "<base64-encoded file data>"}]
)

# Builder
response = (
    client.Messaging.Fax
    .Set()
    .AddDestination("+6491232345")
    .AddAttachment("path/to/doc.pdf")
    .SendMessage()
)

Full reference: docs/fax.md

TTS

# Flat kwargs
response = client.Messaging.TTS.SendMessage(
    MessageToPeople="Hi there!",
    Destinations=[{"ToNumber": "+64211232345"}],
    Reference="Voice Test - 64211232345",
    Keypads=[{"Tone": 1, "Play": "You pressed 1", "RouteNumber": "+6491232345"}]
)

# Builder
response = (
    client.Messaging.TTS
    .Set(
        MessageToPeople="Hi there!",
        Reference="Voice Test - 64211232345"
    )
    .AddDestination("+64211232345")
    .AddKeypad(
        Tone=1,
        Play="You pressed 1",
        RouteNumber="+6491232345"
    )
    .SendMessage()
)

Full reference: docs/tts.md

Voice

# Flat kwargs
response = client.Messaging.Voice.SendMessage(
    MessageToPeople="path/to/audio.wav",
    MessageToAnswerPhones="path/to/audio.wav",
    Destinations=[{"ToNumber": "+64211232345"}],
    Reference="Voice Test - 64211232345",
    Keypads=[{"Tone": 1, "RouteNumber": "+6491232345"}]
)

# Builder
response = (
    client.Messaging.Voice
    .Set(
        MessageToPeople="path/to/audio.wav",
        Reference="Voice Test - 64211232345"
    )
    .AddDestination("+64211232345")
    .AddKeypad(
        Tone=1,
        RouteNumber="+6491232345"
    )
    .SendMessage()
)

Full reference: docs/voice.md

WhatsApp

WhatsApp requires an approved TemplateID and a FromNumber (your registered WhatsApp sender number):

# Flat kwargs
response = client.Messaging.WhatsApp.SendMessage(
    TemplateID="[Your Template ID]",
    Message="Hi there!",
    FromNumber="+6495006000",
    Destination="+64211234567"
)

# Builder
response = (
    client.Messaging.WhatsApp
    .Set(
        TemplateID="[Your Template ID]",
        Message="Hi there!",
        FromNumber="+6495006000"
    )
    .AddDestination("+64211234567")
    .SendMessage()
)

Full reference: docs/whatsapp.md

RCS

# Flat kwargs
response = client.Messaging.RCS.SendMessage(
    Message="Hi there!",
    Destination="+64211234567"
)

# Builder
response = (
    client.Messaging.RCS
    .Set(
        Message="Hi there!"
    )
    .AddDestination("+64211234567")
    .SendMessage()
)

Full reference: docs/rcs.md

Workflow

Workflow requires a WorkflowTemplateID — the send is driven entirely by the template configured against your account:

# Flat kwargs
response = client.Messaging.Workflow.SendMessage(
    WorkflowTemplateID="[Your Workflow Template ID]",
    Destination="+64211234567"
)

# Builder
response = (
    client.Messaging.Workflow
    .Set(
        WorkflowTemplateID="[Your Workflow Template ID]"
    )
    .AddDestination("+64211234567")
    .SendMessage()
)

Full reference: docs/workflow.md

Using Addressbook Destinations

Every channel's AddDestination(...) also accepts a ContactID or GroupID from your Addressbook, instead of a raw phone number/email:

response = (
    client.Messaging.SMS
    .Set(
        Message="Hi [[FirstName]]"
    )
    .AddDestination(ContactID="[Contact ID]")
    .AddDestination(GroupID="[Group ID]")
    .SendMessage()
)

Destination Model reference

AddDestination(...), and the Destination/Destinations fields on every channel's SendMessage(...), all accept the same three shapes: a bare string (shorthand for the channel's primary field — ToNumber for most channels, EmailAddress for Email), a plain dict, or a tnzapi.core.destination.Destination instance. All three are equivalent — pick whichever reads best at your call site:

from tnzapi.core.destination import Destination

sms = client.Messaging.SMS
sms.Set(Message="Hi [[FirstName]]!")
sms.AddDestination("+64211231234")                                 # bare string
sms.AddDestination({"ToNumber": "+64211231234", "FirstName": "Alice"})  # dict
sms.AddDestination(
    Destination(ToNumber="+64211231234", FirstName="Alice")        # typed
)
response = sms.SendMessage()

Destination is a plain @dataclass with every field defaulting to None, shared identically across every channel:

Field Description
Recipient Generic destination address, sent as-is regardless of channel.
ToNumber / MobilePhone Phone number fields — interchangeable for SMS/WhatsApp/RCS/TTS/Voice.
MainPhone Alternative phone field, used by TTS/Voice.
EmailAddress Used by Email.
FaxNumber Used by Fax.
ContactID / GroupID / GroupCode Addressbook references — see Using Addressbook Destinations above.
Attention / FirstName / LastName / Company Personalisation tokens, e.g. [[FirstName]].
Custom1Custom9 Arbitrary per-recipient personalisation values, [[Custom1]][[Custom9]].

A raw dict with an unrecognised key (e.g. a typo) is rejected the same way a Destination(...) constructor call is — Destination(ToNumber="...", Bogus="...") raises TypeError at construction time; an unknown dict key raises ValueError from AddDestination(...)/Set(...), or is returned as Result="Failed" from SendMessage(...). Not every field is read by every channel — see each channel's own doc page (e.g. docs/sms.md) for which Destination fields it actually uses.

AddDestinations([...]) adds several destinations in one call, on every messaging channel - each item can be any of the three shapes shown above (bare string, dict, or typed Destination), mixed freely in the same list:

sms.AddDestinations([
    "+64211231234",
    {"ToNumber": "+64211231235", "FirstName": "Alice"},
    Destination(ToNumber="+64211231236", FirstName="Bob"),
])

AddDestination(...) (singular) only ever adds one destination per call - it raises TypeError if given a list directly, naming AddDestinations(...) as the replacement.

File Attachment reference

AddAttachment(...), and the Files field on Email/Fax/RCS/SMS/WhatsApp's SendMessage(...), all accept a bare file path string, a {"Name": ..., "Data": ...} dict, or a tnzapi.core.file_attachment.FileAttachment instance - pick whichever reads best at your call site:

from tnzapi.core.file_attachment import FileAttachment

fax = client.Messaging.Fax
fax.Set()
fax.AddAttachment("path/to/doc.pdf")                                # bare file path
fax.AddAttachment({"Name": "doc.pdf", "Data": "<base64>"})          # dict
fax.AddAttachment(
    FileAttachment("path/to/doc.pdf")                            # typed
)
response = fax.AddDestination("+64211231234").SendMessage()

A file path is read and base64-encoded automatically. Data is never filesystem-checked, under any circumstances - AddAttachment(Name, Data)'s second argument and a dict's Data key both always store the value exactly as given, so passing base64 content from an external source (e.g. an HTTP request body) is always safe there, even if that content happens to coincidentally look like a real local path. Only the explicit FileName=/positional-path form of FileAttachment (and the bare-file-path-string shorthand shown above) ever reads from disk. Voice's audio fields (MessageToPeople, etc.) accept the same shapes too - see docs/voice.md. TTS's same-named fields are plain spoken text and are unaffected

AddAttachments([...]) adds several attachments in one call - each item can be any shape AddAttachment(...) accepts (a path string, a {"Name": ..., "Data": ...} dict, or a FileAttachment instance), e.g. AddAttachments(["path/to/doc.pdf", FileAttachment("path/to/other.pdf")]).

Send Mode reference

Every channel's Mode field (all except Workflow, which has none) accepts the typed tnzapi.core.send_mode.SendMode enum as an equivalent alternative to the plain string - Mode=SendMode.Test/Mode=SendMode.Live and Mode="Test"/Mode="Live" are fully interchangeable, since SendMode is a str-subclass Enum (SendMode.Test == "Test" is True) and serializes identically over the wire:

from tnzapi.core.send_mode import SendMode

response = (
    client.Messaging.SMS
    .Set(
        Message="Test SMS Message click [[Reply]] to opt out",
        Mode=SendMode.Test
    )
    .AddDestination("+64211231234")
    .SendMessage()
)

Leaving Mode unset is unaffected either way - it's dropped from the outgoing request body, and the server applies its own "Live" default, exactly as before.

Webhooks

Full reference: docs/webhooks.md

If you configure a WebhookCallbackURL on a send, TNZ will POST a status update (and, for SMS, inbound replies) back to that URL. tnzapi.webhooks gives you typed shapes to parse that payload in your own receiver — the SDK never sends these itself, it only documents them:

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():
    payload = ResultWebhookPayload(**request.get_json())
    print(f"{payload.MessageID} is now {payload.Status}")
    return "", 204

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

Reports

client.Reports is a thin convenience wrapper, not a distinct implementation: every method here delegates straight to the same channel-specific Status(...)/Received(...) methods reachable directly via client.Messaging.<Channel> (e.g. Reports.SMSReply.Poll(...) internally calls Messaging.SMS.Status(...), and Reports.SMSReceived.Poll(...) calls Messaging.SMS.Received(...)) — so the response shape and field names are exactly whatever that channel's own doc page describes (e.g. docs/sms.md's Response section for SMS), not a Reports-specific shape.

Get Message Status

Status.Poll(...) routes to any of the 7 supported channels (sms, email, fax, tts, voice, whatsapp, rcs — not workflow) by its Channel string, case-insensitive. An unrecognised channel returns Result="Failed" with ErrorMessage=["Unknown or unsupported channel for Status: <Channel>"] client-side, without making an HTTP call.

response = client.Reports.Status.Poll(
    Channel="sms",
    MessageID="ID123456"
)

print(response)

Get SMS Reply

SMS-specific: internally the same call as client.Messaging.SMS.Status(MessageID). Inspect each entry in the response's Recipients list for its SMSReplies field to see inbound replies attached to that recipient — there's no separate reply-only response shape.

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

print(response)

Get SMS Received List

SMS-specific: internally the same call as client.Messaging.SMS.Received(...), for inbound SMS not tied to a specific outbound MessageID (e.g. an unprompted message to your inbound number). Only ever returns the requested page — see samples/messaging/_pagination.py's WalkAllPages helper if you need to walk everything.

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

print(response)

Actions

Each action takes the messaging Channel plus the MessageID to act on, and routes to that channel's own Abort/Reschedule/Resubmit/Pacing method (same one reachable directly via client.Messaging.<Channel>) — not every action is supported on every channel:

Action Supported channels Unsupported-channel error
Abort sms, email, fax, tts, voice, whatsapp, rcs (not workflow) "Unknown or unsupported channel for Abort: <Channel>"
Reschedule sms, email, fax, tts, voice, whatsapp, rcs (not workflow) "Unknown or unsupported channel for Reschedule: <Channel>"
Resubmit email, fax, tts, voice only "Resubmit is not supported for channel: <Channel>"
Pacing tts, voice only "Pacing is not supported for channel: <Channel>"

The error message wording itself differs by action ("Unknown or unsupported channel for X: ..." vs. "X is not supported for channel: ...") — this isn't a typo to normalise, both forms are the SDK's actual behavior; match on Result != "Success" rather than parsing ErrorMessage text if you need to detect this case programmatically. Every action's response type (Result, ErrorMessage, plus action-specific fields like ActionResult/Status on success) is documented on each channel's own doc page, e.g. docs/sms.md's Response section.

Abort a Job

response = client.Actions.Abort.SendRequest(
    Channel="sms",
    MessageID="ID123456"
)

print(response)

Reschedule a Job

response = client.Actions.Reschedule.SendRequest(
    Channel="sms",
    MessageID="ID123456",
    SendTime="2023-07-10T09:00"
)

print(response)

Resubmit a Failed Job

response = client.Actions.Resubmit.SendRequest(
    Channel="fax",
    MessageID="ID123456",
    SendTime="2023-07-10T09:00"
)

print(response)

Adjust Pacing

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

print(response)

Opt-Out Management

Full reference: docs/optout.md

Manage the do-not-contact list that TNZ checks before sending. DestType accepts Fax, Text, SMS, Email, Speech or Voice (comma-separated for more than one) — SMS and Voice are accepted as aliases of Text and Speech respectively.

# Create a single opt-out
response = client.OptOut.Create(
    DestType="SMS",
    Destination="+64211234567",
    Notes="Requested via support call"
)

# Create a batch of opt-outs in one call
response = client.OptOut.CreateBatch(
    DestType="SMS,Email",
    Destinations=["+64211234567", "+64221234567"]
)

# List opt-outs, optionally filtered by time period / dest type / contact
response = client.OptOut.List(
    DestType="SMS",
    RecordsPerPage=20,
    Page=1
)

# Get details of one opt-out
response = client.OptOut.Details("[Opt-Out ID]")

# Update an opt-out (partial - only supplied fields are changed)
response = client.OptOut.Update("[Opt-Out ID]", Notes="Updated notes")

# Delete an opt-out
response = client.OptOut.Delete("[Opt-Out ID]")

# Update/Delete using a Response you already have (e.g. from Details()) -
# only the fields Update() actually accepts are sent (read-only fields like
# ID/timestamps are dropped automatically), and the ID itself can come
# straight from the Response too
details_result = client.OptOut.Details("[Opt-Out ID]")
details_result.Notes = "Updated notes"
response = client.OptOut.Update(details_result, model=details_result)
response = client.OptOut.Delete(details_result)

print(response)

Addressbook

Full reference: docs/addressbook.md

Contacts

# List
response = client.Addressbook.Contact.List(RecordsPerPage=10, Page=1)

# Search
response = client.Addressbook.Contact.Search(Attention="Joe", RecordsPerPage=10, Page=1)

# Detail
response = client.Addressbook.Contact.Detail("[Contact ID]")

# Create
response = client.Addressbook.Contact.Create(
    Title="Mr",
    Company="TNZ Group",
    FirstName="First",
    LastName="Last",
    MobilePhone="+642122223333"
)

# Update
response = client.Addressbook.Contact.Update("[Contact ID]", Attention="Test Attention")

# Delete
response = client.Addressbook.Contact.Delete("[Contact ID]")

# Update/Delete using a Response you already have (e.g. from Detail()) -
# only the fields Update() actually accepts are sent (read-only fields like
# ContactID/Owner/timestamps are dropped automatically), and the ID itself
# can come straight from the Response too
detail_result = client.Addressbook.Contact.Detail("[Contact ID]")
detail_result.Attention = "Test Attention"
response = client.Addressbook.Contact.Update(detail_result, model=detail_result)
response = client.Addressbook.Contact.Delete(detail_result)

print(response)

Groups

# List
response = client.Addressbook.Group.List(RecordsPerPage=10, Page=1)

# Detail
response = client.Addressbook.Group.Detail("[Group ID]")

# Create
response = client.Addressbook.Group.Create(GroupName="[Group Name]")

# Update
response = client.Addressbook.Group.Update("[Group ID]", GroupName="[New Group Name]")

# Delete
response = client.Addressbook.Group.Delete("[Group ID]")

# Update/Delete using a Response you already have (e.g. from Detail()) -
# only the fields Update() actually accepts are sent (read-only fields like
# GroupID/Owner/timestamps are dropped automatically), and the ID itself
# can come straight from the Response too
detail_result = client.Addressbook.Group.Detail("[Group ID]")
detail_result.GroupName = "[New Group Name]"
response = client.Addressbook.Group.Update(detail_result, model=detail_result)
response = client.Addressbook.Group.Delete(detail_result)

print(response)

Contact Groups

Manage which groups a contact belongs to:

# List the groups a contact belongs to
response = client.Addressbook.ContactGroup.List("[Contact ID]", RecordsPerPage=10, Page=1)

# Detail of one contact/group membership
response = client.Addressbook.ContactGroup.Detail("[Contact ID]", "[Group ID]")

# Add a contact to a group
response = client.Addressbook.ContactGroup.Create("[Contact ID]", "[Group ID]")

# Remove a contact from a group
response = client.Addressbook.ContactGroup.Delete("[Contact ID]", "[Group ID]")

print(response)

Group Contacts

The same relationship viewed from the group's side:

# List the contacts belonging to a group
response = client.Addressbook.GroupContact.List("[Group ID]", RecordsPerPage=10, Page=1)

# Detail of one group/contact membership
response = client.Addressbook.GroupContact.Detail("[Group ID]", "[Contact ID]")

# Add a contact to a group
response = client.Addressbook.GroupContact.Create("[Group ID]", "[Contact ID]")

# Remove a contact from a group
response = client.Addressbook.GroupContact.Delete("[Group ID]", "[Contact ID]")

print(response)

List() results only cover the requested page — this SDK never auto-walks every page on your behalf. Inspect PageCount/TotalRecords on the response and request further pages yourself if you need them.

Legacy API Support

Code written against the pre-2.0.3 method-call API (client.Send.SMS(...), client.Get.Status(...), client.Set.Pacing(...)) is still fully supported — it isn't deprecated and isn't being removed. .Send, .Get and .Set continue to wrap the older v2.04 REST API under the hood (rather than v3.00), and can be authenticated with either AuthToken or the legacy Sender/APIKey pair:

from tnzapi import TNZAPI

client = TNZAPI(
    Sender="[Your Sender]",
    APIKey="[Your API Key]"
)

response = client.Send.SMS(
    Recipients=["+64211231234"],
    MessageText="Hi there!"
).SendMessage()

print(response)

If you're maintaining an existing integration built on this style, there's no need to migrate it to .Messaging/.Reports/.Actions — both styles are supported side by side on the same TNZAPI client and will keep working.

Response Structure

Every response object (from .Messaging, .Reports, .Actions, .Addressbook and .OptOut) exposes at least:

  • .Result"Success" on success, otherwise a failure code such as "Failed", "Unauthorized" or "RecordNotFound".
  • .ErrorMessage — a list of human-readable error strings, populated whenever .Result != "Success".

Successful responses additionally expose the fields relevant to that call (e.g. .MessageID on a send, .ContactID on a contact create/detail, .OptOuts on an opt-out list).

If you want to type-hint against a response's class (e.g. in a function signature), import it from tnzapi.models rather than the internal tnzapi.api.v300....models.responses... module path:

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, etc.) — these were named with a ...DTO suffix in earlier versions, which still works via a DeprecationWarning-emitting alias.

Recipients/Messages are plain dicts, except SMS

Every channel's Status(...)/Received(...) response carries a list of per-recipient/per-message entries (Recipients, Messages, or similar). On every channel except SMS, these are plain dict objects — access fields with entry.get("Field") or entry["Field"], not attribute access. SMS is the one exception: its Recipients list holds typed SMSRecipient objects (with a nested SMSReply type for inbound replies), so entry.Field works there. This asymmetry is deliberate, not an oversight — SMS was the first channel to get typed response objects; the rest still use plain dicts. Each channel's own doc page under docs/ states which shape applies.

Common response enums

  • JobStatus (on every channel's Status(...) response): "Pending", "Delayed", "Completed", "CreditHold", "Unknown".
  • Per-recipient Status (inside Recipients/Messages entries): "Success", "Failed", "Pending".
  • Per-recipient Type (inside Recipients/Messages entries): the channel that actually delivered this recipient, not necessarily the channel you called — relevant when FallbackMode triggers. TTS and Voice both report Type="Voice" — there's no separate "TTS" value on the wire, since the underlying delivery mechanism is identical for both from the TNZ API's point of view.

ID-validation guards

Every method that takes a required, standalone ID parameter (e.g. MessageID on Status(...), ContactID/GroupID on Detail(...)/Update(...)/Delete(...), OptOutID on OptOut.Details(...)) raises ValueError immediately if you pass None or "" — rather than silently building a broken request URL like .../contact/None or comparing an unset ID in memory. Update(...)/Delete(...) methods that also accept a Response object in place of a plain ID string raise the same ValueError if that Response's ID field itself isn't set (e.g. it came from a failed prior call). This is a client-side guard — no HTTP request is made when it fires.

Error Handling

result = client.Messaging.SMS.SendMessage(
    Message="Hi there!",
    Destination="+64211231234"
)

if result.Result != "Success":
    print(result.ErrorMessage)
else:
    print(result.MessageID)

Development & Testing

# Install in editable mode with test dependencies
pip install -e .[test]

# Run the test suite
pytest

Tests load configuration via python-dotenv, layered in this order (later files override earlier ones): .env, then .env.local, then .env.test. Unit tests are hermetic by default — a fixture clears TNZ_API_URL/TNZ_AUTH_TOKEN/TNZ_ALLOW_INSECURE_HTTP from the environment before each test, so a developer's local .env/.env.local (e.g. pointing at a local test server) never leaks into a test unless that test explicitly opts in.

Integration tests

tests/integration/ hits a real TNZ API instance instead of mocking the HTTP layer — it's excluded from a plain pytest run (pytest.ini's addopts = -m "not integration") and only runs when asked for explicitly:

pytest -m integration

Copy .env.test.example to .env.test and fill in real values first — each test skips gracefully if the environment variables it needs aren't set. Messaging-send tests pass Mode="Test" on every channel that supports it (all except Workflow) on the assumption it's a no-cost/no-delivery switch; this hasn't been confirmed against production, so treat a real send/charge as a live possibility until verified.

Beyond having TNZ_AUTH_TOKEN configured, running any live test also requires TNZ_RUN_LIVE_TESTS=1 — a deliberate second confirmation, since a real token being present isn't itself permission to fire real (possibly billable) requests. The Workflow send test needs a further TNZ_ALLOW_WORKFLOW_SEND=1, since Workflow has no Mode="Test" equivalent and the configured TNZ_TEST_WORKFLOW_TEMPLATE_ID genuinely executes on every run. WhatsApp/RCS send tests skip (rather than fail) on a Failed result, since that can mean the account isn't provisioned for that channel rather than an SDK regression.

Getting Help

If you need help installing or using the library, please check the TNZ Contact if you don't find an answer to your question.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tnzapi-3.0.0.0.tar.gz (125.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tnzapi-3.0.0.0-py3-none-any.whl (247.1 kB view details)

Uploaded Python 3

File details

Details for the file tnzapi-3.0.0.0.tar.gz.

File metadata

  • Download URL: tnzapi-3.0.0.0.tar.gz
  • Upload date:
  • Size: 125.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for tnzapi-3.0.0.0.tar.gz
Algorithm Hash digest
SHA256 120d694c67e8f95c01da09c6b5c9df58551a59eb1bb5b04883fc7b0b3c794af2
MD5 92dad9f570f4cc1d44249ab9184683eb
BLAKE2b-256 a23c18fe3d82b5f32fa3da9438e331dd684df46b0d7280589736f041d8f990d9

See more details on using hashes here.

File details

Details for the file tnzapi-3.0.0.0-py3-none-any.whl.

File metadata

  • Download URL: tnzapi-3.0.0.0-py3-none-any.whl
  • Upload date:
  • Size: 247.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for tnzapi-3.0.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 03bc30592a4579c6268a86c4dc8325db56547d5ef54e692256460f06a7471d8a
MD5 7c3d14143a47fb83375eebd9751ae79f
BLAKE2b-256 27216e1065470a9b1311ae14b4ec3cce1811e6353b9f435ed0b1857dd7c2af9a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page