- Adds the lookup channel: bird.lookup.email() and bird.lookup.phone_number(), on both Bird and AsyncBird.
- The contacts documentation now names
phone_number, the identifier the API accepts, where it previously namedphone— including the batchmatch_onvalue, which is rejected asphone. - LookupPropertyStatus is now an open enum, so a future property status will not break a deployed client.
- Breaking: a contact's phone identifier is now named
phone_number, matching the rest of the API. It replacesphonein create, update, batch-upsert and read bodies; theGET /v1/contactsfilter becomes?phone_number=; theidentifierfilter value and the batchmatch_on/matched_onvalues becomephone_number— they name the field, so they move with it. On the TCR brand surface, a brand's ownphonebecomesphone_numberas well. The CLI'sbird contacts create|updatetake--phone-number, andbird contacts listfilters on--phone-number. Qualified compounds are unchanged:mobile_phone,primary_phoneandbusiness_contact_phonekeep their names. - Add
options.smart_encodingto an SMS send (--smart-encodingon the CLI): it replaces characters outside the GSM-7 alphabet (curly quotes, dashes, ellipses, fullwidth forms, non-breaking spaces) with their closest equivalent, which lowers the segment count and the cost on a body that would otherwise send asUCS2. Off unless you ask for it, and all-or-nothing: a body still holding an emoji or a non-Latin script afterwards is sent exactly as you supplied it. The message reports the settings that were applied, and itstextreflects the body as sent. - Breaking: a verification's email recipient is now named
email, replacingemail_address— rename it at the call site on a verification'sto, and in any handler reading thetoof averify.*webhook payload. Phone recipients are unchanged:phone_numberkeeps its name. The old spelling is not accepted: a request sendingemail_addressis rejected as an unknown property.
- Breaking:
carrierandmcc_mncare omitted fromsms.sent,sms.deliveredandsms.receivedwebhook payloads when the carrier reports none, instead of arriving asnull;subjectonsms.receivedbehaves the same way. They are now optional rather than required, so a typed field changes to a pointer or an optional — check for absence where you checked fornull. This matches how the message resource has always reported the same fields. sms.acceptednow carriessegments, the segment breakdown the send is billed on, so a webhook-only integration can explain thecoston the same event instead of fetching the message to reconcile a charge.- Subscribe to
sms.receivedto be pushed inbound SMS instead of polling for it. The payload carries the message body, its segment breakdown, both numbers, and the sending operator where the carrier reports one; aSTOParrives as an ordinary received message and is yours to act on.
- A verification attempt can now report
delivery_timeoutas its failure reason, meaning no delivery confirmation arrived before the channel's timeout and the verification failed over to the next channel. - Path ids are percent-encoded, so an id containing a reserved character reaches the intended endpoint instead of silently becoming URL structure.
- An SMS error
codeis now an open enum: a reason added by a newer server parses through as a plain string instead of raising a validation error, so a delivery receipt carrying one no longer fails to read. The values known at this version are exported asSMSErrorCode. - The
sms.expiredwebhook payload now carrieserror, the same failure detail the other terminal SMS events carry: a Bird-stablecode, adescription, and the provider'scarrier_error_codewhen it sent one. - Breaking: An email template reads the recipient's contact record through
bird.contact.<attribute>and the unsubscribe link throughbird.unsubscribe_url. Rewrite a template that uses any other spelling for those values and republish it. - Breaking: A send by template must supply a value for every parameter its template uses, and a parameter name must be a single word. A send that omits one is rejected rather than delivered with a blank in place of the value.
- Breaking: A
marketingtemplate has to placebird.unsubscribe_urlin its body to publish, in every language it carries. The token stands on its own: no filters, not inside an{% if %}or{% for %}block, and not in the subject. Where a language has both an HTML and a text body, the HTML one has to carry it. birdis now the only name you cannot use for a parameter, socontact,unsubscribe_urlandfirst_nameare all available.- Listing calls now accepts in-flight and final statuses together in one
statusfilter and returns them as a single page, where mixing them used to be rejected. - Breaking: a WhatsApp template parameter's
textis now optional and should be read as nullable. It carries a value only on atextparameter, and a parameter of any other kind carries its value in the field named for that kind. - Template parameters can now describe more than text.
typeacceptsimage,video,gif,documentandlocationalongsidetext:image,video,gifanddocumentcarry a media header's file inurl, andlocationcarries a location header's point inlocation. A parameter's kind names only the field its value travels in — a coupon button's code is a plain string, so it is still atextparameter ({"type":"text","text":"LUCAS25"}), the same shape the code was authored under. Acarouselcomponent carries its values per card, incards[], one entry per card in the order the template was approved with. - Three WhatsApp field descriptions now match what the API does. Omitting a template send's
languagesends the template's default language rather than returning a422;receivedis documented as an inbound message's status rather than as reserved; and a WhatsApp message'scostexplains that an inbound message is never priced.
- Listing contacts gains an
identifierfilter (emailorphone), and each contact now includes itsaudiences. - Breaking: an SMS message's
textis now optional — a message you sent always carries one, but a received message may not. Handle its absence rather than assuming every message has a body. - Every
sms.*webhook payload now carriescost, split intotransaction_amountandpassthrough_amountover one currency. The figure is as of that event, and the components are named so a subscriber merges them per component rather than replacing the object, since webhook delivery is not ordered. - Verify gains a next-channel action: when a recipient reports the passcode never arrived, send a fresh one on the next channel in the verification's plan without waiting out the resend cooldown. Identify the verification by the same recipient you started it with, as with a check — there is still no id to store. Every passcode already sent stays valid, so a late arrival can still be checked. Available as
verify.verifications.nextChannel/NextChannel/next_channel/nextChannelon the TypeScript, Go, Python, and PHP SDKs,bird verify verifications next-channelon the CLI, and theverify_verifications_next_channelMCP tool. A verification whose channel plan is exhausted answers422 NoNextChannel. - Add a
voiceresource for reading the call log: list the workspace's calls with the dashboard's filters, and fetch one call at any point in its lifecycle. - A voice call now reports
actor, the API key or user that placed it. It is absent on calls that ended before Bird began recording it, and on any call your trunk admitted by source IP address, since that path carries no credential to identify a caller. - A WhatsApp message's
costnow names its components, matching SMS:transaction_amountis what Bird charged to send the message, andpassthrough_amountis reserved for third-party fees.amountremains the total. - The create-verification docs no longer name SMS as the phone channel: a phone recipient is verified over the phone channels enabled for its destination country, in that country's configured order.
- An SMS message's
costnow names its components:transaction_amountis what Bird charged to carry the message, andpassthrough_amountis reserved for third-party fees such as US 10DLC carrier surcharges.amountremains the total.
- Listing WhatsApp messages gains a
categoryfilter, matching the equivalent filter on SMS and email messages.
- The Realtime app credentials can now be overridden per call, keyed by security scheme:
options={"credentials": {"RealtimeKey": …, "RealtimeSecret": …}}. One client can address several Realtime apps; setting them on the client stays the default. - Batch contact upserts now match each entry automatically on every identifier it carries (email, phone, or external_id), refusing entries whose identifiers belong to more than one contact;
match_on(email,phone, orexternal_id) forces a single key. Result items echo what each entry supplied under a nestedentryobject (email,phone,external_id, null where absent, never the contact's current state), plus a top-levelmatched_onnaming which identifier matched, null for created rows. - Failed rows in a batch contact upsert carry the specific error
code(for exampleE04058, ambiguous match, versusE04055, phone taken) alongsidetypeandmessage, so a sync can branch on which conflict it hit. - Breaking (0.x):
Contact.channelsis removed: the field restated which identifiers are set under a reachability claim the platform cannot back. Reademail/phonepresence directly. - Breaking (0.x):
Contact.emailis now optional: a contact may be identified by an E.164 phone number instead of, or as well as, an email address. Contacts gainphoneand the contact list gains an exactphonefilter. - Filter WhatsApp messages by
direction. The unfiltered list returns the whole conversation, sodirectionnarrows it to what you sent or what the contact sent you.
- Voice call webhook payloads name the two parties from and to, replacing src_number and dst_number. A handler reading those fields on voice_call.initiated, voice_call.answered or voice_call.ended must rename them; every other field is unchanged.
- WhatsApp and SMS template
languagefields now take a BCP-47 tag (for examplept-BR); Meta's underscore form (pt_BR) is still accepted as an input alias but is no longer echoed back. WhatsApp template sends can now also address a template byid, as an alternative toslug: the existingtemplateargument takes either, resolving awat_-prefixed value as the id — the same convention it already uses for SMS'ssmt_. - WhatsApp send: the
tofield now accepts a business-scoped user ID as well as an E.164 phone number, so you can message a WhatsApp user whose phone number you do not have. One-time-passcode templates still require a phone number and return422 WhatsAppRecipientNotSupportedForTemplatewhen sent to a business-scoped user ID.
- Add a
datetimecontact property type: an RFC 3339 timestamp with an explicit offset. - Add realtime.members.send: deliver an event to every connection one member holds, addressing the person rather than a channel.
- Listing contacts gains an
include_totalflag for a total count, andqnow matches first and last name as well as email. - WhatsApp
rejectednow covers every refusal before transmit, not only a suppressed recipient: a charge decline (insufficient wallet balance, unpriced destination) and an undeliverable recipient reportstatus: rejectedwith awhatsapp.rejectedtimeline event instead offailed.whatsapp.rejectedis also now published on the message-events enum. - Breaking: a WhatsApp send against a template that declares named parameters must now name every parameter, matched as a set, so order no longer matters; an unnamed, misnamed, duplicated, or missing name returns
422 WhatsAppTemplateParameterMismatch.bird_otpstays positional: its values go in{{n}}order and must carry no name. Unnamed values were previously matched by position and could render the wrong content while reporting success, so sends made against these templates before this release are worth re-checking. - Clarify the email broadcast and template descriptions, including the errors each operation can return.
- Correct the
parametersfield description for an inline send. - Document that an authentication-category message returns a redacted body.
- Template variables now report a
sensitiveflag showing whether the value is redacted before storage. - The mailbox read and statistics methods now carry a description instead of an empty docstring.
- Open-enum fields now carry their known values: reading one offers the values the API can send, and a value added by a newer server still decodes.
- Add a per-send
languageoverride for templated email:EmailSendParams.Languageon the Go SDK, alanguagekeyword argument onclient.email.send/send_batchin the Python SDK, and--languageonbird email send. Omitting it keeps today's behavior, sending the template's default language. - Export the known values of every open enum (EmailEventType, VerificationChannel, WhatsAppErrorCode and the rest) as constants, so an open enum is no longer a bare string with its values buried in prose.
- Email message reads and send responses now report
requested_languageandresolved_language. - Email message reads and send responses now report
template_idandtemplate_version_id. A template's live version changes each time you submit it, so the version is what identifies the wording a message was actually delivered with, and the two together fetch that content back. - Passcode attempts can report
channel_disabled, meaning Bird has temporarily stopped sending over that channel and the verification moved on to the next one.
- Breaking: a WhatsApp send addresses its template by
slug(previouslyname), and a WhatsApp message read echoestemplate.slug(previouslytemplate.name). Templates carry the slug as their permanent handle; the display name is cosmetic and never affects sending.
- Email message reads now report the message as delivered. For a send that used a template,
subjectand the bodies from the message-content endpoint previously returned the template source, tokens and all, which is content no recipient received; they now return that source with the send's substitution values applied. The values themselves are exposed as a newparametersfield so the inputs stay visible beside the result. Sends that supplied their content inline are unaffected. - Nest mailboxes and threads under email, matching the URL and the CLI/MCP names. client.mailbox becomes client.email.mailboxes, client.mailbox_thread becomes client.email.threads, client.mailbox_thread_message becomes client.email.threads.messages, client.mailbox_receive_rule becomes client.email.mailboxes.receive_rules, and mailbox.compose becomes email.mailboxes.messages.create. Params TypedDicts are renamed to match.
- Document that a template's
language_source_requiredsetting makes the send block'slanguagemandatory. - Documented how category resolves on a send-by-template: omitting it takes the template's own classification, and setting it always overrides the template.
- Add an optional
languageto the email template send block, selecting which of the template's languages to send. The template'son_missing_languagedecides whether an unstocked language falls back or is rejected. domains.create/updatenow take wire-shaped nested params as typed TypedDicts (e.g.tracking={'name': ...},settings={'click_tracking': True}) instead of flattened kwargs, matching Go and TypeScript.- Add
contacts.batch; itscontactsargument is now typedSequence[ContactCreateParams]instead of an untyped mapping. mailbox_thread_message.replynow accepts the full message body, including structuredtags({name, value}) andattachments. In Go,ReplyAllis now*boolso an explicitfalsereaches the wire.mailbox_thread.update:labelsandcontact_idare now typed keyword arguments.mailbox.update: the fields are now typed keyword arguments, andconfirmis accepted and sent as a query parameter.- Add the
mailboxresource:list,create,get,stats,labels,restore,resume, anddelete. - Go: the create body's
ReceivePolicyandRetentionTierare now the typedMailboxCreateReceivePolicy/MailboxCreateRetentionTierenums instead of plain strings. - TypeScript: a write whose body has no required field now defaults its params to
{}, sobird.mailbox.create(),bird.audiences.update(id), andbird.domains.update(id)are callable without a body; the unusedMailboxListenvelope export is removed. - Add the
mailbox_thread_messagereads:list,get,body, andattachments. - Python: the
client.mailbox_thread.messagesnested accessor is replaced by the top-levelclient.mailbox_thread_message(matching Go and TypeScript). - TypeScript: the unused
EmailThreadMessageListexport is removed. - Add the
mailbox_threadreads (list,get) anddelete. - Go (breaking, 0.x):
MailboxThreadService.DeletetakesMailboxThreadDeleteParams{Permanent bool}instead of a positionalpermanent bool. - TypeScript:
mailboxThread.deleteaccepts an optionalMailboxThreadDeleteQuery; the unusedEmailThreadListexport is removed. - Python:
mailbox_thread.deleteaccepts apermanentkeyword. - Add the
mailbox_receive_ruleresource:list,create, anddelete. - Go: the create body's
Actionis now the typedReceiveRuleCreateActionenum instead of a plain string. - Python: the
client.mailbox.receive_rulesnested accessor is replaced by the top-levelclient.mailbox_receive_rule(matching Go and TypeScript). - TypeScript: the unused
ReceiveRuleListexport is removed. - Add the SMS reads
getandlist;listnow accepts atagfilter. - Add the
sms_templatesandwhatsapp_templatesresources; export theSmsTemplateListParamsquery TypedDict. - Add the WhatsApp reads
get,list, andlist_events;listnow accepts atagfilter. - Breaking (0.x):
verify.verificationscreateandchecknow taketo(andoptions_) in the nested shape instead of flatemail/phone/code_length/channelskeyword arguments. - WhatsApp messages now return
cost, the amount charged for the message. - The email template send block addresses a stored template by
slug(previouslyname). Templates carry the slug as their permanent handle plus a separate free-text displayname. - The realtime.* webhook event type constants are no longer exported. Realtime webhooks are created and managed in the Bird dashboard.
- Breaking: remove the WhatsApp templates-list surface —
bird whatsapp templates list, thewhatsapp_templates_listMCP tool, andwhatsappTemplates.list/WhatsappTemplates.List/whatsapp_templates.listin the TypeScript, Go, and Python SDKs. WhatsApp is still in preview and the templates contract is being reshaped for localisation; templates return to the public and command audiences at GA in the new shape. - Clarified the VoiceCallStatus documentation: ringing and in_progress describe a call that is still up, rather than values held back for a future feature.
- SMS alphanumeric sender IDs now allow dashes and underscores alongside letters, digits, and spaces, and must contain at least one letter with no separator at either end. A digits-only value is a long code or short code, and is no longer accepted as a sender ID.
contact-propertiescreateandupdate: the fallback value is typed as any JSON value, and the property type is the namedContactPropertyType.- Add the email read methods:
get,list, andcancel. EmailThreadMessageReplyRequestgains an optionalattachmentsfield, matching the compose and direct-send surfaces.- The SMS-template list filters carry their named enum types from the spec (TemplateScope, SMSMessageCategory) instead of inline unions.
- The stats trend-grain, message direction, and email status read filters are now typed rather than plain strings, so each carries the values it accepts.
- Add Realtime data-plane methods: publish, batch publish, channel list/get/members, and member disconnect.
contacts.updatenow clears a nullable field when passedNone(distinct from omitting it, which leaves it unchanged).- Add the audiences resource.
AudienceContactsAddParams/AudienceContactsRemoveParamsare nowAudienceAddContactsParams/AudienceRemoveContactsParams(match the method names). - Add the domains resource: create, read, update, delete, and verify. List supports
sort/order/include_total. - Export the request-unset sentinel
Omit/omitfrom the top-levelbirdpackage: a request-body parameter defaults toomit(left out of the request), distinct from an explicitNone(sends JSONnull, clearing a nullable field). Standardizes the SDK on a singleOmitsentinel (previously an internalNotGiven). - Add
verify.*webhook event types and payloads. - Internal improvements.
- The verification terminal reason is now the named type
VerificationTerminalReason, carrying its known values, rather than a bare string. - Internal improvements.
- Stats
period.grainis now typed as a sharedStatsGrain(day|hour) instead of a plain string. No wire or behavioural change.
- Resource and package docstrings now describe behavior only.
- Agent mailboxes (inbox.ai): client.mailbox, client.mailbox_receive_rule, client.mailbox_thread, client.mailbox_thread_message
- Internal improvements.
- Add the
rejectedWhatsApp message delivery status, returned when the recipient is on the workspace's suppression list. - Add the
whatsapp.rejectedwebhook event, delivered when Bird rejects a WhatsApp message before sending because the recipient is on the workspace's suppression list. - Rename voice call webhook event types from voice.call.* to voice_call.* (single-dot resource convention; events were never emitted, so no delivered payload changes)
- Add sms.tfn_verification webhook event types
- Add email statistics reads under
email.stats: the period summary, the daily and hourly time series, and the dimension breakdowns (by tag, category, sending IP, sending domain, recipient domain, mailbox provider, mailbox-provider region, template, location, client, bounce code, complaint type, and broadcast). - Breaking: the Realtime webhook event type
realtime.subscription_countis nowrealtime.connection_count, matching Bird's Realtime vocabulary (per channel it counts connections — one connection cannot subscribe twice). Realtime is in early access; the old event type had no GA consumers. - Operations and fields now document their units, defaults, omission behavior, and per-value status meanings. Several descriptions were corrected to match actual behavior, including engagement-rate denominators, suppression prefix matching, and stored-content retention.
- Internal improvements.
- Regenerated models: timestamp examples now render in RFC 3339 format
- WhatsApp templates: create and list/get a workspace's own message templates. Reads now include a template id and an optional description; create takes a name, category, components, a WhatsApp language code, and an optional description; sending gained a named parameter name for named-parameter templates. Additive; no breaking change.
- Suppressions:
reason,origin, andapplies_toare now documented as growing vocabularies (open enums on the wire) —origingainedunsubscribe_link, a suppression created by the recipient through Bird's hosted unsubscribe page or its one-click link. Treat unknown values as informational rather than rejecting the record. Additive; no breaking change.
- Add voice call-event webhook types:
voice.call.initiated,voice.call.answered, andvoice.call.endedare now recognized event types with typed payloads. Additive; no breaking change.
- Breaking: WhatsApp message reads now return
fromandto(each a phone number and/or business-scoped user ID) in place ofbusinessandcontact, matching the SMS/email convention.
- Breaking: the contact list free-text filter is now
q(wassearch), matching the API's renamed query parameter. Updateclient.contacts.list(search=...)toclient.contacts.list(q=...).
- Received messages and the
email.receivedevent now carryauthentication(pass/fail/unknown), a single summary of sender authentication; treatunknownas not verified. Thespf_pass/dkim_pass/dmarc_passfields remain. Additive; no breaking change.
- Add the WhatsApp webhook event types:
WHATSAPP_ACCEPTED,WHATSAPP_SENT,WHATSAPP_DELIVERED,WHATSAPP_READ, andWHATSAPP_FAILED. Additive; no breaking change.
- Docs: align the SDK example markers on the
audiences,contacts, andcontact_propertiesresources with their operation keys so the API reference renders their Python code samples. Marker comments only — no change to the published API or runtime.
- Add the sending domains collection (sync and async):
client.domains.create,.get,.list,.update,.delete, and.verify. Register a sending domain, publish the DNS records it returns, then verify until it is usable as a sender. Requires an API key with thedomainsscope.
- Add the Realtime webhook event types:
REALTIME_CACHE_CHANNELS,REALTIME_CHANNEL_EXISTENCE,REALTIME_CLIENT_EVENTS,REALTIME_PRESENCE, andREALTIME_SUBSCRIPTION_COUNT. Additive; no breaking change.
- Contacts now carry
channels(the channels a contact can be reached on) and audience members carry theaudiencesthey belong to. Listing an audience's contacts gains an optionalsearchfilter (email substring). Additive response fields and an optional parameter; no breaking change.
- Correct the
verify.verifications.checkdocumentation: an already-resolved verification is no longer checkable and returns a 404, not a result withsuccess=False. Documentation only; no API or behavior change.
- WhatsApp failure detail now carries
meta_error_code, the raw error code from the WhatsApp Cloud API, and a fullerdescriptionsourced from Meta's error details. Additive response fields; no breaking change.
- Correct the error-code names shown in preview-feature field descriptions (regenerated from the API spec). Documentation only; no API or behavior change.
- Add the Verify product (sync and async):
client.verify.verifications.createsends a one-time passcode to a recipient andclient.verify.verifications.checkvalidates the code they submit.
- Add the WhatsApp channel (sync and async):
client.whatsapp.send,.get,.list,.list_events. Add WhatsApp templates (read-only):client.whatsapp_templates.list.
- Remove the email templates collection (
client.email_templates.create,.get,.update,.delete,.publish,.list,.list_versions,.get_version), added in 0.3.0. Template management is no longer part of the public API. Sending a published template withclient.email.send(passtemplateas anemt_…ID or name handle) is unchanged.
- Add
client.email.cancel(sync and async): cancel a scheduled message before it sends. A message that already started sending, or was already canceled, raises a conflict error. - Attribute the calling tool on every request via the
Bird-Callerheader, detected from the environment (no configuration).
- Add the contacts collection (sync and async):
client.contacts.create,.get,.list,.update,.delete, and.batch(bulk upsert by email). Requires an API key with theemail_marketingscope. - Add the audiences collection (sync and async):
client.audiences.create,.get,.list,.update,.delete, plus membership.list_contacts,.add_contacts,.remove_contacts,.remove_contact. - Add contact properties:
client.contact_properties.create,.get,.list,.update,.archive,.unarchive.
- Add the SMS channel (sync and async):
client.sms.send,.send_batch,.get,.list. - Add SMS templates (read-only):
client.sms_templates.list,client.sms_templates.get. - Add email templates:
client.email_templates.create,.get,.update,.delete,.publish,.list, plus versions.list_versionsand.get_version. client.email.sendcan send a published template: passtemplate(anemt_…ID or name handle) withparametersin place of inlinesubject/html/text.
- Rename the anonymous client-identity headers from
X-Bird-*toBird-*(theX-prefix is deprecated, RFC 6648). Same telemetry, new header names; no other behavior or API-surface change.
- Send anonymous
X-Bird-*client-identity headers (surface, version, language, os, arch) on every request, so Bird can attribute API usage by surface. No personal data, credentials, or request content: just which Bird client and platform. Telemetry only; no behavior or API-surface change.
- Add batch email send:
client.email.send_batch(sync and async). - Point package metadata at the docs (https://bird.com/docs/sdks/python).
- Initial release: sync and async clients, email send, webhook verification, pagination, typed errors.