From 77c03d988890df623ec37dad0c64f96d97ed32b2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 17:59:07 +0000 Subject: [PATCH 01/15] feat(api): api update --- .stats.yml | 4 ++-- .../types/number_10dlc/ten_dlc_brand.py | 24 +++++++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index ddb1fe8..10360c4 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 183 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-5d75bbdcac3a0b0a8b798c0b4ef0dacfbc64ad4a20605489dc46dda7c8d1d8d8.yml -openapi_spec_hash: bb242cfd8cc43354412164c2dceae89c +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-997e957e7b8c6a15c356fd9182f73a1a149e78b702566daf23b1e7519c06ee95.yml +openapi_spec_hash: a0ee7a79588077d95d4bf9fccc3837a2 config_hash: 261e1b852ca7f8364e4a283a3edd350a diff --git a/src/zavudev/types/number_10dlc/ten_dlc_brand.py b/src/zavudev/types/number_10dlc/ten_dlc_brand.py index a710477..f195ad8 100644 --- a/src/zavudev/types/number_10dlc/ten_dlc_brand.py +++ b/src/zavudev/types/number_10dlc/ten_dlc_brand.py @@ -38,8 +38,20 @@ class TenDlcBrand(BaseModel): state: str - status: Literal["draft", "pending", "verified", "rejected"] - """Status of a 10DLC brand registration.""" + status: Literal["draft", "pending", "verified", "unverified", "rejected", "failed"] + """Status of a 10DLC brand registration. + + - `draft`: created, not yet submitted to the carrier. + - `pending`: submitted, awaiting the carrier's answer. + - `verified`: the carrier registered the brand AND verified the business behind + it. + - `unverified`: the carrier registered the brand but did not verify the business + — the registration exists, the identity check did not pass or has not been + resolved. Campaigns are allowed, with lower daily limits. Read + `identityStatus` for the carrier's own wording. + - `rejected`: refused by the carrier. + - `failed`: the registration never reached the carrier; the fee is refunded. + """ street: str @@ -64,6 +76,14 @@ class TenDlcBrand(BaseModel): first_name: Optional[str] = FieldInfo(alias="firstName", default=None) + identity_status: Optional[str] = FieldInfo(alias="identityStatus", default=None) + """ + The carrier's raw identity verdict on the business, as the carrier spells it + (`VERIFIED`, `VETTED_VERIFIED`, `SELF_DECLARED`, `UNVERIFIED`). Null while the + identity has not been resolved — which is not the same as verified, and is why + such a brand reports `status: unverified`. + """ + last_name: Optional[str] = FieldInfo(alias="lastName", default=None) stock_exchange: Optional[str] = FieldInfo(alias="stockExchange", default=None) From 9a3148cf7bbf2c9ac183e9c2d90ec8f3c6f4da24 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 08:21:32 +0000 Subject: [PATCH 02/15] feat(api): api update --- .stats.yml | 4 +- src/zavudev/resources/addresses.py | 58 ++++-- src/zavudev/resources/phone_numbers.py | 192 +++++++++++++++--- src/zavudev/resources/senders/senders.py | 12 +- src/zavudev/types/address_create_params.py | 14 +- src/zavudev/types/owned_phone_number.py | 25 +++ src/zavudev/types/phone_number_pricing.py | 9 +- .../types/phone_number_purchase_params.py | 43 +++- .../types/phone_number_requirements_params.py | 19 +- src/zavudev/types/phone_number_status.py | 2 +- .../types/phone_number_update_params.py | 6 +- src/zavudev/types/requirement.py | 4 +- src/zavudev/types/requirement_type.py | 1 + src/zavudev/types/sender_create_params.py | 6 +- tests/api_resources/test_addresses.py | 20 +- tests/api_resources/test_phone_numbers.py | 40 ++-- 16 files changed, 364 insertions(+), 91 deletions(-) diff --git a/.stats.yml b/.stats.yml index 10360c4..bbcd963 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 183 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-997e957e7b8c6a15c356fd9182f73a1a149e78b702566daf23b1e7519c06ee95.yml -openapi_spec_hash: a0ee7a79588077d95d4bf9fccc3837a2 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-6a4c9cdb8d971155ac22e679a37d27525a97b8a65991908a1c12969252457811.yml +openapi_spec_hash: 5d999f34f096d592deaa6319f52190b3 config_hash: 261e1b852ca7f8364e4a283a3edd350a diff --git a/src/zavudev/resources/addresses.py b/src/zavudev/resources/addresses.py index 0fade96..0b7fbe5 100644 --- a/src/zavudev/resources/addresses.py +++ b/src/zavudev/resources/addresses.py @@ -48,14 +48,14 @@ def create( self, *, country_code: str, + first_name: str, + last_name: str, locality: str, postal_code: str, street_address: str, administrative_area: str | Omit = omit, business_name: str | Omit = omit, extended_address: str | Omit = omit, - first_name: str | Omit = omit, - last_name: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -63,12 +63,19 @@ def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AddressCreateResponse: - """Create a regulatory address for phone number purchases. - - Some countries require a - verified address before phone numbers can be activated. + """ + Create a regulatory address, to use as the value of an `address` requirement + when buying a phone number. It is registered for review when it is created, with + status `pending`. Args: + first_name: First name of the person the address is registered to. + + last_name: Last name of the person the address is registered to. + + business_name: Business name, when the address belongs to a business. Defaults to the person's + full name. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -82,14 +89,14 @@ def create( body=maybe_transform( { "country_code": country_code, + "first_name": first_name, + "last_name": last_name, "locality": locality, "postal_code": postal_code, "street_address": street_address, "administrative_area": administrative_area, "business_name": business_name, "extended_address": extended_address, - "first_name": first_name, - "last_name": last_name, }, address_create_params.AddressCreateParams, ), @@ -186,9 +193,11 @@ def delete( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: - """Delete a regulatory address. + """Delete a regulatory address from this project. - Cannot delete addresses that are in use. + Any address can be deleted, + whatever its status. Phone numbers already purchased with it are not affected, + and neither is information already submitted for later purchases in its country. Args: extra_headers: Send extra headers @@ -235,14 +244,14 @@ async def create( self, *, country_code: str, + first_name: str, + last_name: str, locality: str, postal_code: str, street_address: str, administrative_area: str | Omit = omit, business_name: str | Omit = omit, extended_address: str | Omit = omit, - first_name: str | Omit = omit, - last_name: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -250,12 +259,19 @@ async def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AddressCreateResponse: - """Create a regulatory address for phone number purchases. - - Some countries require a - verified address before phone numbers can be activated. + """ + Create a regulatory address, to use as the value of an `address` requirement + when buying a phone number. It is registered for review when it is created, with + status `pending`. Args: + first_name: First name of the person the address is registered to. + + last_name: Last name of the person the address is registered to. + + business_name: Business name, when the address belongs to a business. Defaults to the person's + full name. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -269,14 +285,14 @@ async def create( body=await async_maybe_transform( { "country_code": country_code, + "first_name": first_name, + "last_name": last_name, "locality": locality, "postal_code": postal_code, "street_address": street_address, "administrative_area": administrative_area, "business_name": business_name, "extended_address": extended_address, - "first_name": first_name, - "last_name": last_name, }, address_create_params.AddressCreateParams, ), @@ -373,9 +389,11 @@ async def delete( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: - """Delete a regulatory address. + """Delete a regulatory address from this project. - Cannot delete addresses that are in use. + Any address can be deleted, + whatever its status. Phone numbers already purchased with it are not affected, + and neither is information already submitted for later purchases in its country. Args: extra_headers: Send extra headers diff --git a/src/zavudev/resources/phone_numbers.py b/src/zavudev/resources/phone_numbers.py index cd0ee4a..d8a4329 100644 --- a/src/zavudev/resources/phone_numbers.py +++ b/src/zavudev/resources/phone_numbers.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Optional +from typing import Iterable, Optional import httpx @@ -111,7 +111,9 @@ def update( Args: name: Custom name for the phone number. Set to null to clear. - sender_id: Sender ID to assign the phone number to. Set to null to unassign. + sender_id: Sender ID to assign the phone number to. Set to null to unassign. A number under + regulatory review is recorded now and connected to the sender when approved; a + rejected number is refused. extra_headers: Send extra headers @@ -192,6 +194,8 @@ def purchase( *, phone_number: str, name: str | Omit = omit, + regulatory_requirements: Iterable[phone_number_purchase_params.RegulatoryRequirement] | Omit = omit, + type: PhoneNumberType | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -202,17 +206,63 @@ def purchase( """Purchase an available phone number. Requires a paid plan: the Free plan cannot - purchase phone numbers and receives `402` with code `paid_plan_required`. Paid - plans include one US number at no charge. The included number is one per account - and is granted once: claiming it spends the benefit for good, so releasing that - number does not make another one free, and numbers the account already bought do - not consume it. + purchase phone numbers and receives `402` with code `paid_plan_required`. + + **The included number.** A paid plan includes one number at no charge, once per + account: it must be a US or Canadian number (a +1 number) costing $20 a month or + less. `isFreeEligible` in `GET /v1/phone-numbers/available` marks the numbers + that qualify. Claiming it spends the benefit for good, across every team the + account owner owns, so releasing that number does not make another one free. + + **Numbers with regulatory requirements.** Which numbers need regulatory + information is decided per number, not by a fixed country list. The purchase + looks the requirements up for the exact number before charging anything: + + 1. `GET /v1/phone-numbers/requirements?phoneNumber=...`. If `items` is empty, + buy normally. + 2. Create what it asks for: addresses with `POST /v1/addresses`, documents with + `POST /v1/documents`. + 3. Purchase with `type` and `regulatoryRequirements`. The number is bought and + billed at once with `regulatoryStatus: pending_review`. + 4. Poll `GET /v1/phone-numbers/{phoneNumberId}` until `regulatoryStatus` is + `approved`. Assign it to a sender before or after approval; it starts + carrying messages once approved. + + **Reuse.** Information you submitted is kept for your project, per country and + `type`, and a later purchase there may omit `regulatoryRequirements`. Reuse only + happens when what is kept still covers every requirement of the new number and + every address and document in it belongs to the project. Otherwise, or when + nothing is kept, the purchase returns `400 regulatory_compliance_required` with + the missing requirements in `details`. + + Invalid values (a missing, unknown or repeated requirement id, an address or + document from another project, or one rejected in review) return + `400 invalid_request`. If an address or document cannot be registered for + review, the purchase returns `400 invalid_request` naming the requirement. If + the requirements cannot be looked up, the purchase returns + `502 requirements_unavailable`, except for US and Canadian numbers, which are + sold as numbers without requirements. None of these errors charge anything. Args: phone_number: Phone number in E.164 format. name: Optional custom name for the phone number. + regulatory_requirements: Regulatory information, for numbers whose requirements list is not empty. Get + the list with `GET /v1/phone-numbers/requirements?phoneNumber=...` and send one + entry per requirement id, except `action` requirements, which take no value. + Every required id must be present, once, and no unknown id may be sent; + otherwise the purchase is refused with `400 invalid_request` before anything is + charged. + + The information is kept for your project under the number's country and `type`. + A later purchase there may omit this field if what is kept still covers that + number's requirements. Omit it for numbers without requirements. + + type: Type of phone number. `mobile` is stocked in countries where no geographic + (`local`) or non-geographic (`national`) inventory exists, and in several + markets it is the only type that can receive SMS. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -227,6 +277,8 @@ def purchase( { "phone_number": phone_number, "name": name, + "regulatory_requirements": regulatory_requirements, + "type": type, }, phone_number_purchase_params.PhoneNumberPurchaseParams, ), @@ -274,7 +326,8 @@ def release( def requirements( self, *, - country_code: str, + country_code: str | Omit = omit, + phone_number: str | Omit = omit, type: PhoneNumberType | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -284,14 +337,31 @@ def requirements( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PhoneNumberRequirementsResponse: """ - Get regulatory requirements for purchasing phone numbers in a specific country. - Some countries require additional documentation (addresses, identity documents) - before phone numbers can be activated. + Get the regulatory information needed to buy a phone number, for one specific + number or for a country and number type. Prefer `phoneNumber`: the response is + then exactly the list the purchase of that number validates against. Pass each + `requirementTypes[].id` back as `requirementType` in `regulatoryRequirements` on + `POST /v1/phone-numbers`. + + For `phoneNumber`, the requirements of that exact number are returned. When they + cannot be resolved for the number itself, the list for its country and `type` is + returned instead, and the purchase uses the same list. An empty `items` array + means the number needs no regulatory information. If the requirements cannot be + retrieved at all, the response is `502 requirements_unavailable`, never an empty + list. + + URL-encode the `+` of `phoneNumber` as `%2B`. An unencoded `+` is also accepted. Args: - country_code: Two-letter ISO country code. + country_code: Two-letter ISO country code. Required unless `phoneNumber` is given. + + phone_number: E.164 number from `GET /v1/phone-numbers/available`, with `+` encoded as `%2B`. + Returns the requirements the purchase of that number checks. Takes precedence + over `countryCode`. - type: Type of phone number (local, mobile, tollFree). + type: Type of phone number (local, national, mobile, tollFree). Defaults to `local`. + With `phoneNumber`, used only when the number's own requirements cannot be + resolved and the country list is returned. extra_headers: Send extra headers @@ -311,6 +381,7 @@ def requirements( query=maybe_transform( { "country_code": country_code, + "phone_number": phone_number, "type": type, }, phone_number_requirements_params.PhoneNumberRequirementsParams, @@ -451,7 +522,9 @@ async def update( Args: name: Custom name for the phone number. Set to null to clear. - sender_id: Sender ID to assign the phone number to. Set to null to unassign. + sender_id: Sender ID to assign the phone number to. Set to null to unassign. A number under + regulatory review is recorded now and connected to the sender when approved; a + rejected number is refused. extra_headers: Send extra headers @@ -532,6 +605,8 @@ async def purchase( *, phone_number: str, name: str | Omit = omit, + regulatory_requirements: Iterable[phone_number_purchase_params.RegulatoryRequirement] | Omit = omit, + type: PhoneNumberType | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -542,17 +617,63 @@ async def purchase( """Purchase an available phone number. Requires a paid plan: the Free plan cannot - purchase phone numbers and receives `402` with code `paid_plan_required`. Paid - plans include one US number at no charge. The included number is one per account - and is granted once: claiming it spends the benefit for good, so releasing that - number does not make another one free, and numbers the account already bought do - not consume it. + purchase phone numbers and receives `402` with code `paid_plan_required`. + + **The included number.** A paid plan includes one number at no charge, once per + account: it must be a US or Canadian number (a +1 number) costing $20 a month or + less. `isFreeEligible` in `GET /v1/phone-numbers/available` marks the numbers + that qualify. Claiming it spends the benefit for good, across every team the + account owner owns, so releasing that number does not make another one free. + + **Numbers with regulatory requirements.** Which numbers need regulatory + information is decided per number, not by a fixed country list. The purchase + looks the requirements up for the exact number before charging anything: + + 1. `GET /v1/phone-numbers/requirements?phoneNumber=...`. If `items` is empty, + buy normally. + 2. Create what it asks for: addresses with `POST /v1/addresses`, documents with + `POST /v1/documents`. + 3. Purchase with `type` and `regulatoryRequirements`. The number is bought and + billed at once with `regulatoryStatus: pending_review`. + 4. Poll `GET /v1/phone-numbers/{phoneNumberId}` until `regulatoryStatus` is + `approved`. Assign it to a sender before or after approval; it starts + carrying messages once approved. + + **Reuse.** Information you submitted is kept for your project, per country and + `type`, and a later purchase there may omit `regulatoryRequirements`. Reuse only + happens when what is kept still covers every requirement of the new number and + every address and document in it belongs to the project. Otherwise, or when + nothing is kept, the purchase returns `400 regulatory_compliance_required` with + the missing requirements in `details`. + + Invalid values (a missing, unknown or repeated requirement id, an address or + document from another project, or one rejected in review) return + `400 invalid_request`. If an address or document cannot be registered for + review, the purchase returns `400 invalid_request` naming the requirement. If + the requirements cannot be looked up, the purchase returns + `502 requirements_unavailable`, except for US and Canadian numbers, which are + sold as numbers without requirements. None of these errors charge anything. Args: phone_number: Phone number in E.164 format. name: Optional custom name for the phone number. + regulatory_requirements: Regulatory information, for numbers whose requirements list is not empty. Get + the list with `GET /v1/phone-numbers/requirements?phoneNumber=...` and send one + entry per requirement id, except `action` requirements, which take no value. + Every required id must be present, once, and no unknown id may be sent; + otherwise the purchase is refused with `400 invalid_request` before anything is + charged. + + The information is kept for your project under the number's country and `type`. + A later purchase there may omit this field if what is kept still covers that + number's requirements. Omit it for numbers without requirements. + + type: Type of phone number. `mobile` is stocked in countries where no geographic + (`local`) or non-geographic (`national`) inventory exists, and in several + markets it is the only type that can receive SMS. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -567,6 +688,8 @@ async def purchase( { "phone_number": phone_number, "name": name, + "regulatory_requirements": regulatory_requirements, + "type": type, }, phone_number_purchase_params.PhoneNumberPurchaseParams, ), @@ -614,7 +737,8 @@ async def release( async def requirements( self, *, - country_code: str, + country_code: str | Omit = omit, + phone_number: str | Omit = omit, type: PhoneNumberType | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -624,14 +748,31 @@ async def requirements( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PhoneNumberRequirementsResponse: """ - Get regulatory requirements for purchasing phone numbers in a specific country. - Some countries require additional documentation (addresses, identity documents) - before phone numbers can be activated. + Get the regulatory information needed to buy a phone number, for one specific + number or for a country and number type. Prefer `phoneNumber`: the response is + then exactly the list the purchase of that number validates against. Pass each + `requirementTypes[].id` back as `requirementType` in `regulatoryRequirements` on + `POST /v1/phone-numbers`. + + For `phoneNumber`, the requirements of that exact number are returned. When they + cannot be resolved for the number itself, the list for its country and `type` is + returned instead, and the purchase uses the same list. An empty `items` array + means the number needs no regulatory information. If the requirements cannot be + retrieved at all, the response is `502 requirements_unavailable`, never an empty + list. + + URL-encode the `+` of `phoneNumber` as `%2B`. An unencoded `+` is also accepted. Args: - country_code: Two-letter ISO country code. + country_code: Two-letter ISO country code. Required unless `phoneNumber` is given. + + phone_number: E.164 number from `GET /v1/phone-numbers/available`, with `+` encoded as `%2B`. + Returns the requirements the purchase of that number checks. Takes precedence + over `countryCode`. - type: Type of phone number (local, mobile, tollFree). + type: Type of phone number (local, national, mobile, tollFree). Defaults to `local`. + With `phoneNumber`, used only when the number's own requirements cannot be + resolved and the country list is returned. extra_headers: Send extra headers @@ -651,6 +792,7 @@ async def requirements( query=await async_maybe_transform( { "country_code": country_code, + "phone_number": phone_number, "type": type, }, phone_number_requirements_params.PhoneNumberRequirementsParams, diff --git a/src/zavudev/resources/senders/senders.py b/src/zavudev/resources/senders/senders.py index ab19db8..5ee6e39 100644 --- a/src/zavudev/resources/senders/senders.py +++ b/src/zavudev/resources/senders/senders.py @@ -145,8 +145,10 @@ def create( phone_number: Phone number in E.164 format, and it must be a number your project already owns (see `GET /v1/phone-numbers`). The number is routed to the sender as part of this call, which is what turns the SMS channel on. Passing a number the project - does not own, or one already attached to another sender, returns 400 rather than - creating a sender that cannot send. Omit for an email-only sender. + does not own, one already attached to another sender, or one rejected in + regulatory review returns 400 rather than creating a sender that cannot send. A + number still under review is attached and starts carrying messages when it is + approved. Omit for an email-only sender. webhook_events: Events to subscribe to. @@ -679,8 +681,10 @@ async def create( phone_number: Phone number in E.164 format, and it must be a number your project already owns (see `GET /v1/phone-numbers`). The number is routed to the sender as part of this call, which is what turns the SMS channel on. Passing a number the project - does not own, or one already attached to another sender, returns 400 rather than - creating a sender that cannot send. Omit for an email-only sender. + does not own, one already attached to another sender, or one rejected in + regulatory review returns 400 rather than creating a sender that cannot send. A + number still under review is attached and starts carrying messages when it is + approved. Omit for an email-only sender. webhook_events: Events to subscribe to. diff --git a/src/zavudev/types/address_create_params.py b/src/zavudev/types/address_create_params.py index a014cd5..7c20576 100644 --- a/src/zavudev/types/address_create_params.py +++ b/src/zavudev/types/address_create_params.py @@ -12,6 +12,12 @@ class AddressCreateParams(TypedDict, total=False): country_code: Required[Annotated[str, PropertyInfo(alias="countryCode")]] + first_name: Required[Annotated[str, PropertyInfo(alias="firstName")]] + """First name of the person the address is registered to.""" + + last_name: Required[Annotated[str, PropertyInfo(alias="lastName")]] + """Last name of the person the address is registered to.""" + locality: Required[str] postal_code: Required[Annotated[str, PropertyInfo(alias="postalCode")]] @@ -21,9 +27,9 @@ class AddressCreateParams(TypedDict, total=False): administrative_area: Annotated[str, PropertyInfo(alias="administrativeArea")] business_name: Annotated[str, PropertyInfo(alias="businessName")] + """Business name, when the address belongs to a business. - extended_address: Annotated[str, PropertyInfo(alias="extendedAddress")] + Defaults to the person's full name. + """ - first_name: Annotated[str, PropertyInfo(alias="firstName")] - - last_name: Annotated[str, PropertyInfo(alias="lastName")] + extended_address: Annotated[str, PropertyInfo(alias="extendedAddress")] diff --git a/src/zavudev/types/owned_phone_number.py b/src/zavudev/types/owned_phone_number.py index e13925b..6550d5c 100644 --- a/src/zavudev/types/owned_phone_number.py +++ b/src/zavudev/types/owned_phone_number.py @@ -2,6 +2,7 @@ from typing import List, Optional from datetime import datetime +from typing_extensions import Literal from pydantic import Field as FieldInfo @@ -23,7 +24,31 @@ class OwnedPhoneNumber(BaseModel): pricing: OwnedPhoneNumberPricing + regulatory_status: Literal["approved", "pending_review", "rejected"] = FieldInfo(alias="regulatoryStatus") + """Regulatory review state. + + Numbers that need no review are `approved` immediately. A number bought with + regulatory information is owned and billed from purchase and starts + `pending_review`; it cannot send messages or place calls until this is + `approved`. The state is re-checked every 6 hours: poll + `GET /v1/phone-numbers/{phoneNumberId}` to follow it. + + Assign it to a sender with `PATCH /v1/phone-numbers/{phoneNumberId}` + (`senderId`) before or after approval. A number assigned while under review is + recorded and connected to that sender when it is approved; the connection is + retried until it succeeds. A sender created over the API is set up for SMS as + part of the assignment. `rejected` means review refused the information: the + number cannot be assigned to a sender. A number that stays `pending_review` may + be waiting on information the API cannot supply; contact support. + """ + status: PhoneNumberStatus + """Billing state of an owned number, separate from `regulatoryStatus`. + + `pending` is legacy and is not written to numbers today. The SDKs carry + `active`, `suspended` and `pending` only; `releasing` and `released` are + returned by the REST API until their next release. + """ name: Optional[str] = None """Optional custom name for the phone number.""" diff --git a/src/zavudev/types/phone_number_pricing.py b/src/zavudev/types/phone_number_pricing.py index 0dc1b8a..6db023b 100644 --- a/src/zavudev/types/phone_number_pricing.py +++ b/src/zavudev/types/phone_number_pricing.py @@ -11,10 +11,11 @@ class PhoneNumberPricing(BaseModel): is_free_eligible: Optional[bool] = FieldInfo(alias="isFreeEligible", default=None) - """Whether this number qualifies as the plan-included US number on paid plans. - - The benefit is one per account: it is never offered again once claimed, not even - after the number is released. + """ + Whether this number qualifies as the plan-included number: a US or Canadian + number (a +1 number) costing $20 a month or less. The benefit is one per + account: it is never offered again once claimed, not even after the number is + released. """ monthly_price: Optional[float] = FieldInfo(alias="monthlyPrice", default=None) diff --git a/src/zavudev/types/phone_number_purchase_params.py b/src/zavudev/types/phone_number_purchase_params.py index 5928239..7938ea9 100644 --- a/src/zavudev/types/phone_number_purchase_params.py +++ b/src/zavudev/types/phone_number_purchase_params.py @@ -2,11 +2,13 @@ from __future__ import annotations +from typing import Iterable from typing_extensions import Required, Annotated, TypedDict from .._utils import PropertyInfo +from .phone_number_type import PhoneNumberType -__all__ = ["PhoneNumberPurchaseParams"] +__all__ = ["PhoneNumberPurchaseParams", "RegulatoryRequirement"] class PhoneNumberPurchaseParams(TypedDict, total=False): @@ -15,3 +17,42 @@ class PhoneNumberPurchaseParams(TypedDict, total=False): name: str """Optional custom name for the phone number.""" + + regulatory_requirements: Annotated[Iterable[RegulatoryRequirement], PropertyInfo(alias="regulatoryRequirements")] + """Regulatory information, for numbers whose requirements list is not empty. + + Get the list with `GET /v1/phone-numbers/requirements?phoneNumber=...` and send + one entry per requirement id, except `action` requirements, which take no value. + Every required id must be present, once, and no unknown id may be sent; + otherwise the purchase is refused with `400 invalid_request` before anything is + charged. + + The information is kept for your project under the number's country and `type`. + A later purchase there may omit this field if what is kept still covers that + number's requirements. Omit it for numbers without requirements. + """ + + type: PhoneNumberType + """Type of phone number. + + `mobile` is stocked in countries where no geographic (`local`) or non-geographic + (`national`) inventory exists, and in several markets it is the only type that + can receive SMS. + """ + + +class RegulatoryRequirement(TypedDict, total=False): + field_value: Required[Annotated[str, PropertyInfo(alias="fieldValue")]] + """ + Depends on the requirement's `type`: the text itself for `textual`; for + `address`, the `id` of an address created in this project with + `POST /v1/addresses`; for `document`, the `id` of a document created with + `POST /v1/documents`. An address or document from another project, or one + rejected in review, is refused. + """ + + requirement_type: Required[Annotated[str, PropertyInfo(alias="requirementType")]] + """A `requirementTypes[].id` from `GET /v1/phone-numbers/requirements`. + + Each id may appear only once. + """ diff --git a/src/zavudev/types/phone_number_requirements_params.py b/src/zavudev/types/phone_number_requirements_params.py index 964fa2d..c960244 100644 --- a/src/zavudev/types/phone_number_requirements_params.py +++ b/src/zavudev/types/phone_number_requirements_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing_extensions import Required, Annotated, TypedDict +from typing_extensions import Annotated, TypedDict from .._utils import PropertyInfo from .phone_number_type import PhoneNumberType @@ -11,8 +11,19 @@ class PhoneNumberRequirementsParams(TypedDict, total=False): - country_code: Required[Annotated[str, PropertyInfo(alias="countryCode")]] - """Two-letter ISO country code.""" + country_code: Annotated[str, PropertyInfo(alias="countryCode")] + """Two-letter ISO country code. Required unless `phoneNumber` is given.""" + + phone_number: Annotated[str, PropertyInfo(alias="phoneNumber")] + """E.164 number from `GET /v1/phone-numbers/available`, with `+` encoded as `%2B`. + + Returns the requirements the purchase of that number checks. Takes precedence + over `countryCode`. + """ type: PhoneNumberType - """Type of phone number (local, mobile, tollFree).""" + """Type of phone number (local, national, mobile, tollFree). + + Defaults to `local`. With `phoneNumber`, used only when the number's own + requirements cannot be resolved and the country list is returned. + """ diff --git a/src/zavudev/types/phone_number_status.py b/src/zavudev/types/phone_number_status.py index 6dba633..a4b93a5 100644 --- a/src/zavudev/types/phone_number_status.py +++ b/src/zavudev/types/phone_number_status.py @@ -4,4 +4,4 @@ __all__ = ["PhoneNumberStatus"] -PhoneNumberStatus: TypeAlias = Literal["active", "suspended", "pending"] +PhoneNumberStatus: TypeAlias = Literal["active", "suspended", "pending", "releasing", "released"] diff --git a/src/zavudev/types/phone_number_update_params.py b/src/zavudev/types/phone_number_update_params.py index ba62aef..2a80e3d 100644 --- a/src/zavudev/types/phone_number_update_params.py +++ b/src/zavudev/types/phone_number_update_params.py @@ -15,4 +15,8 @@ class PhoneNumberUpdateParams(TypedDict, total=False): """Custom name for the phone number. Set to null to clear.""" sender_id: Annotated[Optional[str], PropertyInfo(alias="senderId")] - """Sender ID to assign the phone number to. Set to null to unassign.""" + """Sender ID to assign the phone number to. + + Set to null to unassign. A number under regulatory review is recorded now and + connected to the sender when approved; a rejected number is refused. + """ diff --git a/src/zavudev/types/requirement.py b/src/zavudev/types/requirement.py index 9ff01d5..dd39400 100644 --- a/src/zavudev/types/requirement.py +++ b/src/zavudev/types/requirement.py @@ -11,7 +11,9 @@ class Requirement(BaseModel): - """A group of requirements for a specific country/phone type combination.""" + """ + The requirements for ordering a number: for a country and number type, or for one specific number when requested with `phoneNumber` (then `id` is that phone number and `countryCode` is taken from it). + """ id: str diff --git a/src/zavudev/types/requirement_type.py b/src/zavudev/types/requirement_type.py index f656fb5..a6c3a71 100644 --- a/src/zavudev/types/requirement_type.py +++ b/src/zavudev/types/requirement_type.py @@ -15,6 +15,7 @@ class RequirementType(BaseModel): """A specific requirement type within a requirement group.""" id: str + """Send this as `requirementType` in `regulatoryRequirements` when purchasing.""" description: str diff --git a/src/zavudev/types/sender_create_params.py b/src/zavudev/types/sender_create_params.py index 8bfb662..17d8b29 100644 --- a/src/zavudev/types/sender_create_params.py +++ b/src/zavudev/types/sender_create_params.py @@ -56,8 +56,10 @@ class SenderCreateParams(TypedDict, total=False): Phone number in E.164 format, and it must be a number your project already owns (see `GET /v1/phone-numbers`). The number is routed to the sender as part of this call, which is what turns the SMS channel on. Passing a number the project - does not own, or one already attached to another sender, returns 400 rather than - creating a sender that cannot send. Omit for an email-only sender. + does not own, one already attached to another sender, or one rejected in + regulatory review returns 400 rather than creating a sender that cannot send. A + number still under review is attached and starts carrying messages when it is + approved. Omit for an email-only sender. """ set_as_default: Annotated[bool, PropertyInfo(alias="setAsDefault")] diff --git a/tests/api_resources/test_addresses.py b/tests/api_resources/test_addresses.py index e8e27e0..1d8b267 100644 --- a/tests/api_resources/test_addresses.py +++ b/tests/api_resources/test_addresses.py @@ -27,6 +27,8 @@ class TestAddresses: def test_method_create(self, client: Zavudev) -> None: address = client.addresses.create( country_code="DE", + first_name="John", + last_name="Doe", locality="Berlin", postal_code="10115", street_address="123 Main St", @@ -38,14 +40,14 @@ def test_method_create(self, client: Zavudev) -> None: def test_method_create_with_all_params(self, client: Zavudev) -> None: address = client.addresses.create( country_code="DE", + first_name="John", + last_name="Doe", locality="Berlin", postal_code="10115", street_address="123 Main St", administrative_area="administrativeArea", business_name="businessName", extended_address="extendedAddress", - first_name="John", - last_name="Doe", ) assert_matches_type(AddressCreateResponse, address, path=["response"]) @@ -54,6 +56,8 @@ def test_method_create_with_all_params(self, client: Zavudev) -> None: def test_raw_response_create(self, client: Zavudev) -> None: response = client.addresses.with_raw_response.create( country_code="DE", + first_name="John", + last_name="Doe", locality="Berlin", postal_code="10115", street_address="123 Main St", @@ -69,6 +73,8 @@ def test_raw_response_create(self, client: Zavudev) -> None: def test_streaming_response_create(self, client: Zavudev) -> None: with client.addresses.with_streaming_response.create( country_code="DE", + first_name="John", + last_name="Doe", locality="Berlin", postal_code="10115", street_address="123 Main St", @@ -213,6 +219,8 @@ class TestAsyncAddresses: async def test_method_create(self, async_client: AsyncZavudev) -> None: address = await async_client.addresses.create( country_code="DE", + first_name="John", + last_name="Doe", locality="Berlin", postal_code="10115", street_address="123 Main St", @@ -224,14 +232,14 @@ async def test_method_create(self, async_client: AsyncZavudev) -> None: async def test_method_create_with_all_params(self, async_client: AsyncZavudev) -> None: address = await async_client.addresses.create( country_code="DE", + first_name="John", + last_name="Doe", locality="Berlin", postal_code="10115", street_address="123 Main St", administrative_area="administrativeArea", business_name="businessName", extended_address="extendedAddress", - first_name="John", - last_name="Doe", ) assert_matches_type(AddressCreateResponse, address, path=["response"]) @@ -240,6 +248,8 @@ async def test_method_create_with_all_params(self, async_client: AsyncZavudev) - async def test_raw_response_create(self, async_client: AsyncZavudev) -> None: response = await async_client.addresses.with_raw_response.create( country_code="DE", + first_name="John", + last_name="Doe", locality="Berlin", postal_code="10115", street_address="123 Main St", @@ -255,6 +265,8 @@ async def test_raw_response_create(self, async_client: AsyncZavudev) -> None: async def test_streaming_response_create(self, async_client: AsyncZavudev) -> None: async with async_client.addresses.with_streaming_response.create( country_code="DE", + first_name="John", + last_name="Doe", locality="Berlin", postal_code="10115", street_address="123 Main St", diff --git a/tests/api_resources/test_phone_numbers.py b/tests/api_resources/test_phone_numbers.py index 3e8f6ab..9c3a39c 100644 --- a/tests/api_resources/test_phone_numbers.py +++ b/tests/api_resources/test_phone_numbers.py @@ -171,6 +171,13 @@ def test_method_purchase_with_all_params(self, client: Zavudev) -> None: phone_number = client.phone_numbers.purchase( phone_number="+15551234567", name="Primary Line", + regulatory_requirements=[ + { + "field_value": "jd7x2k3m4n5p6q7r8s9t0abc", + "requirement_type": "8c5b1a2e-0f3d-4f5b-9a61-2c7e4d9b1f10", + } + ], + type="local", ) assert_matches_type(PhoneNumberPurchaseResponse, phone_number, path=["response"]) @@ -245,9 +252,7 @@ def test_path_params_release(self, client: Zavudev) -> None: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_requirements(self, client: Zavudev) -> None: - phone_number = client.phone_numbers.requirements( - country_code="xx", - ) + phone_number = client.phone_numbers.requirements() assert_matches_type(PhoneNumberRequirementsResponse, phone_number, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -255,6 +260,7 @@ def test_method_requirements(self, client: Zavudev) -> None: def test_method_requirements_with_all_params(self, client: Zavudev) -> None: phone_number = client.phone_numbers.requirements( country_code="xx", + phone_number="phoneNumber", type="local", ) assert_matches_type(PhoneNumberRequirementsResponse, phone_number, path=["response"]) @@ -262,9 +268,7 @@ def test_method_requirements_with_all_params(self, client: Zavudev) -> None: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_requirements(self, client: Zavudev) -> None: - response = client.phone_numbers.with_raw_response.requirements( - country_code="xx", - ) + response = client.phone_numbers.with_raw_response.requirements() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -274,9 +278,7 @@ def test_raw_response_requirements(self, client: Zavudev) -> None: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_requirements(self, client: Zavudev) -> None: - with client.phone_numbers.with_streaming_response.requirements( - country_code="xx", - ) as response: + with client.phone_numbers.with_streaming_response.requirements() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -483,6 +485,13 @@ async def test_method_purchase_with_all_params(self, async_client: AsyncZavudev) phone_number = await async_client.phone_numbers.purchase( phone_number="+15551234567", name="Primary Line", + regulatory_requirements=[ + { + "field_value": "jd7x2k3m4n5p6q7r8s9t0abc", + "requirement_type": "8c5b1a2e-0f3d-4f5b-9a61-2c7e4d9b1f10", + } + ], + type="local", ) assert_matches_type(PhoneNumberPurchaseResponse, phone_number, path=["response"]) @@ -557,9 +566,7 @@ async def test_path_params_release(self, async_client: AsyncZavudev) -> None: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_requirements(self, async_client: AsyncZavudev) -> None: - phone_number = await async_client.phone_numbers.requirements( - country_code="xx", - ) + phone_number = await async_client.phone_numbers.requirements() assert_matches_type(PhoneNumberRequirementsResponse, phone_number, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -567,6 +574,7 @@ async def test_method_requirements(self, async_client: AsyncZavudev) -> None: async def test_method_requirements_with_all_params(self, async_client: AsyncZavudev) -> None: phone_number = await async_client.phone_numbers.requirements( country_code="xx", + phone_number="phoneNumber", type="local", ) assert_matches_type(PhoneNumberRequirementsResponse, phone_number, path=["response"]) @@ -574,9 +582,7 @@ async def test_method_requirements_with_all_params(self, async_client: AsyncZavu @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_requirements(self, async_client: AsyncZavudev) -> None: - response = await async_client.phone_numbers.with_raw_response.requirements( - country_code="xx", - ) + response = await async_client.phone_numbers.with_raw_response.requirements() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -586,9 +592,7 @@ async def test_raw_response_requirements(self, async_client: AsyncZavudev) -> No @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_requirements(self, async_client: AsyncZavudev) -> None: - async with async_client.phone_numbers.with_streaming_response.requirements( - country_code="xx", - ) as response: + async with async_client.phone_numbers.with_streaming_response.requirements() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" From cb6fffac9c515d242d6f9c5a225bdae885a40a7f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 08:47:21 +0000 Subject: [PATCH 03/15] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index bbcd963..52045d0 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 183 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-6a4c9cdb8d971155ac22e679a37d27525a97b8a65991908a1c12969252457811.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-c6e6f410e3490fde4e2a5aaca9a4b609856685cfc4b8540c6cef5c2cef097622.yml openapi_spec_hash: 5d999f34f096d592deaa6319f52190b3 -config_hash: 261e1b852ca7f8364e4a283a3edd350a +config_hash: f6a05e2edf2cd1af3f762b8ac759b9a7 From 45765abb2786cb62b67c3924e43845346e6384b1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 09:10:15 +0000 Subject: [PATCH 04/15] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 52045d0..bbcd963 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 183 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-c6e6f410e3490fde4e2a5aaca9a4b609856685cfc4b8540c6cef5c2cef097622.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-6a4c9cdb8d971155ac22e679a37d27525a97b8a65991908a1c12969252457811.yml openapi_spec_hash: 5d999f34f096d592deaa6319f52190b3 -config_hash: f6a05e2edf2cd1af3f762b8ac759b9a7 +config_hash: 261e1b852ca7f8364e4a283a3edd350a From fae26f73607793a1abd9c5ea6263544fdf21f1d2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 09:12:13 +0000 Subject: [PATCH 05/15] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index bbcd963..52045d0 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 183 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-6a4c9cdb8d971155ac22e679a37d27525a97b8a65991908a1c12969252457811.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-c6e6f410e3490fde4e2a5aaca9a4b609856685cfc4b8540c6cef5c2cef097622.yml openapi_spec_hash: 5d999f34f096d592deaa6319f52190b3 -config_hash: 261e1b852ca7f8364e4a283a3edd350a +config_hash: f6a05e2edf2cd1af3f762b8ac759b9a7 From 0eba0b433e64fc732cc52020b6d7f907bede6792 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 14:21:25 +0000 Subject: [PATCH 06/15] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 52045d0..0384ef5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 183 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-c6e6f410e3490fde4e2a5aaca9a4b609856685cfc4b8540c6cef5c2cef097622.yml -openapi_spec_hash: 5d999f34f096d592deaa6319f52190b3 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-cf8de13bbc99e47431a3597b98b7a1d7c5161639ed7513c2b7db3e5ea49c4a20.yml +openapi_spec_hash: 4b35637b2a0e3645fc53e8ccc0b1f2de config_hash: f6a05e2edf2cd1af3f762b8ac759b9a7 From b05594c35e38a800ccd15ec1eeaa2e833b3ab5c6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:57:42 +0000 Subject: [PATCH 07/15] feat(api): api update --- .stats.yml | 4 ++-- src/zavudev/types/sender.py | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.stats.yml b/.stats.yml index 0384ef5..63896e7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 183 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-cf8de13bbc99e47431a3597b98b7a1d7c5161639ed7513c2b7db3e5ea49c4a20.yml -openapi_spec_hash: 4b35637b2a0e3645fc53e8ccc0b1f2de +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-212cea44bc8db5e2be7beb83eafaba27da3ad2d2989b630e05a472e7c91dd6fc.yml +openapi_spec_hash: 965feddccfebb0fc94a7682954a950f0 config_hash: f6a05e2edf2cd1af3f762b8ac759b9a7 diff --git a/src/zavudev/types/sender.py b/src/zavudev/types/sender.py index 4991ea6..eaf047a 100644 --- a/src/zavudev/types/sender.py +++ b/src/zavudev/types/sender.py @@ -49,11 +49,13 @@ class Sender(BaseModel): """Phone number in E.164 format.""" channels: Optional[List[str]] = None - """ - Channels this sender can actually send on right now, computed from its - configuration. Empty means the sender cannot send or receive anything yet: a - phoneNumber alone does not enable SMS or voice. Check this rather than inferring - capability from phoneNumber or emailAddress. + """Channels this sender can actually send on right now: configured AND activated. + + Empty means the sender cannot send or receive anything yet: a phoneNumber alone + does not enable SMS or voice, and a connected account that is not activated is + left out, because every send on it is refused. Check this rather than inferring + capability from phoneNumber or emailAddress, and turn a connected channel on + with `POST /v1/senders/{senderId}/channels/{channel}/activate`. """ created_at: Optional[datetime] = FieldInfo(alias="createdAt", default=None) From aab53c77cf90c954c9dd644424aa3dccc921480b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 19:13:05 +0000 Subject: [PATCH 08/15] feat(api): api update --- .stats.yml | 4 ++-- src/zavudev/resources/senders/senders.py | 22 ++++++++++++++++------ src/zavudev/types/sender_create_params.py | 4 +++- src/zavudev/types/sender_update_params.py | 7 ++++++- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/.stats.yml b/.stats.yml index 63896e7..1a193ff 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 183 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-212cea44bc8db5e2be7beb83eafaba27da3ad2d2989b630e05a472e7c91dd6fc.yml -openapi_spec_hash: 965feddccfebb0fc94a7682954a950f0 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-5257b1c4e3b9259ae2df6cd9ac2d2a31a39ddb336f50baf0dadbcc865c1913f3.yml +openapi_spec_hash: 4103decc37e4053c1cba58ba9527a5e7 config_hash: f6a05e2edf2cd1af3f762b8ac759b9a7 diff --git a/src/zavudev/resources/senders/senders.py b/src/zavudev/resources/senders/senders.py index 5ee6e39..ccf60a1 100644 --- a/src/zavudev/resources/senders/senders.py +++ b/src/zavudev/resources/senders/senders.py @@ -130,8 +130,10 @@ def create( email_from_name: Display name shown in the recipient's inbox for the email channel. - email_receiving_enabled: Enable inbound email receiving on this sender. Requires a verified MX record on - the domain; ignored otherwise. + email_receiving_enabled: Enable inbound email receiving on this sender. Requires a verified inbound MX + record on the domain; the request is ignored otherwise. Read + `emailReceivingEnabled` back off the response to see whether it was applied — it + comes back `false` when the MX has not verified. enable_sms_oneway: Enable the one-way SMS channel (`sms_oneway`). Needs nothing else — no phone number, no credential — so it is the fastest way to get a sender that can send. @@ -273,7 +275,10 @@ def update( email_from_name: Display name shown in the recipient's inbox for the email channel. - email_receiving_enabled: Enable or disable inbound email receiving for this sender. + email_receiving_enabled: Enable or disable inbound email receiving for this sender. Enabling requires a + verified inbound MX record on the domain; the request is ignored otherwise, and + `emailReceivingEnabled` comes back `false` on the response. Disabling always + applies. enable_sms_oneway: Turn the one-way SMS channel on or off. Enabling needs nothing else and takes effect immediately; disabling removes the channel from the sender. Confirm with @@ -666,8 +671,10 @@ async def create( email_from_name: Display name shown in the recipient's inbox for the email channel. - email_receiving_enabled: Enable inbound email receiving on this sender. Requires a verified MX record on - the domain; ignored otherwise. + email_receiving_enabled: Enable inbound email receiving on this sender. Requires a verified inbound MX + record on the domain; the request is ignored otherwise. Read + `emailReceivingEnabled` back off the response to see whether it was applied — it + comes back `false` when the MX has not verified. enable_sms_oneway: Enable the one-way SMS channel (`sms_oneway`). Needs nothing else — no phone number, no credential — so it is the fastest way to get a sender that can send. @@ -809,7 +816,10 @@ async def update( email_from_name: Display name shown in the recipient's inbox for the email channel. - email_receiving_enabled: Enable or disable inbound email receiving for this sender. + email_receiving_enabled: Enable or disable inbound email receiving for this sender. Enabling requires a + verified inbound MX record on the domain; the request is ignored otherwise, and + `emailReceivingEnabled` comes back `false` on the response. Disabling always + applies. enable_sms_oneway: Turn the one-way SMS channel on or off. Enabling needs nothing else and takes effect immediately; disabling removes the channel from the sender. Confirm with diff --git a/src/zavudev/types/sender_create_params.py b/src/zavudev/types/sender_create_params.py index 17d8b29..c6c4760 100644 --- a/src/zavudev/types/sender_create_params.py +++ b/src/zavudev/types/sender_create_params.py @@ -33,7 +33,9 @@ class SenderCreateParams(TypedDict, total=False): email_receiving_enabled: Annotated[bool, PropertyInfo(alias="emailReceivingEnabled")] """Enable inbound email receiving on this sender. - Requires a verified MX record on the domain; ignored otherwise. + Requires a verified inbound MX record on the domain; the request is ignored + otherwise. Read `emailReceivingEnabled` back off the response to see whether it + was applied — it comes back `false` when the MX has not verified. """ enable_sms_oneway: Annotated[bool, PropertyInfo(alias="enableSmsOneway")] diff --git a/src/zavudev/types/sender_update_params.py b/src/zavudev/types/sender_update_params.py index 3e7e87d..8828d6c 100644 --- a/src/zavudev/types/sender_update_params.py +++ b/src/zavudev/types/sender_update_params.py @@ -37,7 +37,12 @@ class SenderUpdateParams(TypedDict, total=False): """Display name shown in the recipient's inbox for the email channel.""" email_receiving_enabled: Annotated[bool, PropertyInfo(alias="emailReceivingEnabled")] - """Enable or disable inbound email receiving for this sender.""" + """Enable or disable inbound email receiving for this sender. + + Enabling requires a verified inbound MX record on the domain; the request is + ignored otherwise, and `emailReceivingEnabled` comes back `false` on the + response. Disabling always applies. + """ enable_sms_oneway: Annotated[bool, PropertyInfo(alias="enableSmsOneway")] """Turn the one-way SMS channel on or off. From 0cf22951bce9cb1719aeac4690629d55aa1d3dab Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 16:53:23 +0000 Subject: [PATCH 09/15] feat(api): api update --- .stats.yml | 4 +-- src/zavudev/resources/templates.py | 30 +++++++++++++++------ src/zavudev/types/template_sync_response.py | 5 +++- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/.stats.yml b/.stats.yml index 1a193ff..a3be4c8 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 183 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-5257b1c4e3b9259ae2df6cd9ac2d2a31a39ddb336f50baf0dadbcc865c1913f3.yml -openapi_spec_hash: 4103decc37e4053c1cba58ba9527a5e7 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-7ddbc5b54ce2afee5a9c67569f652309a8f79e02643e74af3afba4cb7310f737.yml +openapi_spec_hash: de4fcf9ef758597cfcc59299d12d7b33 config_hash: f6a05e2edf2cd1af3f762b8ac759b9a7 diff --git a/src/zavudev/resources/templates.py b/src/zavudev/resources/templates.py index bb802f7..1334dd4 100644 --- a/src/zavudev/resources/templates.py +++ b/src/zavudev/resources/templates.py @@ -312,11 +312,18 @@ def sync( ) -> TemplateSyncResponse: """Reconcile this project's templates against WhatsApp. - Two things happen per + Three things happen per connected WhatsApp Business Account: templates that exist on Meta but not in - Zavu are imported (or linked to an existing template with the same name), and - the approval status of the templates Zavu already knows about is refreshed from - Meta. + Zavu are imported (or linked to an existing template with the same name), the + approval status of the templates Zavu already knows about is refreshed from + Meta, and their **category** is refreshed from Meta. + + The category matters because it is what each message is billed under, and Meta + reassigns it on its own — commonly `UTILITY` to `MARKETING`, on a template that + is already approved and whose status therefore never moves. A template whose + category changed but whose status did not is still counted in `updated`. This is + the way to repair templates whose category drifted before you started listening + for `template.status_changed`. This is what to call when a template was created outside Zavu — in Meta Business Manager, or by another tool — or when a `template.status_changed` webhook was @@ -635,11 +642,18 @@ async def sync( ) -> TemplateSyncResponse: """Reconcile this project's templates against WhatsApp. - Two things happen per + Three things happen per connected WhatsApp Business Account: templates that exist on Meta but not in - Zavu are imported (or linked to an existing template with the same name), and - the approval status of the templates Zavu already knows about is refreshed from - Meta. + Zavu are imported (or linked to an existing template with the same name), the + approval status of the templates Zavu already knows about is refreshed from + Meta, and their **category** is refreshed from Meta. + + The category matters because it is what each message is billed under, and Meta + reassigns it on its own — commonly `UTILITY` to `MARKETING`, on a template that + is already approved and whose status therefore never moves. A template whose + category changed but whose status did not is still counted in `updated`. This is + the way to repair templates whose category drifted before you started listening + for `template.status_changed`. This is what to call when a template was created outside Zavu — in Meta Business Manager, or by another tool — or when a `template.status_changed` webhook was diff --git a/src/zavudev/types/template_sync_response.py b/src/zavudev/types/template_sync_response.py index 8fa51e6..0b1098a 100644 --- a/src/zavudev/types/template_sync_response.py +++ b/src/zavudev/types/template_sync_response.py @@ -36,4 +36,7 @@ class TemplateSyncResponse(BaseModel): """ updated: int - """Templates whose approval status changed to match Meta.""" + """Templates brought back in line with Meta — approval status, category, or both. + + A template whose status and category both moved is counted once. + """ From 04a0b6dbf002cc4c92180a3ce84cd8c0992ca5d6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 21:53:17 +0000 Subject: [PATCH 10/15] feat(api): api update --- .stats.yml | 4 ++-- src/zavudev/resources/messages.py | 26 ++++++++++++++++++++------ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/.stats.yml b/.stats.yml index a3be4c8..ac9266e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 183 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-7ddbc5b54ce2afee5a9c67569f652309a8f79e02643e74af3afba4cb7310f737.yml -openapi_spec_hash: de4fcf9ef758597cfcc59299d12d7b33 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-be6c0709d91750e8a9ea4ac11c26c00ef39b0e09233beb454131452d10a99e95.yml +openapi_spec_hash: ba279d70955f6b4da3fcf63e92158406 config_hash: f6a05e2edf2cd1af3f762b8ac759b9a7 diff --git a/src/zavudev/resources/messages.py b/src/zavudev/resources/messages.py index 1e3c984..265ff49 100644 --- a/src/zavudev/resources/messages.py +++ b/src/zavudev/resources/messages.py @@ -258,9 +258,16 @@ def send( **Plan allowances and email billing:** - WhatsApp, Telegram, Instagram and Messenger share an allowance of 2,000 - messages per month on Free. Over it, sends return 429 with code - `a2p_limit_exceeded` and upgrade details; the counter resets on the 1st of - each month. Paid plans have no message caps + messages per month on Free. **It counts messages in both directions**: a + message a contact sends you consumes one unit exactly as a message you send + them does, so a project that has sent 300 and received 1,700 has used the + whole allowance. Messages you send from the WhatsApp Business App on your own + phone under coexistence are mirrored into your inbox but never counted, and + neither are failed sends. Over the allowance, sends return 429 with code + `a2p_limit_exceeded` and upgrade details, **and inbound messages on those + channels are refused as well**: not stored, not shown in the inbox, and no + `message.inbound` webhook, and not delivered later when the month resets. The + counter resets on the 1st of each month. Paid plans have no message caps - Email is billed from your prepaid balance in 1,000-message blocks: $0.40 per 1,000 transactional emails, $0.80 per 1,000 marketing (broadcast) emails. A block is charged when your monthly count crosses each 1,000 boundary, and at @@ -649,9 +656,16 @@ async def send( **Plan allowances and email billing:** - WhatsApp, Telegram, Instagram and Messenger share an allowance of 2,000 - messages per month on Free. Over it, sends return 429 with code - `a2p_limit_exceeded` and upgrade details; the counter resets on the 1st of - each month. Paid plans have no message caps + messages per month on Free. **It counts messages in both directions**: a + message a contact sends you consumes one unit exactly as a message you send + them does, so a project that has sent 300 and received 1,700 has used the + whole allowance. Messages you send from the WhatsApp Business App on your own + phone under coexistence are mirrored into your inbox but never counted, and + neither are failed sends. Over the allowance, sends return 429 with code + `a2p_limit_exceeded` and upgrade details, **and inbound messages on those + channels are refused as well**: not stored, not shown in the inbox, and no + `message.inbound` webhook, and not delivered later when the month resets. The + counter resets on the 1st of each month. Paid plans have no message caps - Email is billed from your prepaid balance in 1,000-message blocks: $0.40 per 1,000 transactional emails, $0.80 per 1,000 marketing (broadcast) emails. A block is charged when your monthly count crosses each 1,000 boundary, and at From 7e29fa34b6f63690bab2d73d04640acb39f7027c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 19:24:37 +0000 Subject: [PATCH 11/15] feat(api): api update --- .stats.yml | 4 +-- .../resources/broadcasts/broadcasts.py | 34 +++++++++++-------- src/zavudev/resources/messages.py | 10 ++++-- src/zavudev/resources/senders/senders.py | 16 ++++++--- src/zavudev/types/sender_create_params.py | 4 ++- src/zavudev/types/sender_update_params.py | 3 ++ 6 files changed, 48 insertions(+), 23 deletions(-) diff --git a/.stats.yml b/.stats.yml index ac9266e..ea60dc8 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 183 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-be6c0709d91750e8a9ea4ac11c26c00ef39b0e09233beb454131452d10a99e95.yml -openapi_spec_hash: ba279d70955f6b4da3fcf63e92158406 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-2c4f59b60c0f846111040e670ba837bee5fe7a3eefb3e7fb905903b710df1be0.yml +openapi_spec_hash: 0a1d60101d1226bce5c3879b70f9050f config_hash: f6a05e2edf2cd1af3f762b8ac759b9a7 diff --git a/src/zavudev/resources/broadcasts/broadcasts.py b/src/zavudev/resources/broadcasts/broadcasts.py index b97067e..9d2b296 100644 --- a/src/zavudev/resources/broadcasts/broadcasts.py +++ b/src/zavudev/resources/broadcasts/broadcasts.py @@ -520,13 +520,16 @@ def send( An account that has verified nothing is refused with `403` and code `kyc_required` on every channel other than `whatsapp`. Any one of these lifts it: identity verification (KYC), a saved payment method, a settled deposit, or a - paid plan. Business verification (KYB) is not required to broadcast; it gates - 10DLC registration only. A `whatsapp` broadcast is exempt: it can only be built - on a template, and Meta vets the business and the content when it approves that - template, so an unapproved template is refused instead. `smart` is not exempt, - since it can route a contact to SMS or email. Drafts can be created, edited and - kept without any check. Every send path (dashboard, API and CLI) enforces the - same rule. + paid plan. Business verification (KYB) is not required to broadcast on any + channel except `sms_oneway`, which is refused with `403` and code `KYB_REQUIRED` + until it is approved; KYB also gates 10DLC registration. A `smart` broadcast is + never refused for KYB: without it, one-way SMS is simply dropped from the + channels smart routing may pick for a contact. A `whatsapp` broadcast is exempt: + it can only be built on a template, and Meta vets the business and the content + when it approves that template, so an unapproved template is refused instead. + `smart` is not exempt, since it can route a contact to SMS or email. Drafts can + be created, edited and kept without any check. Every send path (dashboard, API + and CLI) enforces the same rule. **Daily ceilings apply per recipient.** Each message a broadcast sends counts against the channel's daily ceiling (see `POST /v1/messages`). Once the ceiling @@ -1035,13 +1038,16 @@ async def send( An account that has verified nothing is refused with `403` and code `kyc_required` on every channel other than `whatsapp`. Any one of these lifts it: identity verification (KYC), a saved payment method, a settled deposit, or a - paid plan. Business verification (KYB) is not required to broadcast; it gates - 10DLC registration only. A `whatsapp` broadcast is exempt: it can only be built - on a template, and Meta vets the business and the content when it approves that - template, so an unapproved template is refused instead. `smart` is not exempt, - since it can route a contact to SMS or email. Drafts can be created, edited and - kept without any check. Every send path (dashboard, API and CLI) enforces the - same rule. + paid plan. Business verification (KYB) is not required to broadcast on any + channel except `sms_oneway`, which is refused with `403` and code `KYB_REQUIRED` + until it is approved; KYB also gates 10DLC registration. A `smart` broadcast is + never refused for KYB: without it, one-way SMS is simply dropped from the + channels smart routing may pick for a contact. A `whatsapp` broadcast is exempt: + it can only be built on a template, and Meta vets the business and the content + when it approves that template, so an unapproved template is refused instead. + `smart` is not exempt, since it can route a contact to SMS or email. Drafts can + be created, edited and kept without any check. Every send path (dashboard, API + and CLI) enforces the same rule. **Daily ceilings apply per recipient.** Each message a broadcast sends counts against the channel's daily ceiling (see `POST /v1/messages`). Once the ceiling diff --git a/src/zavudev/resources/messages.py b/src/zavudev/resources/messages.py index 265ff49..bb33577 100644 --- a/src/zavudev/resources/messages.py +++ b/src/zavudev/resources/messages.py @@ -287,7 +287,10 @@ def send( Zavu's sandbox number. One verification covers WhatsApp, SMS and calls, up to 5 numbers per project. To send to any destination, do any one of these: verify your identity, add a payment method, settle a deposit, or subscribe to a paid - plan. Business verification (KYB) is never required to send + plan. Business verification (KYB) is required for **one channel only**: + `sms_oneway`. Without an approved KYB, one-way SMS returns `403` with code + `kyb_required` and `details.dashboardUrl` pointing at `/kyb`, whatever the + account has otherwise verified. No other channel asks for it - Daily ceilings apply per channel group and rise with verification. An account that has verified nothing: 25/day across `sms` + `sms_oneway`, 5/day for `voice`, 100/day across WhatsApp, Telegram, Instagram and Messenger combined. @@ -685,7 +688,10 @@ async def send( Zavu's sandbox number. One verification covers WhatsApp, SMS and calls, up to 5 numbers per project. To send to any destination, do any one of these: verify your identity, add a payment method, settle a deposit, or subscribe to a paid - plan. Business verification (KYB) is never required to send + plan. Business verification (KYB) is required for **one channel only**: + `sms_oneway`. Without an approved KYB, one-way SMS returns `403` with code + `kyb_required` and `details.dashboardUrl` pointing at `/kyb`, whatever the + account has otherwise verified. No other channel asks for it - Daily ceilings apply per channel group and rise with verification. An account that has verified nothing: 25/day across `sms` + `sms_oneway`, 5/day for `voice`, 100/day across WhatsApp, Telegram, Instagram and Messenger combined. diff --git a/src/zavudev/resources/senders/senders.py b/src/zavudev/resources/senders/senders.py index ccf60a1..7ecacf3 100644 --- a/src/zavudev/resources/senders/senders.py +++ b/src/zavudev/resources/senders/senders.py @@ -138,7 +138,9 @@ def create( enable_sms_oneway: Enable the one-way SMS channel (`sms_oneway`). Needs nothing else — no phone number, no credential — so it is the fastest way to get a sender that can send. Recipients cannot reply. Confirm with `sms_oneway` in the `channels` array on - the response. + the response. Turning the channel on needs nothing, but SENDING on it requires + an approved business verification (KYB): without one every send is refused with + `403 kyb_required`. enable_voice: Let this sender place and answer phone calls. Requires `phoneNumber`; enabling it without one returns 400. Check the `channels` array on the response to @@ -282,7 +284,9 @@ def update( enable_sms_oneway: Turn the one-way SMS channel on or off. Enabling needs nothing else and takes effect immediately; disabling removes the channel from the sender. Confirm with - the `channels` array on the response. + the `channels` array on the response. Turning the channel on needs nothing, but + SENDING on it requires an approved business verification (KYB): without one + every send is refused with `403 kyb_required`. enable_voice: Turn the voice channel on or off. The sender must already have a phone number provisioned for calls; enabling it otherwise returns 400 instead of storing a @@ -679,7 +683,9 @@ async def create( enable_sms_oneway: Enable the one-way SMS channel (`sms_oneway`). Needs nothing else — no phone number, no credential — so it is the fastest way to get a sender that can send. Recipients cannot reply. Confirm with `sms_oneway` in the `channels` array on - the response. + the response. Turning the channel on needs nothing, but SENDING on it requires + an approved business verification (KYB): without one every send is refused with + `403 kyb_required`. enable_voice: Let this sender place and answer phone calls. Requires `phoneNumber`; enabling it without one returns 400. Check the `channels` array on the response to @@ -823,7 +829,9 @@ async def update( enable_sms_oneway: Turn the one-way SMS channel on or off. Enabling needs nothing else and takes effect immediately; disabling removes the channel from the sender. Confirm with - the `channels` array on the response. + the `channels` array on the response. Turning the channel on needs nothing, but + SENDING on it requires an approved business verification (KYB): without one + every send is refused with `403 kyb_required`. enable_voice: Turn the voice channel on or off. The sender must already have a phone number provisioned for calls; enabling it otherwise returns 400 instead of storing a diff --git a/src/zavudev/types/sender_create_params.py b/src/zavudev/types/sender_create_params.py index c6c4760..684b6fc 100644 --- a/src/zavudev/types/sender_create_params.py +++ b/src/zavudev/types/sender_create_params.py @@ -43,7 +43,9 @@ class SenderCreateParams(TypedDict, total=False): Needs nothing else — no phone number, no credential — so it is the fastest way to get a sender that can send. Recipients cannot reply. Confirm with - `sms_oneway` in the `channels` array on the response. + `sms_oneway` in the `channels` array on the response. Turning the channel on + needs nothing, but SENDING on it requires an approved business verification + (KYB): without one every send is refused with `403 kyb_required`. """ enable_voice: Annotated[bool, PropertyInfo(alias="enableVoice")] diff --git a/src/zavudev/types/sender_update_params.py b/src/zavudev/types/sender_update_params.py index 8828d6c..53426ba 100644 --- a/src/zavudev/types/sender_update_params.py +++ b/src/zavudev/types/sender_update_params.py @@ -49,6 +49,9 @@ class SenderUpdateParams(TypedDict, total=False): Enabling needs nothing else and takes effect immediately; disabling removes the channel from the sender. Confirm with the `channels` array on the response. + Turning the channel on needs nothing, but SENDING on it requires an approved + business verification (KYB): without one every send is refused with + `403 kyb_required`. """ enable_voice: Annotated[bool, PropertyInfo(alias="enableVoice")] From 7e1632c7d4d94e60e2c83af46641d2681332769a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 19 Sep 2026 16:33:14 +0000 Subject: [PATCH 12/15] feat(api): api update --- .stats.yml | 4 ++-- src/zavudev/resources/broadcasts/contacts.py | 20 +++++++++++++++++++ src/zavudev/types/broadcast.py | 8 ++++++++ src/zavudev/types/broadcast_contact.py | 13 +++++++++++- src/zavudev/types/broadcast_contact_status.py | 2 +- src/zavudev/types/broadcast_progress.py | 5 ++++- .../types/broadcasts/contact_list_params.py | 13 +++++++++++- 7 files changed, 59 insertions(+), 6 deletions(-) diff --git a/.stats.yml b/.stats.yml index ea60dc8..9cb1abb 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 183 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-2c4f59b60c0f846111040e670ba837bee5fe7a3eefb3e7fb905903b710df1be0.yml -openapi_spec_hash: 0a1d60101d1226bce5c3879b70f9050f +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-055bbc3b65106faf7e50f3d3d93e961d5a9f07f2aef41b5d5524e07eb36a2ce1.yml +openapi_spec_hash: 10eb30ac64baa679f739dc5484f9dfea config_hash: f6a05e2edf2cd1af3f762b8ac759b9a7 diff --git a/src/zavudev/resources/broadcasts/contacts.py b/src/zavudev/resources/broadcasts/contacts.py index e61ae46..836cc1c 100644 --- a/src/zavudev/resources/broadcasts/contacts.py +++ b/src/zavudev/resources/broadcasts/contacts.py @@ -67,6 +67,16 @@ def list( Args: status: Status of a contact within a broadcast. + - `pending`, `queued`, `sending`: not handed to the provider yet. + - `sent`: accepted by the provider; delivery is not confirmed yet. Channels that + never report delivery leave the recipient here. + - `delivered`: the channel confirmed delivery to the device. A WhatsApp read + receipt also counts as delivered. + - `failed`: not delivered. A recipient can move from `sent` or `delivered` to + `failed` when the provider reports a failure late. + - `skipped`: not sent, because the recipient opted out of the channel or the + broadcast was cancelled before reaching it. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -215,6 +225,16 @@ def list( Args: status: Status of a contact within a broadcast. + - `pending`, `queued`, `sending`: not handed to the provider yet. + - `sent`: accepted by the provider; delivery is not confirmed yet. Channels that + never report delivery leave the recipient here. + - `delivered`: the channel confirmed delivery to the device. A WhatsApp read + receipt also counts as delivered. + - `failed`: not delivered. A recipient can move from `sent` or `delivered` to + `failed` when the provider reports a failure late. + - `skipped`: not sent, because the recipient opted out of the channel or the + broadcast was cancelled before reaching it. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request diff --git a/src/zavudev/types/broadcast.py b/src/zavudev/types/broadcast.py index 965dc40..ec2719f 100644 --- a/src/zavudev/types/broadcast.py +++ b/src/zavudev/types/broadcast.py @@ -60,6 +60,7 @@ class Broadcast(BaseModel): """Content for non-text broadcast message types.""" delivered_count: Optional[int] = FieldInfo(alias="deliveredCount", default=None) + """Recipients with confirmed delivery to the device.""" email_subject: Optional[str] = FieldInfo(alias="emailSubject", default=None) @@ -87,6 +88,13 @@ class Broadcast(BaseModel): sending_count: Optional[int] = FieldInfo(alias="sendingCount", default=None) + sent_count: Optional[int] = FieldInfo(alias="sentCount", default=None) + """Recipients whose message the provider accepted, without a confirmed delivery + yet. + + Channels that never report delivery keep their recipients here. + """ + started_at: Optional[datetime] = FieldInfo(alias="startedAt", default=None) text: Optional[str] = None diff --git a/src/zavudev/types/broadcast_contact.py b/src/zavudev/types/broadcast_contact.py index ea706ca..f0edc34 100644 --- a/src/zavudev/types/broadcast_contact.py +++ b/src/zavudev/types/broadcast_contact.py @@ -22,7 +22,18 @@ class BroadcastContact(BaseModel): recipient_type: Literal["phone", "email"] = FieldInfo(alias="recipientType") status: BroadcastContactStatus - """Status of a contact within a broadcast.""" + """Status of a contact within a broadcast. + + - `pending`, `queued`, `sending`: not handed to the provider yet. + - `sent`: accepted by the provider; delivery is not confirmed yet. Channels that + never report delivery leave the recipient here. + - `delivered`: the channel confirmed delivery to the device. A WhatsApp read + receipt also counts as delivered. + - `failed`: not delivered. A recipient can move from `sent` or `delivered` to + `failed` when the provider reports a failure late. + - `skipped`: not sent, because the recipient opted out of the channel or the + broadcast was cancelled before reaching it. + """ cost: Optional[float] = None diff --git a/src/zavudev/types/broadcast_contact_status.py b/src/zavudev/types/broadcast_contact_status.py index 73bdee4..d6c68a5 100644 --- a/src/zavudev/types/broadcast_contact_status.py +++ b/src/zavudev/types/broadcast_contact_status.py @@ -4,4 +4,4 @@ __all__ = ["BroadcastContactStatus"] -BroadcastContactStatus: TypeAlias = Literal["pending", "queued", "sending", "delivered", "failed", "skipped"] +BroadcastContactStatus: TypeAlias = Literal["pending", "queued", "sending", "sent", "delivered", "failed", "skipped"] diff --git a/src/zavudev/types/broadcast_progress.py b/src/zavudev/types/broadcast_progress.py index 0035826..ca38fac 100644 --- a/src/zavudev/types/broadcast_progress.py +++ b/src/zavudev/types/broadcast_progress.py @@ -15,7 +15,7 @@ class BroadcastProgress(BaseModel): broadcast_id: str = FieldInfo(alias="broadcastId") delivered: int - """Successfully delivered.""" + """Confirmed delivered to the device.""" failed: int """Failed to deliver.""" @@ -49,4 +49,7 @@ class BroadcastProgress(BaseModel): reserved_amount: Optional[float] = FieldInfo(alias="reservedAmount", default=None) """Amount reserved from balance in USD.""" + sent: Optional[int] = None + """Accepted by the provider, delivery not confirmed yet.""" + started_at: Optional[datetime] = FieldInfo(alias="startedAt", default=None) diff --git a/src/zavudev/types/broadcasts/contact_list_params.py b/src/zavudev/types/broadcasts/contact_list_params.py index 605e25e..b8c0bc2 100644 --- a/src/zavudev/types/broadcasts/contact_list_params.py +++ b/src/zavudev/types/broadcasts/contact_list_params.py @@ -15,4 +15,15 @@ class ContactListParams(TypedDict, total=False): limit: int status: BroadcastContactStatus - """Status of a contact within a broadcast.""" + """Status of a contact within a broadcast. + + - `pending`, `queued`, `sending`: not handed to the provider yet. + - `sent`: accepted by the provider; delivery is not confirmed yet. Channels that + never report delivery leave the recipient here. + - `delivered`: the channel confirmed delivery to the device. A WhatsApp read + receipt also counts as delivered. + - `failed`: not delivered. A recipient can move from `sent` or `delivered` to + `failed` when the provider reports a failure late. + - `skipped`: not sent, because the recipient opted out of the channel or the + broadcast was cancelled before reaching it. + """ From c019637e4c04a7de3cd248bdaaa00d496422c438 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 20 Sep 2026 12:34:10 +0000 Subject: [PATCH 13/15] feat(api): api update --- .stats.yml | 4 +- .../resources/broadcasts/broadcasts.py | 64 +++++----- src/zavudev/resources/calls.py | 38 +++--- src/zavudev/resources/messages.py | 114 ++++++++++++------ src/zavudev/resources/senders/senders.py | 16 +-- src/zavudev/types/sender_create_params.py | 4 +- src/zavudev/types/sender_update_params.py | 3 - 7 files changed, 129 insertions(+), 114 deletions(-) diff --git a/.stats.yml b/.stats.yml index 9cb1abb..6c831ea 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 183 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-055bbc3b65106faf7e50f3d3d93e961d5a9f07f2aef41b5d5524e07eb36a2ce1.yml -openapi_spec_hash: 10eb30ac64baa679f739dc5484f9dfea +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-42c22e5daf48fe8d621b322e6c9d23d33e160660c2938973490571673ea1ad0d.yml +openapi_spec_hash: f571c734ad0085b66d9a51a0a05d50c0 config_hash: f6a05e2edf2cd1af3f762b8ac759b9a7 diff --git a/src/zavudev/resources/broadcasts/broadcasts.py b/src/zavudev/resources/broadcasts/broadcasts.py index 9d2b296..01428d6 100644 --- a/src/zavudev/resources/broadcasts/broadcasts.py +++ b/src/zavudev/resources/broadcasts/broadcasts.py @@ -516,20 +516,11 @@ def send( """ Start sending the broadcast immediately or schedule for later. - **The account must be past the unverified level to send, except on WhatsApp.** - An account that has verified nothing is refused with `403` and code - `kyc_required` on every channel other than `whatsapp`. Any one of these lifts - it: identity verification (KYC), a saved payment method, a settled deposit, or a - paid plan. Business verification (KYB) is not required to broadcast on any - channel except `sms_oneway`, which is refused with `403` and code `KYB_REQUIRED` - until it is approved; KYB also gates 10DLC registration. A `smart` broadcast is - never refused for KYB: without it, one-way SMS is simply dropped from the - channels smart routing may pick for a contact. A `whatsapp` broadcast is exempt: - it can only be built on a template, and Meta vets the business and the content - when it approves that template, so an unapproved template is refused instead. - `smart` is not exempt, since it can route a contact to SMS or email. Drafts can - be created, edited and kept without any check. Every send path (dashboard, API - and CLI) enforces the same rule. + **Sending a broadcast needs no account verification.** Identity and business + verification raise daily ceilings; neither is a permission to broadcast, on any + channel. What stands in front of a broadcast is the content review below, and it + applies to every send path — dashboard, API and CLI. Drafts can be created, + edited and kept freely. **Daily ceilings apply per recipient.** Each message a broadcast sends counts against the channel's daily ceiling (see `POST /v1/messages`). Once the ceiling @@ -539,10 +530,15 @@ def send( **Review depends on the channel, and cannot be bypassed.** A draft is submitted to automated content review here; it does not go straight out. A WhatsApp broadcast built on a Meta-approved template skips review (Meta already vetted - the content) and begins sending. An email broadcast sends as soon as the - automated review passes. Every other channel moves to `pending_admin_review` and - waits for a person. If the review rejects it, use PATCH to edit the content then - call POST /retry-review. + the content) and begins sending. An email broadcast sends as soon as the review + passes it, unless the review asks for a person. Every other channel moves to + `pending_admin_review` and waits for a person. A broadcast the review refuses + lands on `rejected`: use PATCH to edit the content then call POST /retry-review, + or escalate it for a manual review. + + A broadcast is read once, on its own text, rather than per recipient. While an + account's sending is suspended its recipients fail individually with `errorCode` + `SENDING_SUSPENDED`; see `POST /v1/messages`. Calling this on a broadcast that is already `approved` or `scheduled` sends or reschedules it directly, since it has already been reviewed. Reserves the @@ -1034,20 +1030,11 @@ async def send( """ Start sending the broadcast immediately or schedule for later. - **The account must be past the unverified level to send, except on WhatsApp.** - An account that has verified nothing is refused with `403` and code - `kyc_required` on every channel other than `whatsapp`. Any one of these lifts - it: identity verification (KYC), a saved payment method, a settled deposit, or a - paid plan. Business verification (KYB) is not required to broadcast on any - channel except `sms_oneway`, which is refused with `403` and code `KYB_REQUIRED` - until it is approved; KYB also gates 10DLC registration. A `smart` broadcast is - never refused for KYB: without it, one-way SMS is simply dropped from the - channels smart routing may pick for a contact. A `whatsapp` broadcast is exempt: - it can only be built on a template, and Meta vets the business and the content - when it approves that template, so an unapproved template is refused instead. - `smart` is not exempt, since it can route a contact to SMS or email. Drafts can - be created, edited and kept without any check. Every send path (dashboard, API - and CLI) enforces the same rule. + **Sending a broadcast needs no account verification.** Identity and business + verification raise daily ceilings; neither is a permission to broadcast, on any + channel. What stands in front of a broadcast is the content review below, and it + applies to every send path — dashboard, API and CLI. Drafts can be created, + edited and kept freely. **Daily ceilings apply per recipient.** Each message a broadcast sends counts against the channel's daily ceiling (see `POST /v1/messages`). Once the ceiling @@ -1057,10 +1044,15 @@ async def send( **Review depends on the channel, and cannot be bypassed.** A draft is submitted to automated content review here; it does not go straight out. A WhatsApp broadcast built on a Meta-approved template skips review (Meta already vetted - the content) and begins sending. An email broadcast sends as soon as the - automated review passes. Every other channel moves to `pending_admin_review` and - waits for a person. If the review rejects it, use PATCH to edit the content then - call POST /retry-review. + the content) and begins sending. An email broadcast sends as soon as the review + passes it, unless the review asks for a person. Every other channel moves to + `pending_admin_review` and waits for a person. A broadcast the review refuses + lands on `rejected`: use PATCH to edit the content then call POST /retry-review, + or escalate it for a manual review. + + A broadcast is read once, on its own text, rather than per recipient. While an + account's sending is suspended its recipients fail individually with `errorCode` + `SENDING_SUSPENDED`; see `POST /v1/messages`. Calling this on a broadcast that is already `approved` or `scheduled` sends or reschedules it directly, since it has already been reviewed. Reserves the diff --git a/src/zavudev/resources/calls.py b/src/zavudev/resources/calls.py index 6e14482..235bb39 100644 --- a/src/zavudev/resources/calls.py +++ b/src/zavudev/resources/calls.py @@ -73,15 +73,16 @@ def create( **Requirements:** - The Voice Agents feature must be enabled for your team (otherwise `403`). - - An account that has verified nothing may only call the phone numbers the - project has verified (`403` with code `destination_not_verified`, and - `details.verifiedNumbers` lists them), and at most 5 calls a day (`429` with - code `daily_limit_exceeded`). A number is verified from the dashboard's - Sandbox screen by sending the pre-filled WhatsApp message from that phone; the - same verification covers SMS and calls. Verify your identity, add a payment - method, settle a deposit or subscribe to call any destination. That raises the - ceiling to 50 calls a day on Free; paid plans have no daily call ceiling. Full - reference: https://docs.zavu.dev/concepts/sending-limits + - An account may call any destination from its first minute, within the daily + ceiling: 5 calls a day for an account that has verified nothing, 50 a day on + Free once it has verified its identity, added a payment method, settled a + deposit or subscribed. Paid plans have no daily call ceiling. Over it, `429` + with code `daily_limit_exceeded`. Full reference: + https://docs.zavu.dev/concepts/sending-limits + - The call is read by Zavu's automated risk review before it is dialed, the way + a message is (see `POST /v1/messages`). A call is never held for a person: one + the review stops is failed rather than placed late. Repeated refusals suspend + the account's calling, answered with `403` and code `sending_suspended`. - The sender's agent must have `voice.enabled` set to `true`. - Not available with test-mode API keys. @@ -315,15 +316,16 @@ async def create( **Requirements:** - The Voice Agents feature must be enabled for your team (otherwise `403`). - - An account that has verified nothing may only call the phone numbers the - project has verified (`403` with code `destination_not_verified`, and - `details.verifiedNumbers` lists them), and at most 5 calls a day (`429` with - code `daily_limit_exceeded`). A number is verified from the dashboard's - Sandbox screen by sending the pre-filled WhatsApp message from that phone; the - same verification covers SMS and calls. Verify your identity, add a payment - method, settle a deposit or subscribe to call any destination. That raises the - ceiling to 50 calls a day on Free; paid plans have no daily call ceiling. Full - reference: https://docs.zavu.dev/concepts/sending-limits + - An account may call any destination from its first minute, within the daily + ceiling: 5 calls a day for an account that has verified nothing, 50 a day on + Free once it has verified its identity, added a payment method, settled a + deposit or subscribed. Paid plans have no daily call ceiling. Over it, `429` + with code `daily_limit_exceeded`. Full reference: + https://docs.zavu.dev/concepts/sending-limits + - The call is read by Zavu's automated risk review before it is dialed, the way + a message is (see `POST /v1/messages`). A call is never held for a person: one + the review stops is failed rather than placed late. Repeated refusals suspend + the account's calling, answered with `403` and code `sending_suspended`. - The sender's agent must have `voice.enabled` set to `true`. - Not available with test-mode API keys. diff --git a/src/zavudev/resources/messages.py b/src/zavudev/resources/messages.py index bb33577..029f644 100644 --- a/src/zavudev/resources/messages.py +++ b/src/zavudev/resources/messages.py @@ -276,21 +276,14 @@ def send( 100/day. Teams on earlier plans keep their original email quotas instead - SMS and voice are billed per message from your balance on every plan - **Account verification and daily limits:** - - - A brand-new account can send on every channel immediately, but `sms`, - `sms_oneway` and `voice` reach only the phone numbers the project has - verified. Sending elsewhere returns `403` with code - `destination_not_verified`; `details.verifiedNumbers` lists the numbers that - are reachable. A number is verified from the dashboard's Sandbox screen: - generate a code and send the pre-filled WhatsApp message from that phone to - Zavu's sandbox number. One verification covers WhatsApp, SMS and calls, up to - 5 numbers per project. To send to any destination, do any one of these: verify - your identity, add a payment method, settle a deposit, or subscribe to a paid - plan. Business verification (KYB) is required for **one channel only**: - `sms_oneway`. Without an approved KYB, one-way SMS returns `403` with code - `kyb_required` and `details.dashboardUrl` pointing at `/kyb`, whatever the - account has otherwise verified. No other channel asks for it + **Daily limits:** + + - An account sends on every channel from its first minute, to any destination. + Verification is not a permission to send: identity verification and business + verification (KYB) raise the ceilings below and nothing else asks for them + here. KYB is still required to register a 10DLC brand and campaign, which + every US and Canadian (+1) SMS destination needs — a carrier rule, answered + separately with `403 ten_dlc_required` - Daily ceilings apply per channel group and rise with verification. An account that has verified nothing: 25/day across `sms` + `sms_oneway`, 5/day for `voice`, 100/day across WhatsApp, Telegram, Instagram and Messenger combined. @@ -302,13 +295,37 @@ def send( - The daily ceiling never reduces the monthly allowance: 100/day on the conversational group still reaches the 2,000 monthly A2P messages Free includes - - Email needs no account verification here: a sender with a verified domain - sends from day one, within the plan quota (100/day and 3,000/month on Free). - Over the daily quota it returns `429` with code `daily_limit_exceeded`. Email - broadcasts are the exception: they need the account past the unverified level, - see `POST /v1/broadcasts/{broadcastId}/send` + - Email: a sender with a verified domain sends from day one, within the plan + quota (100/day and 3,000/month on Free). Over the daily quota it returns `429` + with code `daily_limit_exceeded` - Full reference: https://docs.zavu.dev/concepts/sending-limits + **Risk review:** Every outbound `sms`, `sms_oneway`, `email` and `voice` message + is read before it is sent — the content, and how this account has been sending. + What is checked is the message, not who you are. + + - A message can be **held** for a short review. It stays `queued` while it + waits: no new status exists for this, and `MessageStatus` is unchanged. When + it is approved it sends normally. + - A message that is not approved moves to `failed` and fires `message.failed`. + `errorCode` says which happened: `RISK_REJECTED` (a reviewer refused it), + `RISK_REVIEW_EXPIRED` (the review window closed first — it is a couple of + hours, because a code that arrives late is worse than one that does not + arrive), or `RISK_BLOCKED` (refused outright, without a hold). An SMS that + fails this way is not charged; the prepaid amount is returned. + - A call is never held. `POST /v1/calls` fails a call the review stops rather + than placing it hours late. + - A message whose content cannot be read — the check is briefly unavailable — is + held rather than sent. An account with an approved business verification is + unaffected, and so is one that has verified something, already sends real + traffic, and has a clean recent record. + - Repeated refusals suspend an account's sending. While it is suspended every + send is refused with `403` and code `sending_suspended`, + `details.dashboardUrl` points at support, and a message already queued fails + with `errorCode` `SENDING_SUSPENDED`. + - A broadcast is read once, on the broadcast itself, rather than per recipient — + see `POST /v1/broadcasts/{broadcastId}/send`. + **Email recipient pre-flight:** Email messages are validated automatically before dispatch. Sends that would be a guaranteed hard bounce are failed instead of sent, protecting your bounce rate: the message transitions to `failed` @@ -677,21 +694,14 @@ async def send( 100/day. Teams on earlier plans keep their original email quotas instead - SMS and voice are billed per message from your balance on every plan - **Account verification and daily limits:** - - - A brand-new account can send on every channel immediately, but `sms`, - `sms_oneway` and `voice` reach only the phone numbers the project has - verified. Sending elsewhere returns `403` with code - `destination_not_verified`; `details.verifiedNumbers` lists the numbers that - are reachable. A number is verified from the dashboard's Sandbox screen: - generate a code and send the pre-filled WhatsApp message from that phone to - Zavu's sandbox number. One verification covers WhatsApp, SMS and calls, up to - 5 numbers per project. To send to any destination, do any one of these: verify - your identity, add a payment method, settle a deposit, or subscribe to a paid - plan. Business verification (KYB) is required for **one channel only**: - `sms_oneway`. Without an approved KYB, one-way SMS returns `403` with code - `kyb_required` and `details.dashboardUrl` pointing at `/kyb`, whatever the - account has otherwise verified. No other channel asks for it + **Daily limits:** + + - An account sends on every channel from its first minute, to any destination. + Verification is not a permission to send: identity verification and business + verification (KYB) raise the ceilings below and nothing else asks for them + here. KYB is still required to register a 10DLC brand and campaign, which + every US and Canadian (+1) SMS destination needs — a carrier rule, answered + separately with `403 ten_dlc_required` - Daily ceilings apply per channel group and rise with verification. An account that has verified nothing: 25/day across `sms` + `sms_oneway`, 5/day for `voice`, 100/day across WhatsApp, Telegram, Instagram and Messenger combined. @@ -703,13 +713,37 @@ async def send( - The daily ceiling never reduces the monthly allowance: 100/day on the conversational group still reaches the 2,000 monthly A2P messages Free includes - - Email needs no account verification here: a sender with a verified domain - sends from day one, within the plan quota (100/day and 3,000/month on Free). - Over the daily quota it returns `429` with code `daily_limit_exceeded`. Email - broadcasts are the exception: they need the account past the unverified level, - see `POST /v1/broadcasts/{broadcastId}/send` + - Email: a sender with a verified domain sends from day one, within the plan + quota (100/day and 3,000/month on Free). Over the daily quota it returns `429` + with code `daily_limit_exceeded` - Full reference: https://docs.zavu.dev/concepts/sending-limits + **Risk review:** Every outbound `sms`, `sms_oneway`, `email` and `voice` message + is read before it is sent — the content, and how this account has been sending. + What is checked is the message, not who you are. + + - A message can be **held** for a short review. It stays `queued` while it + waits: no new status exists for this, and `MessageStatus` is unchanged. When + it is approved it sends normally. + - A message that is not approved moves to `failed` and fires `message.failed`. + `errorCode` says which happened: `RISK_REJECTED` (a reviewer refused it), + `RISK_REVIEW_EXPIRED` (the review window closed first — it is a couple of + hours, because a code that arrives late is worse than one that does not + arrive), or `RISK_BLOCKED` (refused outright, without a hold). An SMS that + fails this way is not charged; the prepaid amount is returned. + - A call is never held. `POST /v1/calls` fails a call the review stops rather + than placing it hours late. + - A message whose content cannot be read — the check is briefly unavailable — is + held rather than sent. An account with an approved business verification is + unaffected, and so is one that has verified something, already sends real + traffic, and has a clean recent record. + - Repeated refusals suspend an account's sending. While it is suspended every + send is refused with `403` and code `sending_suspended`, + `details.dashboardUrl` points at support, and a message already queued fails + with `errorCode` `SENDING_SUSPENDED`. + - A broadcast is read once, on the broadcast itself, rather than per recipient — + see `POST /v1/broadcasts/{broadcastId}/send`. + **Email recipient pre-flight:** Email messages are validated automatically before dispatch. Sends that would be a guaranteed hard bounce are failed instead of sent, protecting your bounce rate: the message transitions to `failed` diff --git a/src/zavudev/resources/senders/senders.py b/src/zavudev/resources/senders/senders.py index 7ecacf3..ccf60a1 100644 --- a/src/zavudev/resources/senders/senders.py +++ b/src/zavudev/resources/senders/senders.py @@ -138,9 +138,7 @@ def create( enable_sms_oneway: Enable the one-way SMS channel (`sms_oneway`). Needs nothing else — no phone number, no credential — so it is the fastest way to get a sender that can send. Recipients cannot reply. Confirm with `sms_oneway` in the `channels` array on - the response. Turning the channel on needs nothing, but SENDING on it requires - an approved business verification (KYB): without one every send is refused with - `403 kyb_required`. + the response. enable_voice: Let this sender place and answer phone calls. Requires `phoneNumber`; enabling it without one returns 400. Check the `channels` array on the response to @@ -284,9 +282,7 @@ def update( enable_sms_oneway: Turn the one-way SMS channel on or off. Enabling needs nothing else and takes effect immediately; disabling removes the channel from the sender. Confirm with - the `channels` array on the response. Turning the channel on needs nothing, but - SENDING on it requires an approved business verification (KYB): without one - every send is refused with `403 kyb_required`. + the `channels` array on the response. enable_voice: Turn the voice channel on or off. The sender must already have a phone number provisioned for calls; enabling it otherwise returns 400 instead of storing a @@ -683,9 +679,7 @@ async def create( enable_sms_oneway: Enable the one-way SMS channel (`sms_oneway`). Needs nothing else — no phone number, no credential — so it is the fastest way to get a sender that can send. Recipients cannot reply. Confirm with `sms_oneway` in the `channels` array on - the response. Turning the channel on needs nothing, but SENDING on it requires - an approved business verification (KYB): without one every send is refused with - `403 kyb_required`. + the response. enable_voice: Let this sender place and answer phone calls. Requires `phoneNumber`; enabling it without one returns 400. Check the `channels` array on the response to @@ -829,9 +823,7 @@ async def update( enable_sms_oneway: Turn the one-way SMS channel on or off. Enabling needs nothing else and takes effect immediately; disabling removes the channel from the sender. Confirm with - the `channels` array on the response. Turning the channel on needs nothing, but - SENDING on it requires an approved business verification (KYB): without one - every send is refused with `403 kyb_required`. + the `channels` array on the response. enable_voice: Turn the voice channel on or off. The sender must already have a phone number provisioned for calls; enabling it otherwise returns 400 instead of storing a diff --git a/src/zavudev/types/sender_create_params.py b/src/zavudev/types/sender_create_params.py index 684b6fc..c6c4760 100644 --- a/src/zavudev/types/sender_create_params.py +++ b/src/zavudev/types/sender_create_params.py @@ -43,9 +43,7 @@ class SenderCreateParams(TypedDict, total=False): Needs nothing else — no phone number, no credential — so it is the fastest way to get a sender that can send. Recipients cannot reply. Confirm with - `sms_oneway` in the `channels` array on the response. Turning the channel on - needs nothing, but SENDING on it requires an approved business verification - (KYB): without one every send is refused with `403 kyb_required`. + `sms_oneway` in the `channels` array on the response. """ enable_voice: Annotated[bool, PropertyInfo(alias="enableVoice")] diff --git a/src/zavudev/types/sender_update_params.py b/src/zavudev/types/sender_update_params.py index 53426ba..8828d6c 100644 --- a/src/zavudev/types/sender_update_params.py +++ b/src/zavudev/types/sender_update_params.py @@ -49,9 +49,6 @@ class SenderUpdateParams(TypedDict, total=False): Enabling needs nothing else and takes effect immediately; disabling removes the channel from the sender. Confirm with the `channels` array on the response. - Turning the channel on needs nothing, but SENDING on it requires an approved - business verification (KYB): without one every send is refused with - `403 kyb_required`. """ enable_voice: Annotated[bool, PropertyInfo(alias="enableVoice")] From 5c41bdb53bc1191275bc27bd61357eed594b6f55 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 20 Sep 2026 13:41:16 +0000 Subject: [PATCH 14/15] feat(api): api update --- .stats.yml | 4 +-- .../types/senders/agent/flow_trigger.py | 30 +++++++++++++++++-- .../types/senders/agent/flow_trigger_param.py | 30 +++++++++++++++++-- .../api_resources/senders/agent/test_flows.py | 8 ++--- 4 files changed, 60 insertions(+), 12 deletions(-) diff --git a/.stats.yml b/.stats.yml index 6c831ea..98dd734 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 183 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-42c22e5daf48fe8d621b322e6c9d23d33e160660c2938973490571673ea1ad0d.yml -openapi_spec_hash: f571c734ad0085b66d9a51a0a05d50c0 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-e6e18b26cd123fcd2dcaf3554352c9aa1aa890768d7928fa117382f0b4ca89aa.yml +openapi_spec_hash: 6bc9a1491dec8d8157477abeb4aa8f3c config_hash: f6a05e2edf2cd1af3f762b8ac759b9a7 diff --git a/src/zavudev/types/senders/agent/flow_trigger.py b/src/zavudev/types/senders/agent/flow_trigger.py index d8a87ab..bc96271 100644 --- a/src/zavudev/types/senders/agent/flow_trigger.py +++ b/src/zavudev/types/senders/agent/flow_trigger.py @@ -10,10 +10,34 @@ class FlowTrigger(BaseModel): type: Literal["keyword", "intent", "always", "manual"] - """Type of trigger for a flow.""" + """What starts a flow. + + - `keyword`: the message contains one of the words listed in `keywords`. Plain + substring matching, so a word inside another word still counts. + - `intent`: the message MEANS what `intent` describes, whatever words it uses. + - `always`: any message starts it. + - `manual`: reserved. Nothing starts a `manual` flow today — it is accepted and + stored, and no message or endpoint runs it. + """ intent: Optional[str] = None - """Intent that triggers the flow (for intent type).""" + """One plain sentence describing what the contact wants, for `intent` triggers. + + Any language. + + The message is judged for meaning, not for words, so "kiero saber el presio" + starts a flow whose intent is "quiere saber precios o cotizar", and "no quiero + info de precios" starts nothing. + + A `keyword` or `always` flow with a higher `priority` is matched first and wins. + At most 12 intent flows are considered per message, highest priority first. When + the classification is unavailable or uncertain, the message is handled as if no + intent matched, so a flow never starts on a guess. + """ keywords: Optional[List[str]] = None - """Keywords that trigger the flow (for keyword type).""" + """Words that start the flow, for `keyword` triggers. + + Matched as substrings, case-insensitively, against the whole message: a flow on + `info` also starts on "no quiero info". Use `intent` when that matters. + """ diff --git a/src/zavudev/types/senders/agent/flow_trigger_param.py b/src/zavudev/types/senders/agent/flow_trigger_param.py index a6230ba..e9b6860 100644 --- a/src/zavudev/types/senders/agent/flow_trigger_param.py +++ b/src/zavudev/types/senders/agent/flow_trigger_param.py @@ -11,10 +11,34 @@ class FlowTriggerParam(TypedDict, total=False): type: Required[Literal["keyword", "intent", "always", "manual"]] - """Type of trigger for a flow.""" + """What starts a flow. + + - `keyword`: the message contains one of the words listed in `keywords`. Plain + substring matching, so a word inside another word still counts. + - `intent`: the message MEANS what `intent` describes, whatever words it uses. + - `always`: any message starts it. + - `manual`: reserved. Nothing starts a `manual` flow today — it is accepted and + stored, and no message or endpoint runs it. + """ intent: str - """Intent that triggers the flow (for intent type).""" + """One plain sentence describing what the contact wants, for `intent` triggers. + + Any language. + + The message is judged for meaning, not for words, so "kiero saber el presio" + starts a flow whose intent is "quiere saber precios o cotizar", and "no quiero + info de precios" starts nothing. + + A `keyword` or `always` flow with a higher `priority` is matched first and wins. + At most 12 intent flows are considered per message, highest priority first. When + the classification is unavailable or uncertain, the message is handled as if no + intent matched, so a flow never starts on a guess. + """ keywords: SequenceNotStr[str] - """Keywords that trigger the flow (for keyword type).""" + """Words that start the flow, for `keyword` triggers. + + Matched as substrings, case-insensitively, against the whole message: a flow on + `info` also starts on "no quiero info". Use `intent` when that matters. + """ diff --git a/tests/api_resources/senders/agent/test_flows.py b/tests/api_resources/senders/agent/test_flows.py index e2959c7..909012c 100644 --- a/tests/api_resources/senders/agent/test_flows.py +++ b/tests/api_resources/senders/agent/test_flows.py @@ -74,7 +74,7 @@ def test_method_create_with_all_params(self, client: Zavudev) -> None: ], trigger={ "type": "keyword", - "intent": "intent", + "intent": "quiere saber precios o cotizar", "keywords": ["info", "pricing", "demo"], }, description="Capture lead information", @@ -249,7 +249,7 @@ def test_method_update_with_all_params(self, client: Zavudev) -> None: ], trigger={ "type": "keyword", - "intent": "intent", + "intent": "quiere saber precios o cotizar", "keywords": ["string"], }, ) @@ -516,7 +516,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncZavudev) - ], trigger={ "type": "keyword", - "intent": "intent", + "intent": "quiere saber precios o cotizar", "keywords": ["info", "pricing", "demo"], }, description="Capture lead information", @@ -691,7 +691,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncZavudev) - ], trigger={ "type": "keyword", - "intent": "intent", + "intent": "quiere saber precios o cotizar", "keywords": ["string"], }, ) From 2fd2e418438f62e15bd92354c4cbe45d59a3012f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 20 Sep 2026 13:41:42 +0000 Subject: [PATCH 15/15] release: 0.51.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 17 +++++++++++++++++ pyproject.toml | 2 +- src/zavudev/_version.py | 2 +- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 26b1ce2..2b2b4fa 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.50.0" + ".": "0.51.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 7617f51..416e450 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 0.51.0 (2026-09-20) + +Full Changelog: [v0.50.0...v0.51.0](https://github.com/zavudev/sdk-python/compare/v0.50.0...v0.51.0) + +### Features + +* **api:** api update ([5c41bdb](https://github.com/zavudev/sdk-python/commit/5c41bdb53bc1191275bc27bd61357eed594b6f55)) +* **api:** api update ([c019637](https://github.com/zavudev/sdk-python/commit/c019637e4c04a7de3cd248bdaaa00d496422c438)) +* **api:** api update ([7e1632c](https://github.com/zavudev/sdk-python/commit/7e1632c7d4d94e60e2c83af46641d2681332769a)) +* **api:** api update ([7e29fa3](https://github.com/zavudev/sdk-python/commit/7e29fa34b6f63690bab2d73d04640acb39f7027c)) +* **api:** api update ([04a0b6d](https://github.com/zavudev/sdk-python/commit/04a0b6dbf002cc4c92180a3ce84cd8c0992ca5d6)) +* **api:** api update ([0cf2295](https://github.com/zavudev/sdk-python/commit/0cf22951bce9cb1719aeac4690629d55aa1d3dab)) +* **api:** api update ([aab53c7](https://github.com/zavudev/sdk-python/commit/aab53c77cf90c954c9dd644424aa3dccc921480b)) +* **api:** api update ([b05594c](https://github.com/zavudev/sdk-python/commit/b05594c35e38a800ccd15ec1eeaa2e833b3ab5c6)) +* **api:** api update ([9a3148c](https://github.com/zavudev/sdk-python/commit/9a3148cf7bbf2c9ac183e9c2d90ec8f3c6f4da24)) +* **api:** api update ([77c03d9](https://github.com/zavudev/sdk-python/commit/77c03d988890df623ec37dad0c64f96d97ed32b2)) + ## 0.50.0 (2026-09-08) Full Changelog: [v0.49.1...v0.50.0](https://github.com/zavudev/sdk-python/compare/v0.49.1...v0.50.0) diff --git a/pyproject.toml b/pyproject.toml index fb774b6..a11da93 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "zavudev" -version = "0.50.0" +version = "0.51.0" description = "The official Python library for the zavudev API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/zavudev/_version.py b/src/zavudev/_version.py index df55d3f..0666ce6 100644 --- a/src/zavudev/_version.py +++ b/src/zavudev/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "zavudev" -__version__ = "0.50.0" # x-release-please-version +__version__ = "0.51.0" # x-release-please-version