From 8f0577c72a92d57241496022c8642968acc34d07 Mon Sep 17 00:00:00 2001 From: Leon Leibmann Date: Tue, 5 Aug 2025 18:56:34 -0700 Subject: [PATCH 01/11] build out new calls endpoint --- docs/api_reference.rst | 6 + tests/test_calls.py | 285 +++++++++++++++++++++++++++++++++++ textverified/__init__.py | 30 +++- textverified/call_api.py | 165 ++++++++++++++++++++ textverified/data/dtypes.py | 111 +++++++++++++- textverified/textverified.py | 5 + 6 files changed, 594 insertions(+), 8 deletions(-) create mode 100644 tests/test_calls.py create mode 100644 textverified/call_api.py diff --git a/docs/api_reference.rst b/docs/api_reference.rst index 7d61cb7..f5535e8 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -43,6 +43,12 @@ SMS API .. automodule:: textverified.sms_api :members: +Call API +~~~~~~~ + +.. automodule:: textverified.call_api + :members: + Reservations API ~~~~~~~~~~~~~~~ diff --git a/tests/test_calls.py b/tests/test_calls.py new file mode 100644 index 0000000..03c791d --- /dev/null +++ b/tests/test_calls.py @@ -0,0 +1,285 @@ +import pytest +from .fixtures import ( + tv, + mock_http_from_disk, + mock_http, + dict_subset, + verification_compact, + verification_expanded, + renewable_rental_compact, + renewable_rental_expanded, + nonrenewable_rental_compact, + nonrenewable_rental_expanded, +) +from textverified.textverified import TextVerified, BearerToken +from textverified.action import _Action +from textverified.data import ( + Call, + TwilioCallingContextDto, + NonrenewableRentalCompact, + NonrenewableRentalExpanded, + RenewableRentalCompact, + RenewableRentalExpanded, + VerificationCompact, + VerificationExpanded, + ReservationType, + Reservation, +) +import datetime +from unittest.mock import patch + + +def test_list_calls_by_to_number(tv, mock_http_from_disk): + """Test listing calls filtered by destination phone number.""" + calls_list = tv.calls.list(to_number="+1234567890") + + call_messages = [x.to_api() for x in calls_list] + print("truth", mock_http_from_disk.last_response["data"][0]) + print("test", call_messages[0]) + print([ + dict_subset(call_test, call_truth) + for call_test, call_truth in zip(call_messages, mock_http_from_disk.last_response["data"]) + ]) + assert all( + dict_subset(call_test, call_truth) is None + for call_test, call_truth in zip(call_messages, mock_http_from_disk.last_response["data"]) + ) + + +def test_list_calls_by_reservation_type(tv, mock_http_from_disk): + """Test listing calls filtered by reservation type.""" + calls_list = tv.calls.list(to_number="+1234567890", reservation_type=ReservationType.RENEWABLE) + + call_messages = [x.to_api() for x in calls_list] + assert all( + dict_subset(call_test, call_truth) is None + for call_test, call_truth in zip(call_messages, mock_http_from_disk.last_response["data"]) + ) + + +def test_list_calls_by_renewable_rental_compact(tv, mock_http_from_disk, renewable_rental_compact): + """Test listing calls for a renewable rental compact object.""" + calls_list = tv.calls.list(data=renewable_rental_compact) + + call_messages = [x.to_api() for x in calls_list] + assert all( + dict_subset(call_test, call_truth) is None + for call_test, call_truth in zip(call_messages, mock_http_from_disk.last_response["data"]) + ) + + +def test_list_calls_by_renewable_rental_expanded(tv, mock_http_from_disk, renewable_rental_expanded): + """Test listing calls for a renewable rental expanded object.""" + calls_list = tv.calls.list(data=renewable_rental_expanded) + + call_messages = [x.to_api() for x in calls_list] + assert all( + dict_subset(call_test, call_truth) is None + for call_test, call_truth in zip(call_messages, mock_http_from_disk.last_response["data"]) + ) + + +def test_list_calls_by_nonrenewable_rental_compact(tv, mock_http_from_disk, nonrenewable_rental_compact): + """Test listing calls for a nonrenewable rental compact object.""" + calls_list = tv.calls.list(data=nonrenewable_rental_compact) + + call_messages = [x.to_api() for x in calls_list] + assert all( + dict_subset(call_test, call_truth) is None + for call_test, call_truth in zip(call_messages, mock_http_from_disk.last_response["data"]) + ) + + +def test_list_calls_by_nonrenewable_rental_expanded(tv, mock_http_from_disk, nonrenewable_rental_expanded): + """Test listing calls for a nonrenewable rental expanded object.""" + calls_list = tv.calls.list(data=nonrenewable_rental_expanded) + + call_messages = [x.to_api() for x in calls_list] + assert all( + dict_subset(call_test, call_truth) is None + for call_test, call_truth in zip(call_messages, mock_http_from_disk.last_response["data"]) + ) + + +def test_list_calls_by_verification_compact(tv, mock_http_from_disk, verification_compact): + """Test listing calls for a verification compact object.""" + calls_list = tv.calls.list(data=verification_compact) + + call_messages = [x.to_api() for x in calls_list] + assert all( + dict_subset(call_test, call_truth) is None + for call_test, call_truth in zip(call_messages, mock_http_from_disk.last_response["data"]) + ) + + +def test_list_calls_by_verification_expanded(tv, mock_http_from_disk, verification_expanded): + """Test listing calls for a verification expanded object.""" + calls_list = tv.calls.list(data=verification_expanded) + + call_messages = [x.to_api() for x in calls_list] + assert all( + dict_subset(call_test, call_truth) is None + for call_test, call_truth in zip(call_messages, mock_http_from_disk.last_response["data"]) + ) + + +def test_list_calls_all(tv, mock_http_from_disk): + """Test listing all calls without any filters.""" + calls_list = tv.calls.list() + + call_messages = [x.to_api() for x in calls_list] + assert all( + dict_subset(call_test, call_truth) is None + for call_test, call_truth in zip(call_messages, mock_http_from_disk.last_response["data"]) + ) + + +def test_list_calls_with_reservation_object(tv, mock_http_from_disk): + """Test listing calls with a generic Reservation object.""" + reservation = Reservation( + id="reservation_123", + reservation_type=ReservationType.RENEWABLE, + service_name="allservices", + ) + + calls_list = tv.calls.list(data=reservation) + + call_messages = [x.to_api() for x in calls_list] + assert all( + dict_subset(call_test, call_truth) is None + for call_test, call_truth in zip(call_messages, mock_http_from_disk.last_response["data"]) + ) + + +def test_list_calls_error_data_and_to_number(tv): + """Test that providing both data and to_number raises ValueError.""" + rental = RenewableRentalCompact( + created_at=datetime.datetime.now(datetime.timezone.utc), + id="rental_123", + sale_id="sale_123", + service_name="test_service", + state="RENEWABLE_ACTIVE", + billing_cycle_id="cycle_123", + is_included_for_next_renewal=True, + number="+1234567890", + always_on=True, + ) + + with pytest.raises(ValueError, match="Cannot specify both rental/verification data and to_number"): + tv.calls.list(data=rental, to_number="+0987654321") + + +def test_list_calls_error_reservation_type_with_data(tv): + """Test that providing reservation_type with data raises ValueError.""" + rental = RenewableRentalCompact( + created_at=datetime.datetime.now(datetime.timezone.utc), + id="rental_123", + sale_id="sale_123", + service_name="test_service", + state="RENEWABLE_ACTIVE", + billing_cycle_id="cycle_123", + is_included_for_next_renewal=True, + number="+1234567890", + always_on=True, + ) + + with pytest.raises(ValueError, match="Cannot specify reservation_type when using a rental or verification object"): + tv.calls.list(data=rental, reservation_type=ReservationType.RENEWABLE) + + +def test_open_call_session_with_reservation_id(tv, mock_http_from_disk): + """Test opening a call session with a reservation ID string.""" + reservation_id = "reservation_123" + + twilio_context = tv.calls.open_call_session(reservation_id) + + assert isinstance(twilio_context, TwilioCallingContextDto) + # Verify the request was made with correct reservation ID + assert mock_http_from_disk.last_body_params["reservationId"] == reservation_id + + +def test_open_call_session_with_renewable_rental_compact(tv, mock_http_from_disk, renewable_rental_compact): + """Test opening a call session with a RenewableRentalCompact object.""" + twilio_context = tv.calls.open_call_session(renewable_rental_compact) + + assert isinstance(twilio_context, TwilioCallingContextDto) + # Verify the request was made with correct reservation ID + assert mock_http_from_disk.last_body_params["reservationId"] == renewable_rental_compact.id + + +def test_open_call_session_with_renewable_rental_expanded(tv, mock_http_from_disk, renewable_rental_expanded): + """Test opening a call session with a RenewableRentalExpanded object.""" + twilio_context = tv.calls.open_call_session(renewable_rental_expanded) + + assert isinstance(twilio_context, TwilioCallingContextDto) + # Verify the request was made with correct reservation ID + assert mock_http_from_disk.last_body_params["reservationId"] == renewable_rental_expanded.id + + +def test_open_call_session_with_nonrenewable_rental_compact(tv, mock_http_from_disk, nonrenewable_rental_compact): + """Test opening a call session with a NonrenewableRentalCompact object.""" + twilio_context = tv.calls.open_call_session(nonrenewable_rental_compact) + + assert isinstance(twilio_context, TwilioCallingContextDto) + # Verify the request was made with correct reservation ID + assert mock_http_from_disk.last_body_params["reservationId"] == nonrenewable_rental_compact.id + + +def test_open_call_session_with_nonrenewable_rental_expanded(tv, mock_http_from_disk, nonrenewable_rental_expanded): + """Test opening a call session with a NonrenewableRentalExpanded object.""" + twilio_context = tv.calls.open_call_session(nonrenewable_rental_expanded) + + assert isinstance(twilio_context, TwilioCallingContextDto) + # Verify the request was made with correct reservation ID + assert mock_http_from_disk.last_body_params["reservationId"] == nonrenewable_rental_expanded.id + + +def test_open_call_session_with_verification_compact(tv, mock_http_from_disk, verification_compact): + """Test opening a call session with a VerificationCompact object.""" + twilio_context = tv.calls.open_call_session(verification_compact) + + assert isinstance(twilio_context, TwilioCallingContextDto) + # Verify the request was made with correct reservation ID + assert mock_http_from_disk.last_body_params["reservationId"] == verification_compact.id + + +def test_open_call_session_with_verification_expanded(tv, mock_http_from_disk, verification_expanded): + """Test opening a call session with a VerificationExpanded object.""" + twilio_context = tv.calls.open_call_session(verification_expanded) + + assert isinstance(twilio_context, TwilioCallingContextDto) + # Verify the request was made with correct reservation ID + assert mock_http_from_disk.last_body_params["reservationId"] == verification_expanded.id + + +def test_open_call_session_with_reservation_object(tv, mock_http_from_disk): + """Test opening a call session with a generic Reservation object.""" + reservation = Reservation( + id="reservation_123", + reservation_type=ReservationType.RENEWABLE, + service_name="allservices", + ) + + twilio_context = tv.calls.open_call_session(reservation) + + assert isinstance(twilio_context, TwilioCallingContextDto) + # Verify the request was made with correct reservation ID + assert mock_http_from_disk.last_body_params["reservationId"] == reservation.id + + +def test_open_call_session_invalid_reservation_none(tv): + """Test that providing None reservation raises ValueError.""" + with pytest.raises(ValueError, match="reservation_id must be a valid ID or instance of Reservation/Verification"): + tv.calls.open_call_session(None) + + +def test_open_call_session_invalid_reservation_empty_string(tv): + """Test that providing empty string reservation raises ValueError.""" + with pytest.raises(ValueError, match="reservation_id must be a valid ID or instance of Reservation/Verification"): + tv.calls.open_call_session("") + + +def test_open_call_session_invalid_reservation_non_string(tv): + """Test that providing non-string reservation raises ValueError.""" + with pytest.raises(ValueError, match="reservation_id must be a valid ID or instance of Reservation/Verification"): + tv.calls.open_call_session(123) diff --git a/textverified/__init__.py b/textverified/__init__.py index 194eaad..936bd53 100644 --- a/textverified/__init__.py +++ b/textverified/__init__.py @@ -111,7 +111,7 @@ def __call__(self, *args, **kwargs): account_info = account.me() # Get account balance - balance = account.balance() + balance = account.balance """, ) @@ -145,13 +145,16 @@ def __call__(self, *args, **kwargs): configured TextVerified instance. Example: - from textverified import reservations + from textverified import reservations, NewRentalRequest # Create a new reservation - reservation = reservations.create(service_id=1, area_code="555") + reservation = reservations.create(NewRentalRequest(...)) - # List all reservations - all_reservations = reservations.list() + # List renewable reservations + all_reservations = reservations.list_renewable() + + # List non-renewable reservations + non_renewable = reservations.list_nonrenewable() """, ) @@ -257,6 +260,22 @@ def __call__(self, *args, **kwargs): """, ) +calls = _LazyAPI( + "calls", + """ +Static access to call management functionality. + +Provides methods for listing incoming calls, opening call sessions, and managing call-related data. +This is a static wrapper around the CallAPI class that uses the globally +configured TextVerified instance. + +Example: + from textverified import calls + + # List calls + call_list = calls.list() +""", +) # Available for import: __all__ = [ @@ -276,6 +295,7 @@ def __call__(self, *args, **kwargs): "verifications", "wake_requests", "sms", + "calls", # API classes (for direct instantiation if needed) "AccountAPI", "BillingCycleAPI", diff --git a/textverified/call_api.py b/textverified/call_api.py new file mode 100644 index 0000000..691f4bf --- /dev/null +++ b/textverified/call_api.py @@ -0,0 +1,165 @@ +from .action import _ActionPerformer, _Action +from typing import List, Union +from .paginated_list import PaginatedList +from .data import ( + Call, + Reservation, + NonrenewableRentalCompact, + NonrenewableRentalExpanded, + RenewableRentalCompact, + RenewableRentalExpanded, + VerificationCompact, + VerificationExpanded, + ReservationType, + TwilioCallingContextDto, +) + + +class CallAPI: + """API endpoints related to calls.""" + + def __init__(self, client: _ActionPerformer): + self.client = client + + def list( + self, + data: Union[ + Reservation, + NonrenewableRentalCompact, + NonrenewableRentalExpanded, + RenewableRentalCompact, + RenewableRentalExpanded, + VerificationCompact, + VerificationExpanded, + ] = None, + *, + to_number: str = None, + reservation_type: ReservationType = None, + ) -> PaginatedList[Call]: + """List calls to rentals and verifications associated with this account. + + You can retrieve all calls across all your rentals and verifications, or filter by specific criteria. + When providing a rental or verification object, calls for that specific number will be returned. + + Args: + data (Union[Reservation, NonrenewableRentalCompact, NonrenewableRentalExpanded, RenewableRentalCompact, RenewableRentalExpanded, VerificationCompact, VerificationExpanded], optional): A rental or verification object to get calls for. The phone number will be extracted from this object. Defaults to None. + to_number (str, optional): Filter calls by the destination phone number. Cannot be used together with data parameter. Defaults to None. + reservation_type (ReservationType, optional): Filter calls by reservation type (renewable, non-renewable, verification). Cannot be used when providing a data object. Defaults to None. + + Raises: + ValueError: If both data and to_number are provided, or if reservation_type is specified when using a rental/verification object. + + Returns: + PaginatedList[Call]: A paginated list of calls matching the specified criteria. + """ + + # Extract needed data from provided objects + reservation_id = None + if data and isinstance( + data, + ( + NonrenewableRentalCompact, + NonrenewableRentalExpanded, + RenewableRentalCompact, + RenewableRentalExpanded, + VerificationCompact, + VerificationExpanded, + ), + ): + if hasattr(data, "number") and to_number: + raise ValueError("Cannot specify both rental/verification data and to_number.") + to_number = data.number + + if reservation_type is not None: + raise ValueError("Cannot specify reservation_type when using a rental or verification object.") + + if isinstance( + data, + ( + Reservation, + NonrenewableRentalCompact, + NonrenewableRentalExpanded, + RenewableRentalCompact, + RenewableRentalExpanded, + ), + ): + reservation_id = data.id + + # Construct url params + params = dict() + if to_number: + params["to"] = to_number + + if reservation_id: + params["reservationId"] = reservation_id + + if isinstance(reservation_type, ReservationType): + params["reservationType"] = reservation_type.to_api() + + # Construct and perform the action + action = _Action(method="GET", href="/api/pub/v2/calls") + response = self.client._perform_action(action, params=params) + + return PaginatedList(request_json=response.data, parse_item=Call.from_api, api_context=self.client) + + def open_call_session( + self, + reservation: Union[ + str, + Reservation, + NonrenewableRentalCompact, + NonrenewableRentalExpanded, + RenewableRentalCompact, + RenewableRentalExpanded, + VerificationCompact, + VerificationExpanded, + ], + ) -> TwilioCallingContextDto: + """Create a call access token for incoming calls. + + This endpoint generates a short-lived Twilio access token. + The token is associated with a reservation ID, which is obtained from a voice verification. + This token is then used with Twilio's SDKs to handle an incoming call. + + **Warning:** + This is an advanced integration. No support will be provided for implementation or debugging. + You are responsible for implementing the provider's integration to handle incoming calls successfully. + + Workflow: + 1. Creating a voice verification and getting the corresponding `reservation id`. + See: `Creating a voice verification `_ + 2. Posting to this endpoint to obtain a short-lived Twilio access token associated with the `reservation id`. + 3. Using the access token with Twilio's integration to handle an incoming call. + See: `Twilio's integration `_ + + Args: + reservation (Union[str, Reservation, NonrenewableRentalCompact, NonrenewableRentalExpanded, RenewableRentalCompact, RenewableRentalExpanded, VerificationCompact, VerificationExpanded]): The reservation ID or object to create a call session for. + + Returns: + TwilioCallingContextDto: The Twilio calling context containing the access token and other details. + """ + + reservation_id = ( + reservation.id + if isinstance( + reservation, + ( + Reservation, + NonrenewableRentalCompact, + NonrenewableRentalExpanded, + RenewableRentalCompact, + RenewableRentalExpanded, + VerificationCompact, + VerificationExpanded, + ), + ) + else reservation + ) + + if not reservation_id or not isinstance(reservation_id, str): + raise ValueError("reservation_id must be a valid ID or instance of Reservation/Verification.") + + action = _Action(method="POST", href="/api/pub/v2/calls/access-token") + response = self.client._perform_action(action, json={"reservationId": reservation_id}) + + return TwilioCallingContextDto.from_api(response.data) diff --git a/textverified/data/dtypes.py b/textverified/data/dtypes.py index 7830bbc..1796922 100644 --- a/textverified/data/dtypes.py +++ b/textverified/data/dtypes.py @@ -170,7 +170,11 @@ def from_api(cls, value: str) -> 'ReservationSaleState': @dataclass(frozen=True) class Account: username: str + """The username of the account holder.""" + current_balance: float + """The current balance of the account.""" + def to_api(self) -> Dict[str, Any]: api_dict = dict() @@ -238,6 +242,8 @@ def from_api(cls, data: Dict[str, Any]) -> 'AreaCode': class BackOrderReservationCompact: id: str service_name: str + """Name of service""" + status: BackOrderState def to_api(self) -> Dict[str, Any]: @@ -329,6 +335,22 @@ def from_api(cls, data: Dict[str, Any]) -> 'BillingCycleCompact': ) +@dataclass(frozen=True) +class CallSessionRequest: + reservation_id: str + + def to_api(self) -> Dict[str, Any]: + api_dict = dict() + api_dict['reservationId'] = self.reservation_id + return api_dict + + @classmethod + def from_api(cls, data: Dict[str, Any]) -> 'CallSessionRequest': + return cls( + reservation_id=str(data.get("reservationId", None)), + ) + + @dataclass(frozen=True) class CancelAction: can_cancel: bool @@ -472,6 +494,8 @@ class ReservationSaleCompact: id: str state: ReservationSaleState total_cost: float + """Example: 0.95""" + updated_at: datetime.datetime def to_api(self) -> Dict[str, Any]: @@ -540,6 +564,8 @@ class VerificationCompact: service_name: str state: ReservationState total_cost: float + """Example: 0.95""" + number: str def to_api(self) -> Dict[str, Any]: @@ -570,10 +596,10 @@ class VerificationPriceCheckRequest: """Example: yahoo""" area_code: bool - """Example: True""" + """Set to true if a specific area code will be requested when creating a verification, false if any area code can be used.""" carrier: bool - """Example: True""" + """Set to true if a specific carrier will be requested when creating a verification, false if any carrier can be used.""" number_type: NumberType capability: ReservationCapability @@ -752,6 +778,34 @@ def from_api(cls, data: Dict[str, Any]) -> 'BillingCycleWebhookEvent': ) +@dataclass(frozen=True) +class Call: + to_value: str + created_at: datetime.datetime + id: str + from_value: Optional[str] = None + recording_uri: Optional[str] = None + + def to_api(self) -> Dict[str, Any]: + api_dict = dict() + api_dict['to'] = self.to_value + api_dict['createdAt'] = self.created_at.isoformat() + api_dict['id'] = self.id + api_dict['from'] = (self.from_value if self.from_value is not None else None) + api_dict['recordingUri'] = (self.recording_uri if self.recording_uri is not None else None) + return api_dict + + @classmethod + def from_api(cls, data: Dict[str, Any]) -> 'Call': + return cls( + to_value=str(data.get("to", None)), + created_at=dateutil.parser.parse(data.get("createdAt", None)), + id=str(data.get("id", None)), + from_value=(str(data.get("from", None)) if data.get("from", None) is not None else None), + recording_uri=(str(data.get("recordingUri", None)) if data.get("recordingUri", None) is not None else None), + ) + + @dataclass(frozen=True) class Error: error_code: Optional[str] = None @@ -873,10 +927,14 @@ class RenewableRentalCompact: created_at: datetime.datetime id: str service_name: str + """Example: yahoo""" + state: ReservationState billing_cycle_id: str is_included_for_next_renewal: bool number: str + """Example: 2223334444""" + always_on: bool sale_id: Optional[str] = None @@ -940,7 +998,7 @@ class RentalPriceCheckRequest: """Name of the service""" area_code: bool - """Example: True""" + """Set to true if a specific area code will be requested when creating a rental, false if any area code can be used.""" number_type: NumberType capability: ReservationCapability @@ -1108,6 +1166,22 @@ def from_api(cls, data: Dict[str, Any]) -> 'SmsWebhookEvent': ) +@dataclass(frozen=True) +class TwilioCallingContextDto: + token: Optional[str] = None + + def to_api(self) -> Dict[str, Any]: + api_dict = dict() + api_dict['token'] = (self.token if self.token is not None else None) + return api_dict + + @classmethod + def from_api(cls, data: Dict[str, Any]) -> 'TwilioCallingContextDto': + return cls( + token=(str(data.get("token", None)) if data.get("token", None) is not None else None), + ) + + @dataclass(frozen=True) class UsageWindowEstimateResponse: reservation_id: str @@ -1168,6 +1242,8 @@ def from_api(cls, data: Dict[str, Any]) -> 'WakeResponse': class RentalSnapshot: number: str renewal_cost: float + """Renewal cost, in account credits.""" + service_name: str already_renewed: bool included_add_ons: List[AddOnSnapshot] @@ -1360,6 +1436,8 @@ class RenewableRentalExpanded: billing_cycle_id: str is_included_for_next_renewal: bool number: str + """Example: 2223334444""" + always_on: bool sale_id: Optional[str] = None @@ -1401,6 +1479,8 @@ class ReservationSaleExpanded: reservations: List[Reservation] state: ReservationSaleState total: float + """Total amount of the sale, in account credits.""" + updated_at: datetime.datetime def to_api(self) -> Dict[str, Any]: @@ -1440,6 +1520,8 @@ class VerificationExpanded: service_name: str state: ReservationState total_cost: float + """Example: 0.95""" + def to_api(self) -> Dict[str, Any]: api_dict = dict() @@ -1509,6 +1591,27 @@ def from_api(cls, data: Dict[str, Any]) -> 'WebhookEventSmsWebhookEvent': ) +@dataclass(frozen=True) +class CallContext: + reservation_id: str + """Id of the verification that this call context is associated with.""" + + twilio_context: TwilioCallingContextDto + + def to_api(self) -> Dict[str, Any]: + api_dict = dict() + api_dict['reservationId'] = self.reservation_id + api_dict['twilioContext'] = self.twilio_context.to_api() + return api_dict + + @classmethod + def from_api(cls, data: Dict[str, Any]) -> 'CallContext': + return cls( + reservation_id=str(data.get("reservationId", None)), + twilio_context=TwilioCallingContextDto.from_api(data.get("twilioContext", None)), + ) + + @dataclass(frozen=True) class BillingCycleRenewalInvoice: created_at: datetime.datetime @@ -1517,6 +1620,8 @@ class BillingCycleRenewalInvoice: included_rentals: List[RentalSnapshot] is_paid_for: bool total_cost: float + """Total amount cost of the invoice, in account credits.""" + def to_api(self) -> Dict[str, Any]: api_dict = dict() diff --git a/textverified/textverified.py b/textverified/textverified.py index 0bc7bd4..1947f63 100644 --- a/textverified/textverified.py +++ b/textverified/textverified.py @@ -10,6 +10,7 @@ from .sms_api import SMSApi from .verifications_api import VerificationsAPI from .wake_api import WakeAPI +from .call_api import CallAPI import requests import datetime from requests.adapters import HTTPAdapter @@ -75,6 +76,10 @@ def wake_requests(self) -> WakeAPI: def sms(self) -> SMSApi: return SMSApi(self) + @property + def calls(self) -> CallAPI: + return CallAPI(self) + def __post_init__(self): self.bearer = None self.base_url = self.base_url.rstrip("/") From e0d33220de403b6a22c3387ea8cdc341da45f4f4 Mon Sep 17 00:00:00 2001 From: Leon Leibmann Date: Tue, 5 Aug 2025 18:56:43 -0700 Subject: [PATCH 02/11] critical bugfix --- textverified/sms_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/textverified/sms_api.py b/textverified/sms_api.py index a6a9cd1..3ec21d1 100644 --- a/textverified/sms_api.py +++ b/textverified/sms_api.py @@ -96,10 +96,10 @@ def list( params["to"] = to_number if reservation_id: - params["reservation_id"] = reservation_id + params["reservationId"] = reservation_id if isinstance(reservation_type, ReservationType): - params["reservation_type"] = reservation_type.to_api() + params["reservationType"] = reservation_type.to_api() # Construct and perform the action action = _Action(method="GET", href="/api/pub/v2/sms") From 0f7ed998e093fcbba6445c7cac95c5258aa777d8 Mon Sep 17 00:00:00 2001 From: Leon Leibmann Date: Tue, 5 Aug 2025 18:57:00 -0700 Subject: [PATCH 03/11] migrations and updated enums --- generate_enums.py | 63 + swagger-old.json | 3739 +++++++++++++++++ swagger.json | 837 +++- .../api.pub.v2.account.me.json | 4 +- .../api.pub.v2.backorders.{id}.json | 4 +- .../api.pub.v2.billing-cycles.json | 4 +- ...i.pub.v2.billing-cycles.{id}.invoices.json | 56 +- .../api.pub.v2.billing-cycles.{id}.json | 4 +- ...b.v2.billing-cycles.{id}.next-invoice.json | 26 +- .../api.pub.v2.billing-cycles.{id}.renew.json | 26 +- .../api.pub.v2.calls.access-token.json | 10 + .../api.pub.v2.calls.json | 36 + .../api.pub.v2.pricing.rentals.json | 2 +- .../api.pub.v2.pricing.verifications.json | 2 +- .../api.pub.v2.reservations.rental.json | 4 +- ...b.v2.reservations.rental.nonrenewable.json | 12 +- ...reservations.rental.nonrenewable.{id}.json | 8 +- ....pub.v2.reservations.rental.renewable.json | 20 +- ...v2.reservations.rental.renewable.{id}.json | 10 +- .../api.pub.v2.reservations.{id}.health.json | 2 +- .../api.pub.v2.reservations.{id}.json | 4 +- .../api.pub.v2.sales.json | 16 +- .../api.pub.v2.sales.{id}.json | 26 +- .../api.pub.v2.services.json | 4 +- .../api.pub.v2.sms.json | 4 +- .../api.pub.v2.verifications.json | 20 +- .../api.pub.v2.verifications.{id}.json | 22 +- ....pub.v2.verifications.{id}.reactivate.json | 4 +- .../api.pub.v2.verifications.{id}.reuse.json | 4 +- .../api.pub.v2.wake-requests.json | 4 +- 30 files changed, 4725 insertions(+), 252 deletions(-) create mode 100644 swagger-old.json create mode 100644 tests/mock_endpoints_generated/api.pub.v2.calls.access-token.json create mode 100644 tests/mock_endpoints_generated/api.pub.v2.calls.json diff --git a/generate_enums.py b/generate_enums.py index 96e9ae4..bc12bda 100644 --- a/generate_enums.py +++ b/generate_enums.py @@ -660,8 +660,14 @@ def render_mock_call(graph, path: str, path_data: dict) -> str: if compiled_code.strip(): f.write(compiled_code) f.write("\n\n") + print("Generating dtypes.py...") + print("===============================") + print(f"Total nodes: {len(compile_list)}") + print(f"Total properties: {len(raw_graph.edges())}") # Create mock endpoints + print("\nGenerating mock endpoints...") + print("===============================") os.makedirs("./tests/mock_endpoints_generated/", exist_ok=True) unfiltered_graph = create_dependency_graph(swagger.get("components", {}).get("schemas", {}), patterns_to_ignore=[]) for path, path_data in swagger.get("paths", {}).items(): @@ -672,3 +678,60 @@ def render_mock_call(graph, path: str, path_data: dict) -> str: with open(f"./tests/mock_endpoints_generated/{path}.json", "w") as f: json.dump(mock_call_json, f, indent=2) print(f"Mock endpoint for {path} written to {f.name}") + + # If swagger-old exists, show me what must be migrated + if os.path.exists("swagger-old.json"): + print("\nMigration report:") + print("===============================") + + with open("swagger-old.json", "r") as f: + old_swagger = json.load(f) + old_graph = create_dependency_graph(old_swagger.get("components", {}).get("schemas", {})) + + old_nodes = {node_name: node_data["node"] for node_name, node_data in old_graph.nodes(data=True)} + new_nodes = {node_name: node_data["node"] for node_name, node_data in raw_graph.nodes(data=True)} + + for node_name in old_nodes.keys() - new_nodes.keys(): + node = old_nodes[node_name] + if isinstance(node, ObjectNode): + print(f"CRITICAL: Object {node_name} was removed in the new schema.") + elif isinstance(node, EnumNode): + print(f"CRITICAL: Enum {node_name} was removed in the new schema.") + print(f"\tValues: {', '.join(node.values)}") + elif isinstance(node, (DtypeNode, DateTimeNode)): + pass # not important for migration + else: + print(f"CRITICAL: Node {node_name} ({node.annotation_name}) was removed in the new schema.") + + for node_name in new_nodes.keys() - old_nodes.keys(): + node = new_nodes[node_name] + if isinstance(node, ObjectNode): + print(f"Object {node_name} was added in the new schema.") + elif isinstance(node, EnumNode): + print(f"Enum {node_name} was added in the new schema.") + print(f"\tValues: {', '.join(node.values)}") + elif isinstance(node, (DtypeNode, DateTimeNode)): + pass # not important for migration + else: + print(f"Node {node_name} ({node.annotation_name}) was added in the new schema.") + + old_edges = {(u, v) for u, v in old_graph.edges()} + new_edges = {(u, v) for u, v in raw_graph.edges()} + + for src, dst in old_edges - new_edges: + if src in new_nodes: + print(f"CRITICAL: Node {src} lost property {dst} in the new schema.") + + for src, dst in new_edges - old_edges: + if src in old_nodes: + print(f"Node {src} gained property {dst} in the new schema.") + + old_endpoints = set(old_swagger.get("paths", {}).keys()) + new_endpoints = set(swagger.get("paths", {}).keys()) + + print("\nEndpoints changes:") + for endpoint in old_endpoints - new_endpoints: + print(f"CRITICAL: Endpoint {endpoint} was removed in the new schema.") + + for endpoint in new_endpoints - old_endpoints: + print(f"Endpoint {endpoint} was added in the new schema.") diff --git a/swagger-old.json b/swagger-old.json new file mode 100644 index 0000000..a08608c --- /dev/null +++ b/swagger-old.json @@ -0,0 +1,3739 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "API Overview", + "description": "

This is where you can get started with automating your workflows and integrating your apps with our API.

\n

For rentals (long term use of numbers), you'll need to check out the following links in order:

    \n
  1. Authenticate: Authentication
  2. \n
  3. Obtain a list of rental services: Service List
  4. \n
  5. Create a rental: Create New Rental
  6. \n
  7. Follow the location header returned to get the rental sale.
  8. \n
  9. From the sale, you'll be able to see the reservations and backorders.
  10. \n
\n

For verifications (single use of numbers), you'll need to check out the following links in order:

    \n
  1. Authenticate: Authentication
  2. \n
  3. Obtain a list of verification services: Service List
  4. \n
  5. Create a verification: Create Verification
  6. \n
  7. Follow the location header returned to get the newly created verification.
  8. \n
\n

Our API documentation has an embedded javascript client so that you can try out the API. You will need to setup your bearer token to be able to access secured endpoints.\n


\n

Paginated List Responses

\n

When you successfully get a list of data (generally, ordered by when the resource was created), for example List Billing Cycles, you will need to follow it's links results.\n
Inside the links result, you will get current, previous, and next values. Inside these values, there will be href and method values.\n
With these values, you can get more of your list by navigating through them.

\n

Example

\n

We get the result from List Billing Cycles, but we don't see what we need and need more billing cycles.
\nThe following code is whiteboarding code, use the language you are using for your integration with the API
\nWe will need to do the following:

\n
  • Get our api result data = getListBillingCyclesResult()
  • \n
  • Get the href = data['links']['next']['href']
  • \n
  • Get the method = data['links']['next']['method']
  • \n
  • Now you can get the next page: nextPage = fetch(href, {method=method})
  • \n
  • If we need the next page's result, we can repeat this process.
\n

Note that only renewable rentals will be associated with a billing cycle.

\n\n\n", + "version": "v2", + "x-logo": { + "url": "../../tvlogo.svg", + "altText": "Textverified", + "href": "/app" + } + }, + "servers": [ + { + "url": "https://www.textverified.com" + } + ], + "paths": { + "/api/pub/v2/auth": { + "post": { + "tags": [ + "Bearer Tokens" + ], + "summary": "Generate Bearer Token", + "description": "Generate a bearer token to start making requests to secured endpoints. \r\nYou will need to supply the following headers:\r\n1) ```X-API-KEY``` (your primary API key)\r\n2) ```X-API-USERNAME``` (your username, which is the email you used to register) \r\nAll secured endpoints will need your bearer token supplied to them.
\r\nBecause bearer tokens can expire due to having a limited lifespan, you will need to obtain a new bearer token by generating a new one periodically.
\r\nYou can view and manage your API keys on under API Settings.
\r\nAfter successfully generating a bearer token ```eyJh...```, you can use set the bearer token here and explore our secured API endpoints.", + "parameters": [ + { + "name": "X-API-USERNAME", + "in": "header", + "description": "Your username.", + "schema": { + "type": "string" + }, + "example": "example@example.com" + }, + { + "name": "X-API-KEY", + "in": "header", + "description": "Your API key.", + "schema": { + "type": "string" + }, + "example": "APIKey12345" + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BearerToken" + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "429": { + "description": "Too many requests" + } + } + } + }, + "/api/pub/v2/account/me": { + "get": { + "tags": [ + "Account" + ], + "summary": "Get Account Details", + "description": "Get the details of your user account.", + "operationId": "GetAccountExpandedPublic", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Account" + } + } + } + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/area-codes": { + "get": { + "tags": [ + "Services" + ], + "summary": "Area Codes List", + "description": "Returns a list of our supported area codes.\r\n\r\nThese supported area codes you get in the ```areaCode``` property from the response can be used for API calls where an ```areaCodeSelectOption``` is in the body or parameter.", + "operationId": "ListAdvertisedAreaCodesPublic", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AreaCode" + } + } + } + } + }, + "400": { + "description": "Bad Request" + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/services": { + "get": { + "tags": [ + "Services" + ], + "summary": "Service List", + "description": "Returns a list of our supported services.\r\n\r\nThese supported services you get in the ```serviceName``` property from the response will be used for API calls where a ```serviceName``` is required in the body or parameter.", + "operationId": "ListAdvertisedTargetsPublic", + "parameters": [ + { + "name": "numberType", + "in": "query", + "description": "", + "schema": { + "$ref": "#/components/schemas/NumberType" + }, + "example": "mobile" + }, + { + "name": "reservationType", + "in": "query", + "description": "Required field", + "schema": { + "$ref": "#/components/schemas/ReservationType" + }, + "example": "renewable" + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Service" + } + } + } + } + }, + "400": { + "description": "Bad Request" + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/pricing/rentals": { + "post": { + "tags": [ + "Services" + ], + "summary": "Rental Pricing", + "description": "Get rental prices for a specific service and option(s). The ```serviceName``` is required, and the list of supported service names can be found at Service List.\r\n\r\nNot all service and options are compatible with each other. This endpoint point will return a ```400 Bad Request``` for unsupported service and option combinations.", + "operationId": "PriceCheckRentalPublic", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RentalPriceCheckRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/RentalPriceCheckRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/RentalPriceCheckRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PricingSnapshot" + } + } + } + }, + "400": { + "description": "Bad Request" + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/pricing/verifications": { + "post": { + "tags": [ + "Services" + ], + "summary": "Verification Pricing", + "description": "Clients can call this endpoint to check the verification price of a specific service and option(s). The list of supported services (```serviceName```) can be found here.
\r\nNot all service and options are compatible with each other. This endpoint point will return a ```400 Bad Request``` for unsupported service and option combinations.", + "operationId": "PriceCheckVerificationPublic", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerificationPriceCheckRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/VerificationPriceCheckRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/VerificationPriceCheckRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PricingSnapshot" + } + } + } + }, + "400": { + "description": "Bad Request" + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/sales/{id}": { + "get": { + "tags": [ + "Sales" + ], + "summary": "Get Reservation Sale Details", + "description": "Get the details on a reservation sale by ID.", + "operationId": "GetReservationSaleExpandedPublic", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of the sale. If the sale ID does not exist, a 404 response will be returned.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReservationSaleExpanded" + } + } + } + }, + "404": { + "description": "Not Found" + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/sales": { + "get": { + "tags": [ + "Sales" + ], + "summary": "List Reservation Sales", + "description": "Get a paginated list of your reservation sales.\r\n\r\nNote: This endpoint returns a paginated result of the list, check\r\nthe Paginated List Responses section of the overview page for more details\r\nif you are having problems.", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedData.ReservationSaleCompact" + } + } + } + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/billing-cycles/{id}": { + "get": { + "tags": [ + "Billing Cycles" + ], + "summary": "Get Billing Cycle Details", + "description": "Get the details of a billing cycle by ID. A billing cycle can have one or many renewable rentals. Only renewable rentals are associated with a billing cycle.", + "operationId": "GetBillingCyclePublic", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of the billing cycle. If the billing cycle ID does not exist, a 404 response will be returned.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BillingCycleExpanded" + } + } + } + }, + "404": { + "description": "Not Found" + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + }, + "post": { + "tags": [ + "Billing Cycles" + ], + "summary": "Updating a Billing Cycle", + "description": "Update a billing cycle by ID.\r\n\r\nWhat you can currently update are:\r\n- ```nickname``` (Your custom string to identify the billing cycle)\r\n- ```remindersEnabled``` (Email reminders)\r\n\r\nNot providing an update option will leave it unchanged. For example, not providing ```remindersEnabled``` but having \r\n```\"nickname\": \"aNickname\"``` will only update the nickname.", + "operationId": "UpdateBillingCyclePublic", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of the billing cycle.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BillingCycleUpdateRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/BillingCycleUpdateRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/BillingCycleUpdateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + }, + "400": { + "description": "Bad Request" + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/billing-cycles/{id}/invoices": { + "get": { + "tags": [ + "Billing Cycles" + ], + "summary": "Get Billing Cycle Invoices", + "description": "Get a paginated invoice history of a billing cycle by it's ID.\r\n\r\nNote: This endpoint returns a paginated result of the list, check\r\nthe Paginated List Responses section of the overview page for more details\r\nif you are having problems.", + "operationId": "ListBillingCycleInvoicesPublic", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of the billing cycle.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedData.BillingCycleRenewalInvoice" + } + } + } + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/billing-cycles": { + "get": { + "tags": [ + "Billing Cycles" + ], + "summary": "List Billing Cycles", + "description": "Returns a paginated list of your billing cycles. A billing cycle can have one or many renewable rentals. Only renewable rentals are associated with a billing cycle.\r\n\r\nNote: This endpoint returns a paginated result of the list, check\r\nthe Paginated List Responses section of the overview page for more details\r\nif you are having problems.", + "operationId": "ListBillingCyclesPublic", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedData.BillingCycleCompact" + } + } + } + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/billing-cycles/{id}/next-invoice": { + "post": { + "tags": [ + "Billing Cycles" + ], + "summary": "Preview Billing Cycle Invoice", + "description": "Get an invoice preview for your next billing cycle by it's ID.", + "operationId": "GenerateNextBillingCycleRenewableInvoicePublic", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of the billing cycle.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BillingCycleRenewalInvoicePreview" + } + } + } + }, + "400": { + "description": "Bad Request" + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/billing-cycles/{id}/renew": { + "post": { + "tags": [ + "Billing Cycles" + ], + "summary": "Renew Billing Cycle", + "description": "Renew the active rentals on your billing cycle by it's ID.\r\n \r\nThis will not renew overdue rentals.", + "operationId": "RenewBillingCyclePublic", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of the billing cycle.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BillingCycleRenewalInvoice" + } + } + } + }, + "400": { + "description": "Bad Request" + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/backorders/{id}": { + "get": { + "tags": [ + "Back Order Rental Reservations" + ], + "summary": "Get Back Order Reservation Detail", + "description": "Get the details on a back order reservation by ID.\r\n\r\nBack order reservations are created if you have a parameter set when creating a new rental and there is no available stock. See Create New Rental for more info.", + "operationId": "GetBackOrderReservationExpandedPublic", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of the back order reservation. If the reservation ID does not exist, a 404 response will be returned.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BackOrderReservationExpanded" + } + } + } + }, + "404": { + "description": "Not Found" + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/reservations/{id}": { + "get": { + "tags": [ + "Line Reservations" + ], + "summary": "Line Reservation Details", + "description": "Get the details on a line reservation by ID.\r\n\r\nSuccessfully getting the details of the line reservation will require following the ```href``` link provided in the response.", + "operationId": "GetLineReservationExpandedPublic", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of the line reservation. If the reservation ID does not exist, a 404 response will be returned.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "type", + "in": "query", + "description": "Type of line reservation. If the type supplied does not match the reservation, a 404 response will be returned.", + "schema": { + "$ref": "#/components/schemas/ReservationType" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Link" + } + } + } + }, + "404": { + "description": "Not Found" + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/reservations/{id}/health": { + "get": { + "tags": [ + "Line Reservations" + ], + "summary": "Reservation Health Check", + "operationId": "GetReservationLineHealthPublic", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of the line reservation. If the reservation ID does not exist or if the reservation is not 'active', a 404 response will be returned.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LineHealth" + } + } + } + }, + "404": { + "description": "Not Found" + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/verifications": { + "post": { + "tags": [ + "New Verification" + ], + "summary": "Create Verification", + "description": "Create/purchase a new verification. If successful, a 201 response will be returned with the location of the newly created verification in the http response 'location' header.

\r\nThe ```serviceName``` is required, and the list of supported service names can be found at Service List.
", + "requestBody": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewVerificationRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/NewVerificationRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/NewVerificationRequest" + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/Link" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Link" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Link" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + }, + "get": { + "tags": [ + "Verifications" + ], + "summary": "List Verifications", + "description": "Clients can call this endpoint to retrieve the paginated list of verifications.", + "operationId": "ListVerificationsPublic", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedData.VerificationCompact" + } + } + } + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/verifications/{id}": { + "get": { + "tags": [ + "Verifications" + ], + "summary": "Verification Details", + "description": "Clients can call this endpoint to retrieve the details of a verification.", + "operationId": "GetVerificationExpandedPublic", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Id of the verification.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerificationExpanded" + } + } + } + }, + "404": { + "description": "Not Found" + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/verifications/{id}/cancel": { + "post": { + "tags": [ + "Verifications" + ], + "summary": "Cancel Verification", + "description": "Clients can call this endpoint to try to cancel a verification.", + "operationId": "CancelVerificationPublic", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + }, + "400": { + "description": "Bad Request" + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/verifications/{id}/reactivate": { + "post": { + "tags": [ + "Verifications" + ], + "summary": "Reactivate Verification", + "description": "Clients can call this endpoint to try to reactivate a verification. If successful, a 201 response will be returned with the location of the newly created verification in the http response 'location' header.", + "operationId": "ReactivateVerificationPublic", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "Created", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/Link" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Link" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Link" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/verifications/{id}/report": { + "post": { + "tags": [ + "Verifications" + ], + "summary": "Report Verification", + "description": "Clients can call this endpoint to try to report an verification.", + "operationId": "ReportVerificationPublic", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + }, + "400": { + "description": "Bad Request" + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/verifications/{id}/reuse": { + "post": { + "tags": [ + "Verifications" + ], + "summary": "Reuse Verification", + "description": "Clients can call this endpoint to try to reuse a verification. If successful, a 201 response will be returned with the location of the newly created verification in the http response 'location' header.", + "operationId": "ReuseVerificationPublic", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "Created", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/Link" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Link" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Link" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/reservations/rental": { + "post": { + "tags": [ + "New Rental" + ], + "summary": "Create New Rental", + "description": "Create/purchase a new rental. If successful, a 201 response will be returned with the location of the sale in the http response 'location' header. This sale will have details on the outcome of the rental purchase.

\r\nThe ```serviceName``` is required, and the list of supported service names can be found at Service List.
\r\nIf ```allowBackOrderReservations``` is ```true``` in the request body, a rental back order will be created if the requested rental is out of stock.", + "requestBody": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewRentalRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/NewRentalRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/NewRentalRequest" + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/Link" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Link" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Link" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/reservations/rental/renewable/{id}": { + "get": { + "tags": [ + "Renewable Rentals" + ], + "summary": "Renewable Rental Details", + "description": "Get the details of a renewable rental by ID.", + "operationId": "GetRenewableRentalPublic", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of the line reservation. If the reservation ID does not exist, a 404 response will be returned.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RenewableRentalExpanded" + } + } + } + }, + "404": { + "description": "Not Found" + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + }, + "post": { + "tags": [ + "Renewable Rentals" + ], + "summary": "Update a Renewable Rental", + "description": "Update a renewable rental by ID.\r\n\r\nWhat you can currently update are:\r\n- ```userNotes``` (Notes)\r\n- ```includeForRenewal``` (Include/Exclude rental for renew)\r\n- ```markAllSmsRead``` (Mark SMS as read)\r\n\r\nNot providing an update option will leave it unchanged. For example, not providing\r\n```userNotes``` but having ```\"includeForRenewal\": true``` will only include the rental for renew.", + "operationId": "UpdateRenewableRentalPublic", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of the rental.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RenewableRentalUpdateRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/RenewableRentalUpdateRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/RenewableRentalUpdateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + }, + "400": { + "description": "Bad Request" + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/reservations/rental/renewable": { + "get": { + "tags": [ + "Renewable Rentals" + ], + "summary": "List Renewable Rentals", + "description": "Returns a paginated list of your renewable rentals.\r\n\r\nOptional Parameters:\r\n- ```billingCycleId``` (Get renewable rentals under this billing cycle ID)\r\n\r\nTo create a new rental, see Create New Rental.\r\n\r\nNote: This endpoint returns a paginated result of the list, check\r\nthe Paginated List Responses section of the overview page for more details\r\nif you are having problems.", + "operationId": "ListRenewableLineRentalsPublic", + "parameters": [ + { + "name": "billingCycleId", + "in": "query", + "description": "ID of the billing cycle.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedData.RenewableRentalCompact" + } + } + } + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/reservations/rental/renewable/{id}/refund": { + "post": { + "tags": [ + "Renewable Rentals" + ], + "summary": "Refund Renewable Rental", + "description": "Attempt to refund a renewable rental by ID.", + "operationId": "RenewableSelfServiceRefundRentalPublic", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of the line reservation.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + }, + "400": { + "description": "Bad Request" + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/reservations/rental/renewable/{id}/renew": { + "post": { + "tags": [ + "Renewable Rentals" + ], + "summary": "Renew Overdue Rental", + "description": "Renew an overdue rental by it's ID. \r\n\r\nCalling this method will automatically include the rental for renewal and perform the renewal. Only overdue rentals may be renewed this way.", + "operationId": "RenewOverdueRenewableRental", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of the renewable rental.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + }, + "400": { + "description": "Bad Request" + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/reservations/rental/nonrenewable/{id}": { + "get": { + "tags": [ + "Non-Renewable Rentals" + ], + "summary": "Non-Renewable Rental Details", + "description": "Get the details of a non-renewable rental by ID.", + "operationId": "GetNonrenewableRentalPublic", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of the line reservation. If the reservation ID does not exist, a 404 response will be returned.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NonrenewableRentalExpanded" + } + } + } + }, + "404": { + "description": "Not Found" + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + }, + "post": { + "tags": [ + "Non-Renewable Rentals" + ], + "summary": "Update a Non-Renewable Rental", + "description": "Update a non-renewable rental by ID.\r\n\r\nWhat you can currently update are:\r\n- ```userNotes``` (Notes)\r\n- ```markAllSmsRead``` (Mark SMS as read)", + "operationId": "UpdateNonrenewableRentalPublic", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of the line reservation.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NonrenewableRentalUpdateRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/NonrenewableRentalUpdateRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/NonrenewableRentalUpdateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + }, + "400": { + "description": "Bad Request" + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/reservations/rental/nonrenewable": { + "get": { + "tags": [ + "Non-Renewable Rentals" + ], + "summary": "List Non-Renewable Rentals", + "description": "Returns a paginated list of your non-renewable rentals.\r\n\r\nTo create a new rental, see Create New Rental.\r\n\r\nNote: This endpoint returns a paginated result of the list, check\r\nthe Paginated List Responses section of the overview page for more details\r\nif you are having problems.", + "operationId": "ListNonrenewableRentalsPublic", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedData.NonrenewableRentalCompact" + } + } + } + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/reservations/rentals/extensions": { + "post": { + "tags": [ + "Non-Renewable Rentals" + ], + "summary": "Extend a Non-Renewable rental", + "description": "\r\n

Extensions are NOT refundable and cannot be undone.

\r\n
\r\n

\r\n Attempt to extend the duration of a non-renewable rental by ID.

", + "operationId": "ExtendNonrenewableRentalPublic", + "requestBody": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RentalExtensionRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/RentalExtensionRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/RentalExtensionRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + }, + "400": { + "description": "Bad Request" + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/reservations/rental/nonrenewable/{id}/refund": { + "post": { + "tags": [ + "Non-Renewable Rentals" + ], + "summary": "Refund Non-Renewable Rental", + "description": "Attempt to refund a non-renewable rental by ID.", + "operationId": "NonrenewableSelfServiceRefundRentalPublic", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of the line reservation.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + }, + "400": { + "description": "Bad Request" + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/sms": { + "get": { + "tags": [ + "Sms" + ], + "summary": "List Sms", + "description": "Returns a paginated list of your latest sms.\r\n\r\nOptional Parameters:\r\n- ```to``` (A phone number)\r\n- ```reservationId``` (The reservation's ID)\r\n- ```reservationType``` (The reservation's type)\r\n\r\nNote: This endpoint returns a paginated result of the list, check\r\nthe Paginated List Responses section of the overview page for more details\r\nif you are having problems.", + "operationId": "ListSmsPublic", + "parameters": [ + { + "name": "to", + "in": "query", + "description": "If provided, will filter results to only include sms sent to the specified number.", + "schema": { + "type": "string" + } + }, + { + "name": "reservationId", + "in": "query", + "description": "If provided, will filter results to only include sms associated with the specified reservation.", + "schema": { + "type": "string" + } + }, + { + "name": "reservationType", + "in": "query", + "description": "If provided, will filter results to only include sms associated with the specified reservation types.", + "schema": { + "$ref": "#/components/schemas/ReservationType" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedData.Sms" + } + } + } + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/wake-requests/{id}": { + "get": { + "tags": [ + "Wake Requests" + ], + "summary": "Get Wake Request", + "description": "Get a wake request by ID.\r\n\r\nThe details of a wake request include:\r\n- ```isScheduled``` (Indicates if the wake request has been successfully created and enqueued.)\r\n- ```usageWindowStart``` (The datetime when you can start using the reservation.)\r\n- ```usageWindowEnd``` (The datetime when you will no longer be able to use the reservation.)\r\nRefer to the ```SCHEMA``` below for more details.\r\n\r\nTo create a wake request, see Create Wake Request.", + "operationId": "GetWakeRequestPublic", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of the wake request. If the wake request ID does not exist, a 404 response will be returned.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WakeResponse" + } + } + } + }, + "404": { + "description": "Not Found" + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/wake-requests": { + "post": { + "tags": [ + "Wake Requests" + ], + "summary": "Create Wake Request", + "description": "Create a wake request for a reservation. If successful, a 201 response will be returned with the location of the newly created wake request in the http response 'location' header.

\r\nSome reservation types will require a wake request before the line reservation can be used.
\r\nCreating a successful wake request will enqueue your request on that line reservation.\r\n\r\nTo see the details of your wake request, see Get Wake Request.", + "operationId": "CreateWakeRequestPublic", + "requestBody": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WakeRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/WakeRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/WakeRequest" + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/Link" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Link" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Link" + } + } + } + }, + "400": { + "description": "Bad Request" + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/wake-requests/estimate": { + "post": { + "tags": [ + "Wake Requests" + ], + "summary": "Estimate Usage Window", + "description": "Estimate the usage window for a reservation if you were to create a wake request now.\r\n\r\nDoes NOT create a wake request. \r\nTo create a wake request, see Create Wake Request.\r\n \r\nThe details of an usage window estimate include:\r\n- ```estimatedWindowStart``` (The estimated datetime when you can start using the reservation.)\r\n- ```estimatedWindowEnd``` (The estimated datetime when you will no longer be able to use the reservation.)\r\nRefer to the ```SCHEMA``` below for more details.", + "operationId": "EstimateUsageWindow", + "requestBody": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UsageWindowEstimateRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/UsageWindowEstimateRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/UsageWindowEstimateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UsageWindowEstimateResponse" + } + } + } + }, + "400": { + "description": "Bad Request" + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/webhook-events": { + "get": { + "tags": [ + "Webhooks" + ], + "summary": "List Webhook Events", + "description": "Returns a list of webhook events that can be subscribed to. You will need to configure your API Webhook Settings and subscribe to events first.\r\n

\r\nThe schema of the webhook events are documented below. We serialize webhook event data using Json to the `Data` property. All of our webhooks are sent as a HTTP POST to the webhook uri configured on your account.\r\nWe sign all of our webhooks with HMAC and SHA512 which should be validated by your server.\r\n

Webhook validation:
\r\nWe sign all webhook requests using HMAC-SHA512 and your webhook secret (`whsec_...`).\r\nThe signature HTTP Header is `X-Webhook-Signature` and the HTTP Value will be\r\nprefixed by the signing algorithm `HMAC-SHA512='.\r\nThe signature after the prefix is BASE-64 encoded.\r\nThe process for validating a signature is as follows:\r\n1) Extract the HTTP Header with the name `X-Webhook-Signature`.\r\n2) Remove the prefix `HMAC-SHA512=` from the HTTP Header value, which is obtained from step 1.\r\n - This BASE-64 encoded value will be compared to the HTTP Request body hash later.\r\n3) Apply the HMAC-SHA512 hashing algorithm to the HTTP Request body to obtain a byte array.\r\n4) Apply BASE-64 encoding to the byte array (obtained from step 3).\r\n5) Compare the value from step 2 to the value from step 4 and verify that they are the same.\r\n \r\nWebhooks are automatically retried for a period of time with exponential back off. \r\nAfter multiple failed webhook send attempts, webhooks will be automatically disabled.\r\nIf this happens, you will have to manually re-enable your webhooks again.", + "operationId": "GetWebhookEventDefinitionsPublic", + "responses": { + "200": { + "description": "Success" + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "callbacks": { + "v2.rental.billingcycle.renewed": { + "https://www.example.com/": { + "post": { + "requestBody": { + "description": "Triggers when a billing cycle on your account has renewed. Renewal of individual reservations on a billing cycle can occur independently.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookEvent.BillingCycleWebhookEvent" + } + } + } + }, + "responses": { + "200": { + "description": "Your server implementation should return this HTTP status code if the data was received successfully." + }, + "4XX": { + "description": "If your server returns an HTTP status code indicating it does not understand the format of the payload, then the delivery attempt will be treated as a failure." + }, + "5XX": { + "description": "If your server returns an HTTP status code indicating a server-side error, then the delivery attempt will be treated as a failure." + } + } + } + } + }, + "v2.rental.billingcycle.expired": { + "https://www.example.com/": { + "post": { + "requestBody": { + "description": "Triggers when a billing cycle on your account has expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookEvent.BillingCycleWebhookEvent" + } + } + } + }, + "responses": { + "200": { + "description": "Your server implementation should return this HTTP status code if the data was received successfully." + }, + "4XX": { + "description": "If your server returns an HTTP status code indicating it does not understand the format of the payload, then the delivery attempt will be treated as a failure." + }, + "5XX": { + "description": "If your server returns an HTTP status code indicating a server-side error, then the delivery attempt will be treated as a failure." + } + } + } + } + }, + "v2.sms.received": { + "https://www.example.com/": { + "post": { + "requestBody": { + "description": "Triggers when an sms is received and assigned to your account.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookEvent.SmsWebhookEvent" + } + } + } + }, + "responses": { + "200": { + "description": "Your server implementation should return this HTTP status code if the data was received successfully." + }, + "4XX": { + "description": "If your server returns an HTTP status code indicating it does not understand the format of the payload, then the delivery attempt will be treated as a failure." + }, + "5XX": { + "description": "If your server returns an HTTP status code indicating a server-side error, then the delivery attempt will be treated as a failure." + } + } + } + } + }, + "v2.rental.backorder.fulfilled": { + "https://www.example.com/": { + "post": { + "requestBody": { + "description": "Triggers when a back order reservation is fulfilled.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookEvent.BackOrderReservationWebhookEvent" + } + } + } + }, + "responses": { + "200": { + "description": "Your server implementation should return this HTTP status code if the data was received successfully." + }, + "4XX": { + "description": "If your server returns an HTTP status code indicating it does not understand the format of the payload, then the delivery attempt will be treated as a failure." + }, + "5XX": { + "description": "If your server returns an HTTP status code indicating a server-side error, then the delivery attempt will be treated as a failure." + } + } + } + } + }, + "v2.reservation.created": { + "https://www.example.com/": { + "post": { + "requestBody": { + "description": "Triggers when a reservation is created.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookEvent.ReservationCreatedWebhookEvent" + } + } + } + }, + "responses": { + "200": { + "description": "Your server implementation should return this HTTP status code if the data was received successfully." + }, + "4XX": { + "description": "If your server returns an HTTP status code indicating it does not understand the format of the payload, then the delivery attempt will be treated as a failure." + }, + "5XX": { + "description": "If your server returns an HTTP status code indicating a server-side error, then the delivery attempt will be treated as a failure." + } + } + } + } + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/legacy/reservation-id-lookup": { + "get": { + "tags": [ + "Legacy" + ], + "summary": "Legacy Reservation Id Look Up", + "description": "You can use this legacy endpoint for looking up the 'new' ID of a rental reservation (begins with the prefix 'lr_') by supplying a legacy ID (GUID/UUID format). This may be helpful when migration from our deprecated endpoints to our current endpoints.\r\nThis endpoint will be removed in the future without notice.", + "operationId": "GetNewReservationId", + "parameters": [ + { + "name": "oldReservationId", + "in": "query", + "description": "The legacy reservation Id to look up which is formatted as a GUID/UUID. If the legacy ID supplied is invalid or not found, a 404 Not Found response will be returned.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "404": { + "description": "Not Found" + }, + "200": { + "description": "New ID of a reservation (begins with the prefix 'lr_')", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "deprecated": true, + "security": [ + { + "Bearer": [ ] + } + ] + } + } + }, + "components": { + "schemas": { + "Account": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "currentBalance": { + "type": "number", + "format": "double" + } + }, + "additionalProperties": false + }, + "AddOnSnapshot": { + "type": "object", + "properties": { + "addOnId": { + "type": "string" + }, + "description": { + "type": "string" + }, + "renewalCost": { + "type": "number", + "format": "double" + }, + "alreadyRenewed": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "AreaCode": { + "type": "object", + "properties": { + "areaCode": { + "type": "string", + "description": "Area code. Optionally supply this value when an ```areaCodeSelectOption``` is in the request body or parameter.", + "example": "205" + }, + "state": { + "type": "string", + "description": "The US state associated with the area code.", + "example": "Alabama" + } + }, + "additionalProperties": false + }, + "BackOrderReservationCompact": { + "type": "object", + "properties": { + "link": { + "$ref": "#/components/schemas/Link" + }, + "id": { + "type": "string" + }, + "serviceName": { + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/BackOrderState" + } + }, + "additionalProperties": false + }, + "BackOrderReservationExpanded": { + "type": "object", + "properties": { + "link": { + "$ref": "#/components/schemas/Link" + }, + "id": { + "type": "string" + }, + "serviceName": { + "type": "string" + }, + "saleId": { + "type": "string" + }, + "sale": { + "$ref": "#/components/schemas/Link" + }, + "reservation": { + "$ref": "#/components/schemas/Link" + }, + "reservationId": { + "type": "string", + "description": "Will be null if a reservation has not been assigned yet.", + "nullable": true + }, + "status": { + "$ref": "#/components/schemas/BackOrderState" + } + }, + "additionalProperties": false + }, + "BackOrderReservationWebhookEvent": { + "type": "object", + "properties": { + "backOrderId": { + "type": "string", + "description": "Id of the back order reservation." + } + }, + "additionalProperties": false + }, + "BackOrderState": { + "enum": [ + "created", + "fulfilled", + "canceled" + ], + "type": "string" + }, + "BearerToken": { + "type": "object", + "properties": { + "token": { + "type": "string", + "description": "Bearer token", + "example": "eyJh..." + }, + "expiresIn": { + "type": "number", + "description": "Seconds remaining until bearer token expires", + "format": "double", + "example": 899 + }, + "expiresAt": { + "type": "string", + "description": "Timestamp of when the token will expire", + "format": "date-time", + "example": "2020-06-15T00:41:35+00:00" + } + }, + "additionalProperties": false + }, + "BillingCycleCompact": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Id of the billing cycle" + }, + "billingCycleEndsAt": { + "type": "string", + "format": "date-time" + }, + "emailNotificationsEnabled": { + "type": "boolean" + }, + "state": { + "type": "string" + } + }, + "additionalProperties": false + }, + "BillingCycleExpanded": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Id of the billing cycle" + }, + "renewedThrough": { + "type": "string", + "format": "date-time" + }, + "billingCycleEndsAt": { + "type": "string", + "format": "date-time" + }, + "nextAutoRenewAttempt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "emailNotificationsEnabled": { + "type": "boolean" + }, + "state": { + "type": "string" + }, + "rentals": { + "$ref": "#/components/schemas/Link" + }, + "invoices": { + "$ref": "#/components/schemas/Link" + } + }, + "additionalProperties": false + }, + "BillingCycleQueryFilter": { + "type": "object", + "additionalProperties": false + }, + "BillingCycleRenewalInvoice": { + "type": "object", + "properties": { + "createdAt": { + "type": "string", + "format": "date-time" + }, + "id": { + "type": "string" + }, + "excludedRentals": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RentalSnapshot" + } + }, + "includedRentals": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RentalSnapshot" + } + }, + "isPaidFor": { + "type": "boolean" + }, + "totalCost": { + "type": "number", + "format": "double" + } + }, + "additionalProperties": false + }, + "BillingCycleRenewalInvoicePreview": { + "type": "object", + "properties": { + "billingCycleId": { + "type": "string" + }, + "renewalEstimate": { + "$ref": "#/components/schemas/BillingCycleRenewalInvoice" + } + }, + "additionalProperties": false + }, + "BillingCycleUpdateRequest": { + "type": "object", + "properties": { + "remindersEnabled": { + "type": "boolean", + "nullable": true + }, + "nickname": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Supplying a value of 'null' or not supplying a value for any nullable properties will cause the property to be ignored." + }, + "BillingCycleWebhookEvent": { + "type": "object", + "properties": { + "billingCycleId": { + "type": "string", + "description": "Id of the billing cycle.", + "nullable": true + } + }, + "additionalProperties": false + }, + "CancelAction": { + "type": "object", + "properties": { + "canCancel": { + "type": "boolean" + }, + "link": { + "$ref": "#/components/schemas/Link" + } + }, + "additionalProperties": false + }, + "Error": { + "type": "object", + "properties": { + "errorCode": { + "type": "string", + "nullable": true + }, + "errorDescription": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "KeysetPaginationDirectionality": { + "enum": [ + "forward", + "reverse" + ], + "type": "string" + }, + "LineHealth": { + "type": "object", + "properties": { + "lineNumber": { + "type": "string", + "description": "Line number associated with the reservation." + }, + "checkedAt": { + "type": "string", + "description": "Timestamp of when this line was last checked at.", + "format": "date-time", + "nullable": true + } + }, + "additionalProperties": false + }, + "LineReservationType": { + "enum": [ + "verification", + "rental" + ], + "type": "string" + }, + "Link": { + "type": "object", + "properties": { + "method": { + "type": "string", + "description": "The HTTP method for this link.", + "nullable": true + }, + "href": { + "type": "string", + "description": "The HTTP location of the resource.", + "nullable": true + } + }, + "additionalProperties": false + }, + "NewRentalRequest": { + "required": [ + "allowBackOrderReservations", + "alwaysOn", + "capability", + "duration", + "isRenewable", + "numberType", + "serviceName" + ], + "type": "object", + "properties": { + "allowBackOrderReservations": { + "type": "boolean", + "description": "If set to true, a rental back order will be created if the requested rental is out of stock" + }, + "alwaysOn": { + "type": "boolean", + "description": "If set to true, a line that does not require wake up will be assigned if in stock" + }, + "areaCodeSelectOption": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional array of area codes", + "nullable": true, + "example": [ + "222", + "333" + ] + }, + "duration": { + "$ref": "#/components/schemas/RentalDuration" + }, + "isRenewable": { + "type": "boolean" + }, + "numberType": { + "$ref": "#/components/schemas/NumberType" + }, + "billingCycleIdToAssignTo": { + "type": "string", + "description": "Optional billing cycle to which the rental is assigned. If left empty, a new billing cycle will be created for the rental. Only renewable rentals can be assigned to a billing cycle.", + "nullable": true + }, + "serviceName": { + "type": "string", + "description": "Name of the service", + "example": "myservice" + }, + "capability": { + "$ref": "#/components/schemas/ReservationCapability" + } + }, + "additionalProperties": false + }, + "NewVerificationRequest": { + "required": [ + "capability", + "serviceName" + ], + "type": "object", + "properties": { + "areaCodeSelectOption": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Clients can specify a area code when creating a reservation. If provided, our service will try to reserve a number with the specified area code.", + "nullable": true, + "example": [ + "775", + "301" + ] + }, + "carrierSelectOption": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Clients can specify a carrier when creating a reservation. If provided, our service will try to reserve a number with the specified Carrier.", + "nullable": true, + "example": [ + "vz", + "tmo", + "att" + ] + }, + "serviceName": { + "type": "string", + "description": "", + "example": "abra" + }, + "capability": { + "$ref": "#/components/schemas/ReservationCapability" + }, + "serviceNotListedName": { + "type": "string", + "description": "Optional string to specify the service you are using with the \"servicenotlisted\" ```ServiceName```", + "nullable": true + }, + "maxPrice": { + "type": "number", + "description": "Optional decimal to specify the maximum total price you are willing to pay for the requested reservation. If the price exceeds this amount, the request will be rejected and no reservation will be created.", + "format": "double", + "nullable": true + } + }, + "additionalProperties": false + }, + "NonrenewableRentalCompact": { + "type": "object", + "properties": { + "createdAt": { + "type": "string", + "format": "date-time" + }, + "id": { + "type": "string" + }, + "link": { + "$ref": "#/components/schemas/Link" + }, + "sale": { + "$ref": "#/components/schemas/Link" + }, + "saleId": { + "type": "string", + "description": "Sale Id may not be available for older sales.", + "nullable": true + }, + "serviceName": { + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/ReservationState" + }, + "number": { + "type": "string" + }, + "alwaysOn": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "NonrenewableRentalExpanded": { + "type": "object", + "properties": { + "calls": { + "$ref": "#/components/schemas/Link" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "endsAt": { + "type": "string", + "format": "date-time" + }, + "id": { + "type": "string" + }, + "refund": { + "$ref": "#/components/schemas/RefundAction" + }, + "sale": { + "$ref": "#/components/schemas/Link" + }, + "saleId": { + "type": "string", + "description": "Sale Id may not be available for older sales.", + "nullable": true + }, + "serviceName": { + "type": "string" + }, + "sms": { + "$ref": "#/components/schemas/Link" + }, + "state": { + "$ref": "#/components/schemas/ReservationState" + }, + "number": { + "type": "string" + }, + "alwaysOn": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "NonrenewableRentalQueryFilter": { + "type": "object", + "additionalProperties": false + }, + "NonrenewableRentalUpdateRequest": { + "type": "object", + "properties": { + "userNotes": { + "type": "string", + "description": "Update a non-renewable rental's notes. Supply an empty string value to clear the notes.", + "nullable": true, + "example": "Customer note." + }, + "markAllSmsRead": { + "type": "boolean", + "description": "Mark a non-renewable rental's SMS as read.", + "nullable": true, + "example": true + } + }, + "additionalProperties": false, + "description": "Supplying a value of 'null' or not supplying a value for any nullable properties will cause the property to be ignored." + }, + "NumberType": { + "enum": [ + "mobile", + "voip", + "landline" + ], + "type": "string" + }, + "PaginatedData.BillingCycleCompact": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BillingCycleCompact" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrevious": { + "type": "boolean" + }, + "count": { + "type": "integer", + "format": "int64" + }, + "links": { + "$ref": "#/components/schemas/PaginationLinks" + } + }, + "additionalProperties": false + }, + "PaginatedData.BillingCycleRenewalInvoice": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BillingCycleRenewalInvoice" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrevious": { + "type": "boolean" + }, + "count": { + "type": "integer", + "format": "int64" + }, + "links": { + "$ref": "#/components/schemas/PaginationLinks" + } + }, + "additionalProperties": false + }, + "PaginatedData.NonrenewableRentalCompact": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NonrenewableRentalCompact" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrevious": { + "type": "boolean" + }, + "count": { + "type": "integer", + "format": "int64" + }, + "links": { + "$ref": "#/components/schemas/PaginationLinks" + } + }, + "additionalProperties": false + }, + "PaginatedData.RenewableRentalCompact": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RenewableRentalCompact" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrevious": { + "type": "boolean" + }, + "count": { + "type": "integer", + "format": "int64" + }, + "links": { + "$ref": "#/components/schemas/PaginationLinks" + } + }, + "additionalProperties": false + }, + "PaginatedData.ReservationSaleCompact": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ReservationSaleCompact" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrevious": { + "type": "boolean" + }, + "count": { + "type": "integer", + "format": "int64" + }, + "links": { + "$ref": "#/components/schemas/PaginationLinks" + } + }, + "additionalProperties": false + }, + "PaginatedData.Sms": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Sms" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrevious": { + "type": "boolean" + }, + "count": { + "type": "integer", + "format": "int64" + }, + "links": { + "$ref": "#/components/schemas/PaginationLinks" + } + }, + "additionalProperties": false + }, + "PaginatedData.VerificationCompact": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VerificationCompact" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrevious": { + "type": "boolean" + }, + "count": { + "type": "integer", + "format": "int64" + }, + "links": { + "$ref": "#/components/schemas/PaginationLinks" + } + }, + "additionalProperties": false + }, + "PaginationLinks": { + "type": "object", + "properties": { + "current": { + "$ref": "#/components/schemas/Link" + }, + "previous": { + "$ref": "#/components/schemas/Link" + }, + "next": { + "$ref": "#/components/schemas/Link" + } + }, + "additionalProperties": false + }, + "PricingSnapshot": { + "type": "object", + "properties": { + "serviceName": { + "type": "string", + "description": "Name of the service.", + "example": "myservice" + }, + "price": { + "type": "number", + "description": "Total cost.", + "format": "double", + "example": 5 + } + }, + "additionalProperties": false + }, + "ReactivationAction": { + "type": "object", + "properties": { + "canReactivate": { + "type": "boolean" + }, + "link": { + "$ref": "#/components/schemas/Link" + } + }, + "additionalProperties": false + }, + "RefundAction": { + "type": "object", + "properties": { + "canRefund": { + "type": "boolean" + }, + "link": { + "$ref": "#/components/schemas/Link" + }, + "refundableUntil": { + "type": "string", + "format": "date-time", + "nullable": true + } + }, + "additionalProperties": false + }, + "RenewableRentalCompact": { + "type": "object", + "properties": { + "createdAt": { + "type": "string", + "format": "date-time" + }, + "id": { + "type": "string" + }, + "sale": { + "$ref": "#/components/schemas/Link" + }, + "saleId": { + "type": "string", + "description": "Sale Id may not be available for older sales.", + "nullable": true + }, + "serviceName": { + "type": "string" + }, + "link": { + "$ref": "#/components/schemas/Link" + }, + "state": { + "$ref": "#/components/schemas/ReservationState" + }, + "billingCycle": { + "$ref": "#/components/schemas/Link" + }, + "billingCycleId": { + "type": "string" + }, + "isIncludedForNextRenewal": { + "type": "boolean" + }, + "number": { + "type": "string" + }, + "alwaysOn": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "RenewableRentalExpanded": { + "type": "object", + "properties": { + "sms": { + "$ref": "#/components/schemas/Link" + }, + "calls": { + "$ref": "#/components/schemas/Link" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "id": { + "type": "string" + }, + "refund": { + "$ref": "#/components/schemas/RefundAction" + }, + "sale": { + "$ref": "#/components/schemas/Link" + }, + "saleId": { + "type": "string", + "description": "Sale Id may not be available for older sales.", + "nullable": true + }, + "serviceName": { + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/ReservationState" + }, + "billingCycle": { + "$ref": "#/components/schemas/Link" + }, + "billingCycleId": { + "type": "string" + }, + "isIncludedForNextRenewal": { + "type": "boolean" + }, + "number": { + "type": "string" + }, + "alwaysOn": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "RenewableRentalUpdateRequest": { + "type": "object", + "properties": { + "userNotes": { + "type": "string", + "description": "Update a renewable rental's notes. Supply an empty string value to clear the notes.", + "nullable": true, + "example": "Customer note." + }, + "includeForRenewal": { + "type": "boolean", + "description": "Update a renewable rental's inclusion/exclusion for renewal.", + "nullable": true, + "example": true + }, + "markAllSmsRead": { + "type": "boolean", + "description": "Mark a renewable rental's SMS as read.", + "nullable": true, + "example": true + } + }, + "additionalProperties": false, + "description": "Supplying a value of 'null' or not supplying a value for any nullable properties will cause the property to be ignored." + }, + "RentalDuration": { + "enum": [ + "oneDay", + "threeDay", + "sevenDay", + "fourteenDay", + "thirtyDay", + "ninetyDay", + "oneYear" + ], + "type": "string" + }, + "RentalExtensionRequest": { + "type": "object", + "properties": { + "extensionDuration": { + "$ref": "#/components/schemas/RentalDuration" + }, + "rentalId": { + "type": "string" + } + }, + "additionalProperties": false + }, + "RentalPriceCheckRequest": { + "required": [ + "alwaysOn", + "areaCode", + "capability", + "duration", + "isRenewable", + "numberType", + "serviceName" + ], + "type": "object", + "properties": { + "serviceName": { + "type": "string", + "description": "Name of the service", + "example": "myservice" + }, + "areaCode": { + "type": "boolean", + "description": "", + "example": true + }, + "numberType": { + "$ref": "#/components/schemas/NumberType" + }, + "capability": { + "$ref": "#/components/schemas/ReservationCapability" + }, + "alwaysOn": { + "type": "boolean", + "description": "", + "example": true + }, + "callForwarding": { + "type": "boolean", + "description": "", + "nullable": true, + "example": true + }, + "billingCycleIdToAssignTo": { + "type": "string", + "description": "", + "nullable": true, + "example": "abcedfg" + }, + "isRenewable": { + "type": "boolean", + "description": "", + "example": true + }, + "duration": { + "$ref": "#/components/schemas/RentalDuration" + } + }, + "additionalProperties": false + }, + "RentalSnapshot": { + "type": "object", + "properties": { + "number": { + "type": "string" + }, + "rental": { + "$ref": "#/components/schemas/Link" + }, + "renewalCost": { + "type": "number", + "format": "double" + }, + "serviceName": { + "type": "string" + }, + "alreadyRenewed": { + "type": "boolean" + }, + "includedAddOns": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AddOnSnapshot" + }, + "deprecated": true + }, + "excludedAddOns": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AddOnSnapshot" + }, + "deprecated": true + } + }, + "additionalProperties": false + }, + "ReportAction": { + "type": "object", + "properties": { + "canReport": { + "type": "boolean" + }, + "link": { + "$ref": "#/components/schemas/Link" + } + }, + "additionalProperties": false + }, + "Reservation": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Id of the reservation" + }, + "link": { + "$ref": "#/components/schemas/Link" + }, + "reservationType": { + "$ref": "#/components/schemas/ReservationType" + }, + "serviceName": { + "type": "string", + "description": "Name of service" + } + }, + "additionalProperties": false + }, + "ReservationCapability": { + "enum": [ + "sms", + "voice", + "smsAndVoiceCombo" + ], + "type": "string" + }, + "ReservationCreatedWebhookEvent": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Id of the created reservation." + }, + "type": { + "$ref": "#/components/schemas/LineReservationType" + } + }, + "additionalProperties": false + }, + "ReservationSaleCompact": { + "type": "object", + "properties": { + "createdAt": { + "type": "string", + "format": "date-time" + }, + "id": { + "type": "string" + }, + "link": { + "$ref": "#/components/schemas/Link" + }, + "state": { + "$ref": "#/components/schemas/ReservationSaleState" + }, + "totalCost": { + "type": "number", + "format": "double" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "additionalProperties": false + }, + "ReservationSaleExpanded": { + "type": "object", + "properties": { + "createdAt": { + "type": "string", + "format": "date-time" + }, + "id": { + "type": "string" + }, + "backOrderReservations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BackOrderReservationCompact" + }, + "description": "List of back ordered reservations that were not in stock at the time of purchase." + }, + "reservations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Reservation" + }, + "description": "List of reservations that associated with this sale. Any back ordered reservations that are fulfilled at a later time will also appear here." + }, + "state": { + "$ref": "#/components/schemas/ReservationSaleState" + }, + "total": { + "type": "number", + "format": "double" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "additionalProperties": false + }, + "ReservationSaleState": { + "enum": [ + "created", + "processing", + "failed", + "succeeded" + ], + "type": "string" + }, + "ReservationState": { + "enum": [ + "verificationPending", + "verificationCompleted", + "verificationCanceled", + "verificationTimedOut", + "verificationReported", + "verificationRefunded", + "verificationReused", + "verificationReactivated", + "renewableActive", + "renewableOverdue", + "renewableExpired", + "renewableRefunded", + "nonrenewableActive", + "nonrenewableExpired", + "nonrenewableRefunded" + ], + "type": "string" + }, + "ReservationType": { + "enum": [ + "renewable", + "nonrenewable", + "verification" + ], + "type": "string" + }, + "ReuseAction": { + "type": "object", + "properties": { + "link": { + "$ref": "#/components/schemas/Link" + }, + "reusableUntil": { + "type": "string", + "description": "If the verification is reusable, this value will be populated.", + "format": "date-time", + "nullable": true + } + }, + "additionalProperties": false + }, + "Service": { + "type": "object", + "properties": { + "serviceName": { + "type": "string", + "description": "Name of the service. Supply this value when a ```ServiceName``` is required.", + "example": "myservice" + }, + "capability": { + "$ref": "#/components/schemas/ReservationCapability" + } + }, + "additionalProperties": false + }, + "Sms": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "from": { + "type": "string", + "nullable": true + }, + "to": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "smsContent": { + "type": "string", + "nullable": true + }, + "parsedCode": { + "type": "string", + "nullable": true + }, + "encrypted": { + "type": "boolean" + } + }, + "additionalProperties": false, + "description": "Sms" + }, + "SmsWebhookEvent": { + "type": "object", + "properties": { + "from": { + "type": "string", + "nullable": true + }, + "to": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "smsContent": { + "type": "string", + "nullable": true + }, + "parsedCode": { + "type": "string", + "nullable": true + }, + "encrypted": { + "type": "boolean", + "description": "True if the contents of the sms is encrypted at rest." + }, + "reservationId": { + "type": "string", + "description": "The Id of the reservation that this sms was assigned to.", + "nullable": true + } + }, + "additionalProperties": false + }, + "UsageWindowEstimateRequest": { + "required": [ + "reservationId" + ], + "type": "object", + "properties": { + "reservationId": { + "type": "string", + "description": "The reservation Id to get the estimated usage window for. If a valid reservation does not exist, a 400 response will be returned." + } + }, + "additionalProperties": false + }, + "UsageWindowEstimateResponse": { + "type": "object", + "properties": { + "estimatedWindowStart": { + "type": "string", + "description": "The estimated datetime after which the reservation will be woken up and ready for use. Your reservation is estimated to be active and receiving sms and calls after this datetime. This value will be 'null' if the estimate was not successfully calculated.", + "format": "date-time", + "nullable": true + }, + "estimatedWindowEnd": { + "type": "string", + "description": "The estimated datetime after which a wake request will end. Your line is estimated to become offline and no longer be receiving sms or calls after this datetime. This value will be 'null' if the estimate was not successfully calculated.", + "format": "date-time", + "nullable": true + }, + "reservationId": { + "type": "string", + "description": "Id of the reservation that this usage window estimate is associated with." + } + }, + "additionalProperties": false + }, + "VerificationCompact": { + "type": "object", + "properties": { + "createdAt": { + "type": "string", + "format": "date-time" + }, + "id": { + "type": "string" + }, + "link": { + "$ref": "#/components/schemas/Link" + }, + "serviceName": { + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/ReservationState" + }, + "totalCost": { + "type": "number", + "format": "double" + }, + "number": { + "type": "string" + }, + "sms": { + "$ref": "#/components/schemas/Link" + }, + "calls": { + "$ref": "#/components/schemas/Link" + } + }, + "additionalProperties": false + }, + "VerificationExpanded": { + "type": "object", + "properties": { + "number": { + "type": "string" + }, + "sms": { + "$ref": "#/components/schemas/Link" + }, + "calls": { + "$ref": "#/components/schemas/Link" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "endsAt": { + "type": "string", + "format": "date-time" + }, + "id": { + "type": "string" + }, + "cancel": { + "$ref": "#/components/schemas/CancelAction" + }, + "reactivate": { + "$ref": "#/components/schemas/ReactivationAction" + }, + "report": { + "$ref": "#/components/schemas/ReportAction" + }, + "reuse": { + "$ref": "#/components/schemas/ReuseAction" + }, + "sale": { + "$ref": "#/components/schemas/Link" + }, + "serviceName": { + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/ReservationState" + }, + "totalCost": { + "type": "number", + "format": "double" + } + }, + "additionalProperties": false + }, + "VerificationPriceCheckRequest": { + "required": [ + "areaCode", + "capability", + "carrier", + "numberType", + "serviceName" + ], + "type": "object", + "properties": { + "serviceName": { + "type": "string", + "description": "", + "example": "yahoo" + }, + "areaCode": { + "type": "boolean", + "description": "", + "example": true + }, + "carrier": { + "type": "boolean", + "description": "", + "example": true + }, + "numberType": { + "$ref": "#/components/schemas/NumberType" + }, + "capability": { + "$ref": "#/components/schemas/ReservationCapability" + } + }, + "additionalProperties": false + }, + "WakeRequest": { + "required": [ + "reservationId" + ], + "type": "object", + "properties": { + "reservationId": { + "type": "string", + "description": "The reservation Id to create a wake request for. If a valid reservation does not exist, a 400 response will be returned." + } + }, + "additionalProperties": false + }, + "WakeResponse": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The Id of this wake request." + }, + "usageWindowStart": { + "type": "string", + "description": "The datetime after which the reservation will be woken up and ready for use. Your reservation will be active and receiving sms and calls after this datetime. This value will be 'null' if the wake request was not successfully scheduled.", + "format": "date-time", + "nullable": true + }, + "usageWindowEnd": { + "type": "string", + "description": "The datetime after which the wake request will end. Your line will become offline and no longer be receiving sms or calls after this datetime. You can then create another wake request to bring your reservation back online and being receiving sms and calls again. This value will be 'null' if the wake request was not successfully scheduled.", + "format": "date-time", + "nullable": true + }, + "isScheduled": { + "type": "boolean", + "description": "Indicates whether or not the wake request was successfully scheduled. If a wake request fails to be scheduled, then you will have to submit a new wake request. Too many wake requests may result in wake request throttling." + }, + "reservationId": { + "type": "string", + "description": "Id of the reservation that this wake request is associated with.", + "nullable": true + } + }, + "additionalProperties": false + }, + "WebhookEvent.BackOrderReservationWebhookEvent": { + "type": "object", + "properties": { + "attempt": { + "type": "integer", + "description": "Send attempt count", + "format": "int32" + }, + "occurredAt": { + "type": "string", + "description": "When the event occurred", + "format": "date-time" + }, + "data": { + "$ref": "#/components/schemas/BackOrderReservationWebhookEvent" + }, + "event": { + "type": "string", + "description": "Name of the event" + }, + "id": { + "type": "string", + "description": "Id of event" + } + }, + "additionalProperties": false + }, + "WebhookEvent.BillingCycleWebhookEvent": { + "type": "object", + "properties": { + "attempt": { + "type": "integer", + "description": "Send attempt count", + "format": "int32" + }, + "occurredAt": { + "type": "string", + "description": "When the event occurred", + "format": "date-time" + }, + "data": { + "$ref": "#/components/schemas/BillingCycleWebhookEvent" + }, + "event": { + "type": "string", + "description": "Name of the event" + }, + "id": { + "type": "string", + "description": "Id of event" + } + }, + "additionalProperties": false + }, + "WebhookEvent.ReservationCreatedWebhookEvent": { + "type": "object", + "properties": { + "attempt": { + "type": "integer", + "description": "Send attempt count", + "format": "int32" + }, + "occurredAt": { + "type": "string", + "description": "When the event occurred", + "format": "date-time" + }, + "data": { + "$ref": "#/components/schemas/ReservationCreatedWebhookEvent" + }, + "event": { + "type": "string", + "description": "Name of the event" + }, + "id": { + "type": "string", + "description": "Id of event" + } + }, + "additionalProperties": false + }, + "WebhookEvent.SmsWebhookEvent": { + "type": "object", + "properties": { + "attempt": { + "type": "integer", + "description": "Send attempt count", + "format": "int32" + }, + "occurredAt": { + "type": "string", + "description": "When the event occurred", + "format": "date-time" + }, + "data": { + "$ref": "#/components/schemas/SmsWebhookEvent" + }, + "event": { + "type": "string", + "description": "Name of the event" + }, + "id": { + "type": "string", + "description": "Id of event" + } + }, + "additionalProperties": false + } + }, + "securitySchemes": { + "Bearer": { + "type": "http", + "description": "This page functions only for the embedded JS client for testing purposes

A valid bearer token will start with eyJh....
To learn how to generate a bearer token, see Generate Bearer Token.

", + "scheme": "Bearer" + } + } + } +} \ No newline at end of file diff --git a/swagger.json b/swagger.json index a08608c..7bdc2da 100644 --- a/swagger.json +++ b/swagger.json @@ -1,5 +1,5 @@ { - "openapi": "3.0.1", + "openapi": "3.0.4", "info": { "title": "API Overview", "description": "

This is where you can get started with automating your workflows and integrating your apps with our API.

\n

For rentals (long term use of numbers), you'll need to check out the following links in order:

    \n
  1. Authenticate: Authentication
  2. \n
  3. Obtain a list of rental services: Service List
  4. \n
  5. Create a rental: Create New Rental
  6. \n
  7. Follow the location header returned to get the rental sale.
  8. \n
  9. From the sale, you'll be able to see the reservations and backorders.
  10. \n
\n

For verifications (single use of numbers), you'll need to check out the following links in order:

    \n
  1. Authenticate: Authentication
  2. \n
  3. Obtain a list of verification services: Service List
  4. \n
  5. Create a verification: Create Verification
  6. \n
  7. Follow the location header returned to get the newly created verification.
  8. \n
\n

Our API documentation has an embedded javascript client so that you can try out the API. You will need to setup your bearer token to be able to access secured endpoints.\n


\n

Paginated List Responses

\n

When you successfully get a list of data (generally, ordered by when the resource was created), for example List Billing Cycles, you will need to follow it's links results.\n
Inside the links result, you will get current, previous, and next values. Inside these values, there will be href and method values.\n
With these values, you can get more of your list by navigating through them.

\n

Example

\n

We get the result from List Billing Cycles, but we don't see what we need and need more billing cycles.
\nThe following code is whiteboarding code, use the language you are using for your integration with the API
\nWe will need to do the following:

\n
  • Get our api result data = getListBillingCyclesResult()
  • \n
  • Get the href = data['links']['next']['href']
  • \n
  • Get the method = data['links']['next']['method']
  • \n
  • Now you can get the next page: nextPage = fetch(href, {method=method})
  • \n
  • If we need the next page's result, we can repeat this process.
\n

Note that only renewable rentals will be associated with a billing cycle.

\n\n\n", @@ -22,7 +22,7 @@ "Bearer Tokens" ], "summary": "Generate Bearer Token", - "description": "Generate a bearer token to start making requests to secured endpoints. \r\nYou will need to supply the following headers:\r\n1) ```X-API-KEY``` (your primary API key)\r\n2) ```X-API-USERNAME``` (your username, which is the email you used to register) \r\nAll secured endpoints will need your bearer token supplied to them.
\r\nBecause bearer tokens can expire due to having a limited lifespan, you will need to obtain a new bearer token by generating a new one periodically.
\r\nYou can view and manage your API keys on under API Settings.
\r\nAfter successfully generating a bearer token ```eyJh...```, you can use set the bearer token here and explore our secured API endpoints.", + "description": "Generate a bearer token to start making requests to secured endpoints. \nYou will need to supply the following headers:\n1) ```X-API-KEY``` (your primary API key)\n2) ```X-API-USERNAME``` (your username, which is the email you used to register) \nAll secured endpoints will need your bearer token supplied to them.\n\nBecause bearer tokens can expire due to having a limited lifespan, you will need to obtain a new bearer token by generating a new one periodically.\n\nYou can view and manage your API keys on under API Settings.\n\nAfter successfully generating a bearer token ```eyJh...```, you can use set the bearer token here and explore our secured API endpoints.", "parameters": [ { "name": "X-API-USERNAME", @@ -45,7 +45,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -69,11 +69,11 @@ "Account" ], "summary": "Get Account Details", - "description": "Get the details of your user account.", + "description": "Retrieves the details of the currently authenticated account. Returns the username and current balance (in account credits) of the user, or ``` 401 Unauthorized ``` if the user is not authenticated.", "operationId": "GetAccountExpandedPublic", "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -102,11 +102,11 @@ "Services" ], "summary": "Area Codes List", - "description": "Returns a list of our supported area codes.\r\n\r\nThese supported area codes you get in the ```areaCode``` property from the response can be used for API calls where an ```areaCodeSelectOption``` is in the body or parameter.", + "description": "Returns a list of our supported area codes.\n\nThese supported area codes you get in the ```areaCode``` property from the response can be used for API calls where an ```areaCodeSelectOption``` is in the body or parameter.", "operationId": "ListAdvertisedAreaCodesPublic", "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -114,6 +114,25 @@ "items": { "$ref": "#/components/schemas/AreaCode" } + }, + "examples": { + "areaCodeList": { + "summary": "List of available area codes", + "value": [ + { + "areaCode": "205", + "state": "Alabama" + }, + { + "areaCode": "251", + "state": "Alabama" + }, + { + "areaCode": "307", + "state": "Wyoming" + } +] + } } } } @@ -141,7 +160,7 @@ "Services" ], "summary": "Service List", - "description": "Returns a list of our supported services.\r\n\r\nThese supported services you get in the ```serviceName``` property from the response will be used for API calls where a ```serviceName``` is required in the body or parameter.", + "description": "Returns a list of our supported services.\n\nThese supported services you get in the ```serviceName``` property from the response will be used for API calls where a ```serviceName``` is required in the body or parameter.", "operationId": "ListAdvertisedTargetsPublic", "parameters": [ { @@ -165,7 +184,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -173,6 +192,25 @@ "items": { "$ref": "#/components/schemas/Service" } + }, + "examples": { + "serviceList": { + "summary": "List of available services", + "value": [ + { + "serviceName": "yahoo", + "capability": "Sms" + }, + { + "serviceName": "facebook", + "capability": "Sms" + }, + { + "serviceName": "google", + "capability": "Sms" + } +] + } } } } @@ -200,7 +238,7 @@ "Services" ], "summary": "Rental Pricing", - "description": "Get rental prices for a specific service and option(s). The ```serviceName``` is required, and the list of supported service names can be found at Service List.\r\n\r\nNot all service and options are compatible with each other. This endpoint point will return a ```400 Bad Request``` for unsupported service and option combinations.", + "description": "Get rental prices for a specific service and option(s). The ```serviceName``` is required, and the list of supported service names can be found at Service List.\n\nNot all service and options are compatible with each other. This endpoint point will return a ```400 Bad Request``` for unsupported service and option combinations.", "operationId": "PriceCheckRentalPublic", "requestBody": { "content": { @@ -223,7 +261,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -255,7 +293,7 @@ "Services" ], "summary": "Verification Pricing", - "description": "Clients can call this endpoint to check the verification price of a specific service and option(s). The list of supported services (```serviceName```) can be found here.
\r\nNot all service and options are compatible with each other. This endpoint point will return a ```400 Bad Request``` for unsupported service and option combinations.", + "description": "Clients can call this endpoint to check the verification price of a specific service and option(s). The list of supported services (```serviceName```) can be found here.\n\nNot all service and options are compatible with each other. This endpoint point will return a ```400 Bad Request``` for unsupported service and option combinations.", "operationId": "PriceCheckVerificationPublic", "requestBody": { "content": { @@ -278,7 +316,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -310,7 +348,7 @@ "Sales" ], "summary": "Get Reservation Sale Details", - "description": "Get the details on a reservation sale by ID.", + "description": "Get the details on a reservation sale by ID.\n\nNote: This endpoint returns a paginated list of reservations and back ordered reservations (if applicable) associated with the provided sale ID, check\nthe Paginated List Responses section of the overview page for more details\nif you are having problems.", "operationId": "GetReservationSaleExpandedPublic", "parameters": [ { @@ -325,7 +363,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -357,10 +395,10 @@ "Sales" ], "summary": "List Reservation Sales", - "description": "Get a paginated list of your reservation sales.\r\n\r\nNote: This endpoint returns a paginated result of the list, check\r\nthe Paginated List Responses section of the overview page for more details\r\nif you are having problems.", + "description": "Get a paginated list of your reservation sales.\n\nNote: This endpoint returns a paginated result of the list, check\nthe Paginated List Responses section of the overview page for more details\nif you are having problems.", "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -389,7 +427,7 @@ "Billing Cycles" ], "summary": "Get Billing Cycle Details", - "description": "Get the details of a billing cycle by ID. A billing cycle can have one or many renewable rentals. Only renewable rentals are associated with a billing cycle.", + "description": "Get the details of a billing cycle by ID. A billing cycle can have one or many renewable rentals.\nOnly renewable rentals are associated with a billing cycle.", "operationId": "GetBillingCyclePublic", "parameters": [ { @@ -404,7 +442,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -434,7 +472,7 @@ "Billing Cycles" ], "summary": "Updating a Billing Cycle", - "description": "Update a billing cycle by ID.\r\n\r\nWhat you can currently update are:\r\n- ```nickname``` (Your custom string to identify the billing cycle)\r\n- ```remindersEnabled``` (Email reminders)\r\n\r\nNot providing an update option will leave it unchanged. For example, not providing ```remindersEnabled``` but having \r\n```\"nickname\": \"aNickname\"``` will only update the nickname.", + "description": "Update a billing cycle by ID.\n\nWhat you can currently update are:\n- ```nickname``` (Your custom string to identify the billing cycle)\n- ```remindersEnabled``` (Email reminders)\n\nNot providing an update option will leave it unchanged. For example, not providing ```remindersEnabled``` but having \n```\"nickname\": \"aNickname\"``` will only update the nickname.", "operationId": "UpdateBillingCyclePublic", "parameters": [ { @@ -469,7 +507,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" }, "400": { "description": "Bad Request" @@ -494,7 +532,7 @@ "Billing Cycles" ], "summary": "Get Billing Cycle Invoices", - "description": "Get a paginated invoice history of a billing cycle by it's ID.\r\n\r\nNote: This endpoint returns a paginated result of the list, check\r\nthe Paginated List Responses section of the overview page for more details\r\nif you are having problems.", + "description": "Get a paginated invoice history of a billing cycle by it's ID.\n\nNote: This endpoint returns a paginated result of the list, check\nthe Paginated List Responses section of the overview page for more details\nif you are having problems.", "operationId": "ListBillingCycleInvoicesPublic", "parameters": [ { @@ -509,7 +547,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -538,11 +576,11 @@ "Billing Cycles" ], "summary": "List Billing Cycles", - "description": "Returns a paginated list of your billing cycles. A billing cycle can have one or many renewable rentals. Only renewable rentals are associated with a billing cycle.\r\n\r\nNote: This endpoint returns a paginated result of the list, check\r\nthe Paginated List Responses section of the overview page for more details\r\nif you are having problems.", + "description": "Returns a paginated list of your billing cycles. A billing cycle can have one or many renewable rentals. Only renewable rentals are associated with a billing cycle.\n\nNote: This endpoint returns a paginated result of the list, check\nthe Paginated List Responses section of the overview page for more details\nif you are having problems.", "operationId": "ListBillingCyclesPublic", "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -586,7 +624,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -618,7 +656,7 @@ "Billing Cycles" ], "summary": "Renew Billing Cycle", - "description": "Renew the active rentals on your billing cycle by it's ID.\r\n \r\nThis will not renew overdue rentals.", + "description": "Renew the active rentals on your billing cycle by it's ID.\n \nThis will not renew overdue rentals. To renew overdue rentals, you must explicitly renew them using the\nRenew Overdue Rental endpoint.", "operationId": "RenewBillingCyclePublic", "parameters": [ { @@ -633,7 +671,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -665,7 +703,7 @@ "Back Order Rental Reservations" ], "summary": "Get Back Order Reservation Detail", - "description": "Get the details on a back order reservation by ID.\r\n\r\nBack order reservations are created if you have a parameter set when creating a new rental and there is no available stock. See Create New Rental for more info.", + "description": "Get the details on a back order reservation by ID.\n\nBack order reservations are created if you have a parameter set when creating a new rental and there is no available stock. See Create New Rental for more info.", "operationId": "GetBackOrderReservationExpandedPublic", "parameters": [ { @@ -680,7 +718,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -712,7 +750,7 @@ "Line Reservations" ], "summary": "Line Reservation Details", - "description": "Get the details on a line reservation by ID.\r\n\r\nSuccessfully getting the details of the line reservation will require following the ```href``` link provided in the response.", + "description": "Get the details on a line reservation by ID.\n\nSuccessfully getting the details of the line reservation will require following the ```href``` link provided in the response.", "operationId": "GetLineReservationExpandedPublic", "parameters": [ { @@ -735,7 +773,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -767,6 +805,7 @@ "Line Reservations" ], "summary": "Reservation Health Check", + "description": "Gets the health of a reservation and its line number.", "operationId": "GetReservationLineHealthPublic", "parameters": [ { @@ -781,7 +820,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -813,7 +852,7 @@ "New Verification" ], "summary": "Create Verification", - "description": "Create/purchase a new verification. If successful, a 201 response will be returned with the location of the newly created verification in the http response 'location' header.

\r\nThe ```serviceName``` is required, and the list of supported service names can be found at Service List.
", + "description": "Create/purchase a new verification. The ```serviceName``` is required, and the list of supported service names can be found at Service List.\n \nIf successful, a 201 response will be returned with the location of the newly created verification in the http response 'location' header.\nSome combinations of options are unavailable. Will return ```400 Bad Request``` if the request is invalid. To validate without the risk of using credits, use the verification pricing endpoint.", "requestBody": { "description": "", "content": { @@ -883,11 +922,11 @@ "Verifications" ], "summary": "List Verifications", - "description": "Clients can call this endpoint to retrieve the paginated list of verifications.", + "description": "Retrieves a paginated list of current verifications.\n \nNote: This endpoint returns a paginated result of the list, check\nthe Paginated List Responses section of the overview page for more details\nif you are having problems.", "operationId": "ListVerificationsPublic", "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -916,7 +955,7 @@ "Verifications" ], "summary": "Verification Details", - "description": "Clients can call this endpoint to retrieve the details of a verification.", + "description": "Retrieves the details of a verification. Also returns actions that can be performed on the verification, including the ability to cancel, report, reactivate, or reuse the verification.", "operationId": "GetVerificationExpandedPublic", "parameters": [ { @@ -931,7 +970,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -963,7 +1002,7 @@ "Verifications" ], "summary": "Cancel Verification", - "description": "Clients can call this endpoint to try to cancel a verification.", + "description": "Call this to attempt to cancel a verification by ID.\n\nReturns ```400 Bad Request``` if the verification does not allow cancellation.", "operationId": "CancelVerificationPublic", "parameters": [ { @@ -978,7 +1017,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" }, "400": { "description": "Bad Request" @@ -1003,7 +1042,7 @@ "Verifications" ], "summary": "Reactivate Verification", - "description": "Clients can call this endpoint to try to reactivate a verification. If successful, a 201 response will be returned with the location of the newly created verification in the http response 'location' header.", + "description": "Call this to attempt to reactivate a completed verification that can no longer be reused.\nThis may not be possible due to a number of reasons - to check eligibility, use verification details.\n\nIf successful, a 201 response will be returned with the location of the newly created verification ID in the http response 'location' header.\nIf the verification is not eligible for reactivation, a ```400 Bad Request``` will be returned with an error message.", "operationId": "ReactivateVerificationPublic", "parameters": [ { @@ -1067,7 +1106,7 @@ "Verifications" ], "summary": "Report Verification", - "description": "Clients can call this endpoint to try to report an verification.", + "description": "Call this to attempt to report a verification by ID for issues such as \"number already in use\", \"verification failed\", or \"code not received\".\n\nReturns ```400 Bad Request``` if the verification does not allow reporting.", "operationId": "ReportVerificationPublic", "parameters": [ { @@ -1082,7 +1121,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" }, "400": { "description": "Bad Request" @@ -1107,7 +1146,7 @@ "Verifications" ], "summary": "Reuse Verification", - "description": "Clients can call this endpoint to try to reuse a verification. If successful, a 201 response will be returned with the location of the newly created verification in the http response 'location' header.", + "description": "Call this to attempt to reuse a verification by ID.\nThis is usable only shortly after a verification has been completed successfully on this ID. To check eligibility, use verification details.\n \nIf successful, a 201 response will be returned with the location of the newly created verification ID in the http response 'location' header.\nIf the verification is not eligible for reuse, a ```400 Bad Request``` will be returned with an error message.", "operationId": "ReuseVerificationPublic", "parameters": [ { @@ -1171,7 +1210,7 @@ "New Rental" ], "summary": "Create New Rental", - "description": "Create/purchase a new rental. If successful, a 201 response will be returned with the location of the sale in the http response 'location' header. This sale will have details on the outcome of the rental purchase.

\r\nThe ```serviceName``` is required, and the list of supported service names can be found at Service List.
\r\nIf ```allowBackOrderReservations``` is ```true``` in the request body, a rental back order will be created if the requested rental is out of stock.", + "description": "Create/purchase a new rental. The ```serviceName``` is required, and the list of supported service names can be found at Service List. \n\nIf ```allowBackOrderReservations``` is ```true``` in the request body, a rental back order will be created if the requested rental is out of stock.\n \nIf successful, a 201 response will be returned with the location of the sale in the http response 'location' header. This sale will have details on the outcome of the rental purchase.\nSome combinations of options are unavailable. Will return ```400 Bad Request``` if the request is invalid. To validate without the risk of using credits, use the rental pricing endpoint.", "requestBody": { "description": "", "content": { @@ -1243,7 +1282,7 @@ "Renewable Rentals" ], "summary": "Renewable Rental Details", - "description": "Get the details of a renewable rental by ID.", + "description": "Get the details of a renewable rental by ID. Also returns relevant links to the billing cycle, calls, SMS, and sale information as well as a refund action if applicable.", "operationId": "GetRenewableRentalPublic", "parameters": [ { @@ -1258,7 +1297,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1288,7 +1327,7 @@ "Renewable Rentals" ], "summary": "Update a Renewable Rental", - "description": "Update a renewable rental by ID.\r\n\r\nWhat you can currently update are:\r\n- ```userNotes``` (Notes)\r\n- ```includeForRenewal``` (Include/Exclude rental for renew)\r\n- ```markAllSmsRead``` (Mark SMS as read)\r\n\r\nNot providing an update option will leave it unchanged. For example, not providing\r\n```userNotes``` but having ```\"includeForRenewal\": true``` will only include the rental for renew.", + "description": "Update a renewable rental by ID.\n\nWhat you can currently update are:\n- ```userNotes``` (Notes)\n- ```includeForRenewal``` (Include/Exclude rental for renew)\n- ```markAllSmsRead``` (Mark SMS as read)\n\nNot providing an update option will leave it unchanged. For example, not providing\n```userNotes``` but having ```\"includeForRenewal\": true``` will only include the rental for renew.", "operationId": "UpdateRenewableRentalPublic", "parameters": [ { @@ -1323,7 +1362,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" }, "400": { "description": "Bad Request" @@ -1348,7 +1387,7 @@ "Renewable Rentals" ], "summary": "List Renewable Rentals", - "description": "Returns a paginated list of your renewable rentals.\r\n\r\nOptional Parameters:\r\n- ```billingCycleId``` (Get renewable rentals under this billing cycle ID)\r\n\r\nTo create a new rental, see Create New Rental.\r\n\r\nNote: This endpoint returns a paginated result of the list, check\r\nthe Paginated List Responses section of the overview page for more details\r\nif you are having problems.", + "description": "Returns a paginated list of your renewable rentals.\n\nOptional Parameters:\n- ```billingCycleId``` (Get renewable rentals under this billing cycle ID)\n\nTo create a new rental, see Create New Rental.\n\nNote: This endpoint returns a paginated result of the list, check\nthe Paginated List Responses section of the overview page for more details\nif you are having problems.", "operationId": "ListRenewableLineRentalsPublic", "parameters": [ { @@ -1362,7 +1401,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1391,7 +1430,7 @@ "Renewable Rentals" ], "summary": "Refund Renewable Rental", - "description": "Attempt to refund a renewable rental by ID.", + "description": "Call this to attempt to refund a renewable rental by ID.\n\nThis could fail if the rental is not refundable or the refund period has passsed.\nWill return ```400 Bad Request``` if the request is invalid or refund is not possible.", "operationId": "RenewableSelfServiceRefundRentalPublic", "parameters": [ { @@ -1406,7 +1445,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" }, "400": { "description": "Bad Request" @@ -1431,7 +1470,7 @@ "Renewable Rentals" ], "summary": "Renew Overdue Rental", - "description": "Renew an overdue rental by it's ID. \r\n\r\nCalling this method will automatically include the rental for renewal and perform the renewal. Only overdue rentals may be renewed this way.", + "description": "Renew an overdue rental by its ID. \n\nCalling this method will automatically include the rental for renewal and perform the renewal. Only overdue rentals may be renewed this way.", "operationId": "RenewOverdueRenewableRental", "parameters": [ { @@ -1446,7 +1485,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" }, "400": { "description": "Bad Request" @@ -1471,7 +1510,7 @@ "Non-Renewable Rentals" ], "summary": "Non-Renewable Rental Details", - "description": "Get the details of a non-renewable rental by ID.", + "description": "Get the details of a non-renewable rental by ID. Also returns relevant links to the billing cycle, calls, SMS, and sale information as well as a refund action if applicable.", "operationId": "GetNonrenewableRentalPublic", "parameters": [ { @@ -1486,7 +1525,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1516,7 +1555,7 @@ "Non-Renewable Rentals" ], "summary": "Update a Non-Renewable Rental", - "description": "Update a non-renewable rental by ID.\r\n\r\nWhat you can currently update are:\r\n- ```userNotes``` (Notes)\r\n- ```markAllSmsRead``` (Mark SMS as read)", + "description": "Update a non-renewable rental by ID.\n\nWhat you can currently update are:\n- ```userNotes``` (Notes)\n- ```markAllSmsRead``` (Mark SMS as read)", "operationId": "UpdateNonrenewableRentalPublic", "parameters": [ { @@ -1551,7 +1590,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" }, "400": { "description": "Bad Request" @@ -1576,11 +1615,11 @@ "Non-Renewable Rentals" ], "summary": "List Non-Renewable Rentals", - "description": "Returns a paginated list of your non-renewable rentals.\r\n\r\nTo create a new rental, see Create New Rental.\r\n\r\nNote: This endpoint returns a paginated result of the list, check\r\nthe Paginated List Responses section of the overview page for more details\r\nif you are having problems.", + "description": "Returns a paginated list of your non-renewable rentals.\n\nTo create a new rental, see Create New Rental.\n\nNote: This endpoint returns a paginated result of the list, check\nthe Paginated List Responses section of the overview page for more details\nif you are having problems.", "operationId": "ListNonrenewableRentalsPublic", "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1609,7 +1648,7 @@ "Non-Renewable Rentals" ], "summary": "Extend a Non-Renewable rental", - "description": "\r\n

Extensions are NOT refundable and cannot be undone.

\r\n
\r\n

\r\n Attempt to extend the duration of a non-renewable rental by ID.

", + "description": "\n

Extensions are NOT refundable and cannot be undone.

\n
\n

\n Attempt to extend the duration of a non-renewable rental by ID.

", "operationId": "ExtendNonrenewableRentalPublic", "requestBody": { "description": "", @@ -1633,7 +1672,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" }, "400": { "description": "Bad Request" @@ -1658,7 +1697,7 @@ "Non-Renewable Rentals" ], "summary": "Refund Non-Renewable Rental", - "description": "Attempt to refund a non-renewable rental by ID.", + "description": "Call this to attempt to refund a non-renewable rental by ID.\n\nThis could fail if the rental is not refundable or the refund period has passsed.\nWill return ```400 Bad Request``` if the request is invalid or refund is not possible.", "operationId": "NonrenewableSelfServiceRefundRentalPublic", "parameters": [ { @@ -1673,7 +1712,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" }, "400": { "description": "Bad Request" @@ -1698,7 +1737,7 @@ "Sms" ], "summary": "List Sms", - "description": "Returns a paginated list of your latest sms.\r\n\r\nOptional Parameters:\r\n- ```to``` (A phone number)\r\n- ```reservationId``` (The reservation's ID)\r\n- ```reservationType``` (The reservation's type)\r\n\r\nNote: This endpoint returns a paginated result of the list, check\r\nthe Paginated List Responses section of the overview page for more details\r\nif you are having problems.", + "description": "Returns a paginated list of your latest sms.\n\nOptional Parameters:\n- ```to``` (A phone number)\n- ```reservationId``` (The reservation's ID)\n- ```reservationType``` (The reservation's type)\n\nNote: This endpoint returns a paginated result of the list, check\nthe Paginated List Responses section of the overview page for more details\nif you are having problems.", "operationId": "ListSmsPublic", "parameters": [ { @@ -1728,7 +1767,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1751,13 +1790,127 @@ ] } }, + "/api/pub/v2/calls": { + "get": { + "tags": [ + "Calls And Voice - [ALPHA]" + ], + "summary": "List Calls", + "description": "Call this endpoint to obtain a list of calls associated with your account.", + "operationId": "ListCallsPublic", + "parameters": [ + { + "name": "to", + "in": "query", + "description": "If provided, will filter results to only include calls made to the specified number.", + "schema": { + "type": "string" + } + }, + { + "name": "reservationId", + "in": "query", + "description": "If provided, will filter results to only include calls associated with the specified reservation.", + "schema": { + "type": "string" + } + }, + { + "name": "reservationType", + "in": "query", + "description": "If provided, will filter results to only include calls associated with the specified reservation type.", + "schema": { + "$ref": "#/components/schemas/ReservationType" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedData.Call" + } + } + } + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, + "/api/pub/v2/calls/access-token": { + "post": { + "tags": [ + "Calls And Voice - [ALPHA]" + ], + "summary": "Create Call Access Token", + "description": "Call this endpoint to create a call access token. This supports incoming calls only.\n\nCall sessions are established by:\n\n1. Creating a voice verification and getting the corresponding ```reservation id```.\n2. Posting to this endpoint to obtain a short-lived Twilio access token associated with the ```reservation id```.\n3. Using the access token with Twilio's integration to handle an incoming call.\n\n\nWarning:\nThis is an advanced integration. \nNo integration/implementation/debugging support will be provided for this endpoint. \nIt is up to you to implement the appropriate provider's integration (connectivity, acceptance, termination, etc.) to successfully handle an incoming call.", + "operationId": "SetupCallingContextPublic", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CallSessionRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CallSessionRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/CallSessionRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CallContext" + } + } + } + }, + "400": { + "description": "Bad Request" + }, + "429": { + "description": "Too many requests" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "Bearer": [ ] + } + ] + } + }, "/api/pub/v2/wake-requests/{id}": { "get": { "tags": [ "Wake Requests" ], "summary": "Get Wake Request", - "description": "Get a wake request by ID.\r\n\r\nThe details of a wake request include:\r\n- ```isScheduled``` (Indicates if the wake request has been successfully created and enqueued.)\r\n- ```usageWindowStart``` (The datetime when you can start using the reservation.)\r\n- ```usageWindowEnd``` (The datetime when you will no longer be able to use the reservation.)\r\nRefer to the ```SCHEMA``` below for more details.\r\n\r\nTo create a wake request, see Create Wake Request.", + "description": "Get a wake request by ID.\n\nThe details of a wake request include:\n- ```isScheduled``` (Indicates if the wake request has been successfully created and enqueued.)\n- ```usageWindowStart``` (The datetime when you can start using the reservation.)\n- ```usageWindowEnd``` (The datetime when you will no longer be able to use the reservation.)\nRefer to the ```SCHEMA``` below for more details.\n\nTo create a wake request, see Create Wake Request.", "operationId": "GetWakeRequestPublic", "parameters": [ { @@ -1772,7 +1925,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1804,7 +1957,7 @@ "Wake Requests" ], "summary": "Create Wake Request", - "description": "Create a wake request for a reservation. If successful, a 201 response will be returned with the location of the newly created wake request in the http response 'location' header.

\r\nSome reservation types will require a wake request before the line reservation can be used.
\r\nCreating a successful wake request will enqueue your request on that line reservation.\r\n\r\nTo see the details of your wake request, see Get Wake Request.", + "description": "Create a wake request for a reservation. Some reservation types, including lines where alwaysOn=false, will require a wake request before the line reservation can be used.\n\nCreating a successful wake request will enqueue your request on that line reservation.\nTo see the details of your wake request, see Get Wake Request.\n\nIf successful, a 201 response will be returned with the location of the newly created wake request in the http response 'location' header.", "operationId": "CreateWakeRequestPublic", "requestBody": { "description": "", @@ -1870,7 +2023,7 @@ "Wake Requests" ], "summary": "Estimate Usage Window", - "description": "Estimate the usage window for a reservation if you were to create a wake request now.\r\n\r\nDoes NOT create a wake request. \r\nTo create a wake request, see Create Wake Request.\r\n \r\nThe details of an usage window estimate include:\r\n- ```estimatedWindowStart``` (The estimated datetime when you can start using the reservation.)\r\n- ```estimatedWindowEnd``` (The estimated datetime when you will no longer be able to use the reservation.)\r\nRefer to the ```SCHEMA``` below for more details.", + "description": "Estimate the usage window for a reservation if you were to create a wake request now.\n\nDoes NOT create a wake request. \nTo create a wake request, see Create Wake Request.\n \nThe details of an usage window estimate include:\n- ```estimatedWindowStart``` (The estimated datetime when you can start using the reservation.)\n- ```estimatedWindowEnd``` (The estimated datetime when you will no longer be able to use the reservation.)\nRefer to the ```SCHEMA``` below for more details.", "operationId": "EstimateUsageWindow", "requestBody": { "description": "", @@ -1894,7 +2047,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1926,11 +2079,11 @@ "Webhooks" ], "summary": "List Webhook Events", - "description": "Returns a list of webhook events that can be subscribed to. You will need to configure your API Webhook Settings and subscribe to events first.\r\n

\r\nThe schema of the webhook events are documented below. We serialize webhook event data using Json to the `Data` property. All of our webhooks are sent as a HTTP POST to the webhook uri configured on your account.\r\nWe sign all of our webhooks with HMAC and SHA512 which should be validated by your server.\r\n

Webhook validation:
\r\nWe sign all webhook requests using HMAC-SHA512 and your webhook secret (`whsec_...`).\r\nThe signature HTTP Header is `X-Webhook-Signature` and the HTTP Value will be\r\nprefixed by the signing algorithm `HMAC-SHA512='.\r\nThe signature after the prefix is BASE-64 encoded.\r\nThe process for validating a signature is as follows:\r\n1) Extract the HTTP Header with the name `X-Webhook-Signature`.\r\n2) Remove the prefix `HMAC-SHA512=` from the HTTP Header value, which is obtained from step 1.\r\n - This BASE-64 encoded value will be compared to the HTTP Request body hash later.\r\n3) Apply the HMAC-SHA512 hashing algorithm to the HTTP Request body to obtain a byte array.\r\n4) Apply BASE-64 encoding to the byte array (obtained from step 3).\r\n5) Compare the value from step 2 to the value from step 4 and verify that they are the same.\r\n \r\nWebhooks are automatically retried for a period of time with exponential back off. \r\nAfter multiple failed webhook send attempts, webhooks will be automatically disabled.\r\nIf this happens, you will have to manually re-enable your webhooks again.", + "description": "Returns a list of webhook events that can be subscribed to. You will need to configure your API Webhook Settings and subscribe to events first.\n\n\n\nThe schema of the webhook events are documented below. We serialize webhook event data using Json to the `Data` property. All of our webhooks are sent as a HTTP POST to the webhook uri configured on your account.\nWe sign all of our webhooks with HMAC and SHA512 which should be validated by your server.\n\n\nWebhook validation:\n\nWe sign all webhook requests using HMAC-SHA512 and your webhook secret (`whsec_...`).\nThe signature HTTP Header is `X-Webhook-Signature` and the HTTP Value will be\nprefixed by the signing algorithm `HMAC-SHA512='.\nThe signature after the prefix is BASE-64 encoded.\nThe process for validating a signature is as follows:\n1) Extract the HTTP Header with the name `X-Webhook-Signature`.\n2) Remove the prefix `HMAC-SHA512=` from the HTTP Header value, which is obtained from step 1.\n - This BASE-64 encoded value will be compared to the HTTP Request body hash later.\n3) Apply the HMAC-SHA512 hashing algorithm to the HTTP Request body to obtain a byte array.\n4) Apply BASE-64 encoding to the byte array (obtained from step 3).\n5) Compare the value from step 2 to the value from step 4 and verify that they are the same.\n \nWebhooks are automatically retried for a period of time with exponential back off. \nAfter multiple failed webhook send attempts, webhooks will be automatically disabled.\nIf this happens, you will have to manually re-enable your webhooks again.", "operationId": "GetWebhookEventDefinitionsPublic", "responses": { "200": { - "description": "Success" + "description": "OK" }, "429": { "description": "Too many requests" @@ -1941,7 +2094,7 @@ }, "callbacks": { "v2.rental.billingcycle.renewed": { - "https://www.example.com/": { + "https://your.webhook-server.url/": { "post": { "requestBody": { "description": "Triggers when a billing cycle on your account has renewed. Renewal of individual reservations on a billing cycle can occur independently.", @@ -1968,7 +2121,7 @@ } }, "v2.rental.billingcycle.expired": { - "https://www.example.com/": { + "https://your.webhook-server.url/": { "post": { "requestBody": { "description": "Triggers when a billing cycle on your account has expired.", @@ -1995,7 +2148,7 @@ } }, "v2.sms.received": { - "https://www.example.com/": { + "https://your.webhook-server.url/": { "post": { "requestBody": { "description": "Triggers when an sms is received and assigned to your account.", @@ -2022,7 +2175,7 @@ } }, "v2.rental.backorder.fulfilled": { - "https://www.example.com/": { + "https://your.webhook-server.url/": { "post": { "requestBody": { "description": "Triggers when a back order reservation is fulfilled.", @@ -2049,7 +2202,7 @@ } }, "v2.reservation.created": { - "https://www.example.com/": { + "https://your.webhook-server.url/": { "post": { "requestBody": { "description": "Triggers when a reservation is created.", @@ -2089,7 +2242,7 @@ "Legacy" ], "summary": "Legacy Reservation Id Look Up", - "description": "You can use this legacy endpoint for looking up the 'new' ID of a rental reservation (begins with the prefix 'lr_') by supplying a legacy ID (GUID/UUID format). This may be helpful when migration from our deprecated endpoints to our current endpoints.\r\nThis endpoint will be removed in the future without notice.", + "description": "You can use this legacy endpoint for looking up the 'new' ID of a rental reservation (begins with the prefix 'lr_') by supplying a legacy ID (GUID/UUID format). This may be helpful when migration from our deprecated endpoints to our current endpoints.\nThis endpoint will be removed in the future without notice.", "operationId": "GetNewReservationId", "parameters": [ { @@ -2134,19 +2287,32 @@ "components": { "schemas": { "Account": { + "required": [ + "username" + ], "type": "object", "properties": { "username": { - "type": "string" + "type": "string", + "description": "The username of the account holder.", + "example": "john_doe@example.com" }, "currentBalance": { "type": "number", - "format": "double" + "description": "The current balance of the account.", + "format": "double", + "example": 1234.56 } }, "additionalProperties": false }, "AddOnSnapshot": { + "required": [ + "addOnId", + "alreadyRenewed", + "description", + "renewalCost" + ], "type": "object", "properties": { "addOnId": { @@ -2166,6 +2332,10 @@ "additionalProperties": false }, "AreaCode": { + "required": [ + "areaCode", + "state" + ], "type": "object", "properties": { "areaCode": { @@ -2182,6 +2352,12 @@ "additionalProperties": false }, "BackOrderReservationCompact": { + "required": [ + "id", + "link", + "serviceName", + "status" + ], "type": "object", "properties": { "link": { @@ -2191,7 +2367,9 @@ "type": "string" }, "serviceName": { - "type": "string" + "type": "string", + "description": "Name of service", + "example": "yahoo" }, "status": { "$ref": "#/components/schemas/BackOrderState" @@ -2200,6 +2378,16 @@ "additionalProperties": false }, "BackOrderReservationExpanded": { + "required": [ + "id", + "link", + "reservation", + "reservationId", + "sale", + "saleId", + "serviceName", + "status" + ], "type": "object", "properties": { "link": { @@ -2232,6 +2420,9 @@ "additionalProperties": false }, "BackOrderReservationWebhookEvent": { + "required": [ + "backOrderId" + ], "type": "object", "properties": { "backOrderId": { @@ -2250,6 +2441,11 @@ "type": "string" }, "BearerToken": { + "required": [ + "expiresAt", + "expiresIn", + "token" + ], "type": "object", "properties": { "token": { @@ -2273,6 +2469,12 @@ "additionalProperties": false }, "BillingCycleCompact": { + "required": [ + "billingCycleEndsAt", + "emailNotificationsEnabled", + "id", + "state" + ], "type": "object", "properties": { "id": { @@ -2293,6 +2495,16 @@ "additionalProperties": false }, "BillingCycleExpanded": { + "required": [ + "billingCycleEndsAt", + "emailNotificationsEnabled", + "id", + "invoices", + "nextAutoRenewAttempt", + "renewedThrough", + "rentals", + "state" + ], "type": "object", "properties": { "id": { @@ -2332,6 +2544,14 @@ "additionalProperties": false }, "BillingCycleRenewalInvoice": { + "required": [ + "createdAt", + "excludedRentals", + "id", + "includedRentals", + "isPaidFor", + "totalCost" + ], "type": "object", "properties": { "createdAt": { @@ -2358,12 +2578,18 @@ }, "totalCost": { "type": "number", - "format": "double" + "description": "Total amount cost of the invoice, in account credits.", + "format": "double", + "example": 11.9 } }, "additionalProperties": false }, "BillingCycleRenewalInvoicePreview": { + "required": [ + "billingCycleId", + "renewalEstimate" + ], "type": "object", "properties": { "billingCycleId": { @@ -2391,6 +2617,9 @@ "description": "Supplying a value of 'null' or not supplying a value for any nullable properties will cause the property to be ignored." }, "BillingCycleWebhookEvent": { + "required": [ + "billingCycleId" + ], "type": "object", "properties": { "billingCycleId": { @@ -2401,7 +2630,71 @@ }, "additionalProperties": false }, + "Call": { + "required": [ + "createdAt", + "from", + "id", + "recordingUri", + "to" + ], + "type": "object", + "properties": { + "from": { + "type": "string", + "nullable": true + }, + "to": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "id": { + "type": "string" + }, + "recordingUri": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "CallContext": { + "required": [ + "reservationId", + "twilioContext" + ], + "type": "object", + "properties": { + "reservationId": { + "type": "string", + "description": "Id of the verification that this call context is associated with." + }, + "twilioContext": { + "$ref": "#/components/schemas/TwilioCallingContextDto" + } + }, + "additionalProperties": false + }, + "CallSessionRequest": { + "required": [ + "reservationId" + ], + "type": "object", + "properties": { + "reservationId": { + "type": "string" + } + }, + "additionalProperties": false + }, "CancelAction": { + "required": [ + "canCancel", + "link" + ], "type": "object", "properties": { "canCancel": { @@ -2414,6 +2707,10 @@ "additionalProperties": false }, "Error": { + "required": [ + "errorCode", + "errorDescription" + ], "type": "object", "properties": { "errorCode": { @@ -2435,11 +2732,16 @@ "type": "string" }, "LineHealth": { + "required": [ + "checkedAt", + "lineNumber" + ], "type": "object", "properties": { "lineNumber": { "type": "string", - "description": "Line number associated with the reservation." + "description": "Line number associated with the reservation.", + "example": "2223334444" }, "checkedAt": { "type": "string", @@ -2458,17 +2760,23 @@ "type": "string" }, "Link": { + "required": [ + "href", + "method" + ], "type": "object", "properties": { "method": { "type": "string", "description": "The HTTP method for this link.", - "nullable": true + "nullable": true, + "example": "GET|POST" }, "href": { "type": "string", "description": "The HTTP location of the resource.", - "nullable": true + "nullable": true, + "example": "https://...." } }, "additionalProperties": false @@ -2522,7 +2830,7 @@ "serviceName": { "type": "string", "description": "Name of the service", - "example": "myservice" + "example": "google" }, "capability": { "$ref": "#/components/schemas/ReservationCapability" @@ -2579,12 +2887,24 @@ "type": "number", "description": "Optional decimal to specify the maximum total price you are willing to pay for the requested reservation. If the price exceeds this amount, the request will be rejected and no reservation will be created.", "format": "double", - "nullable": true + "nullable": true, + "example": 2.95 } }, "additionalProperties": false }, "NonrenewableRentalCompact": { + "required": [ + "alwaysOn", + "createdAt", + "id", + "link", + "number", + "sale", + "saleId", + "serviceName", + "state" + ], "type": "object", "properties": { "createdAt": { @@ -2621,6 +2941,20 @@ "additionalProperties": false }, "NonrenewableRentalExpanded": { + "required": [ + "alwaysOn", + "calls", + "createdAt", + "endsAt", + "id", + "number", + "refund", + "sale", + "saleId", + "serviceName", + "sms", + "state" + ], "type": "object", "properties": { "calls": { @@ -2698,6 +3032,13 @@ "type": "string" }, "PaginatedData.BillingCycleCompact": { + "required": [ + "count", + "data", + "hasNext", + "hasPrevious", + "links" + ], "type": "object", "properties": { "data": { @@ -2723,6 +3064,13 @@ "additionalProperties": false }, "PaginatedData.BillingCycleRenewalInvoice": { + "required": [ + "count", + "data", + "hasNext", + "hasPrevious", + "links" + ], "type": "object", "properties": { "data": { @@ -2747,7 +3095,46 @@ }, "additionalProperties": false }, + "PaginatedData.Call": { + "required": [ + "count", + "data", + "hasNext", + "hasPrevious", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Call" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrevious": { + "type": "boolean" + }, + "count": { + "type": "integer", + "format": "int64" + }, + "links": { + "$ref": "#/components/schemas/PaginationLinks" + } + }, + "additionalProperties": false + }, "PaginatedData.NonrenewableRentalCompact": { + "required": [ + "count", + "data", + "hasNext", + "hasPrevious", + "links" + ], "type": "object", "properties": { "data": { @@ -2773,6 +3160,13 @@ "additionalProperties": false }, "PaginatedData.RenewableRentalCompact": { + "required": [ + "count", + "data", + "hasNext", + "hasPrevious", + "links" + ], "type": "object", "properties": { "data": { @@ -2798,6 +3192,13 @@ "additionalProperties": false }, "PaginatedData.ReservationSaleCompact": { + "required": [ + "count", + "data", + "hasNext", + "hasPrevious", + "links" + ], "type": "object", "properties": { "data": { @@ -2823,6 +3224,13 @@ "additionalProperties": false }, "PaginatedData.Sms": { + "required": [ + "count", + "data", + "hasNext", + "hasPrevious", + "links" + ], "type": "object", "properties": { "data": { @@ -2848,6 +3256,13 @@ "additionalProperties": false }, "PaginatedData.VerificationCompact": { + "required": [ + "count", + "data", + "hasNext", + "hasPrevious", + "links" + ], "type": "object", "properties": { "data": { @@ -2873,6 +3288,11 @@ "additionalProperties": false }, "PaginationLinks": { + "required": [ + "current", + "next", + "previous" + ], "type": "object", "properties": { "current": { @@ -2888,12 +3308,16 @@ "additionalProperties": false }, "PricingSnapshot": { + "required": [ + "price", + "serviceName" + ], "type": "object", "properties": { "serviceName": { "type": "string", "description": "Name of the service.", - "example": "myservice" + "example": "hotmail" }, "price": { "type": "number", @@ -2905,6 +3329,10 @@ "additionalProperties": false }, "ReactivationAction": { + "required": [ + "canReactivate", + "link" + ], "type": "object", "properties": { "canReactivate": { @@ -2917,6 +3345,11 @@ "additionalProperties": false }, "RefundAction": { + "required": [ + "canRefund", + "link", + "refundableUntil" + ], "type": "object", "properties": { "canRefund": { @@ -2934,6 +3367,20 @@ "additionalProperties": false }, "RenewableRentalCompact": { + "required": [ + "alwaysOn", + "billingCycle", + "billingCycleId", + "createdAt", + "id", + "isIncludedForNextRenewal", + "link", + "number", + "sale", + "saleId", + "serviceName", + "state" + ], "type": "object", "properties": { "createdAt": { @@ -2952,7 +3399,8 @@ "nullable": true }, "serviceName": { - "type": "string" + "type": "string", + "example": "yahoo" }, "link": { "$ref": "#/components/schemas/Link" @@ -2970,7 +3418,8 @@ "type": "boolean" }, "number": { - "type": "string" + "type": "string", + "example": "2223334444" }, "alwaysOn": { "type": "boolean" @@ -2979,6 +3428,22 @@ "additionalProperties": false }, "RenewableRentalExpanded": { + "required": [ + "alwaysOn", + "billingCycle", + "billingCycleId", + "calls", + "createdAt", + "id", + "isIncludedForNextRenewal", + "number", + "refund", + "sale", + "saleId", + "serviceName", + "sms", + "state" + ], "type": "object", "properties": { "sms": { @@ -3021,7 +3486,8 @@ "type": "boolean" }, "number": { - "type": "string" + "type": "string", + "example": "2223334444" }, "alwaysOn": { "type": "boolean" @@ -3067,6 +3533,10 @@ "type": "string" }, "RentalExtensionRequest": { + "required": [ + "extensionDuration", + "rentalId" + ], "type": "object", "properties": { "extensionDuration": { @@ -3093,11 +3563,11 @@ "serviceName": { "type": "string", "description": "Name of the service", - "example": "myservice" + "example": "yahoo" }, "areaCode": { "type": "boolean", - "description": "", + "description": "Set to true if a specific area code will be requested when creating a rental, false if any area code can be used.", "example": true }, "numberType": { @@ -3113,9 +3583,9 @@ }, "callForwarding": { "type": "boolean", - "description": "", + "description": "Not supported at this time.", "nullable": true, - "example": true + "example": false }, "billingCycleIdToAssignTo": { "type": "string", @@ -3135,6 +3605,15 @@ "additionalProperties": false }, "RentalSnapshot": { + "required": [ + "alreadyRenewed", + "excludedAddOns", + "includedAddOns", + "number", + "renewalCost", + "rental", + "serviceName" + ], "type": "object", "properties": { "number": { @@ -3145,7 +3624,9 @@ }, "renewalCost": { "type": "number", - "format": "double" + "description": "Renewal cost, in account credits.", + "format": "double", + "example": 5.95 }, "serviceName": { "type": "string" @@ -3171,6 +3652,10 @@ "additionalProperties": false }, "ReportAction": { + "required": [ + "canReport", + "link" + ], "type": "object", "properties": { "canReport": { @@ -3183,6 +3668,12 @@ "additionalProperties": false }, "Reservation": { + "required": [ + "id", + "link", + "reservationType", + "serviceName" + ], "type": "object", "properties": { "id": { @@ -3197,7 +3688,8 @@ }, "serviceName": { "type": "string", - "description": "Name of service" + "description": "Name of service", + "example": "yahoo" } }, "additionalProperties": false @@ -3211,6 +3703,10 @@ "type": "string" }, "ReservationCreatedWebhookEvent": { + "required": [ + "id", + "type" + ], "type": "object", "properties": { "id": { @@ -3224,6 +3720,14 @@ "additionalProperties": false }, "ReservationSaleCompact": { + "required": [ + "createdAt", + "id", + "link", + "state", + "totalCost", + "updatedAt" + ], "type": "object", "properties": { "createdAt": { @@ -3241,7 +3745,8 @@ }, "totalCost": { "type": "number", - "format": "double" + "format": "double", + "example": 0.95 }, "updatedAt": { "type": "string", @@ -3251,6 +3756,15 @@ "additionalProperties": false }, "ReservationSaleExpanded": { + "required": [ + "backOrderReservations", + "createdAt", + "id", + "reservations", + "state", + "total", + "updatedAt" + ], "type": "object", "properties": { "createdAt": { @@ -3279,7 +3793,9 @@ }, "total": { "type": "number", - "format": "double" + "description": "Total amount of the sale, in account credits.", + "format": "double", + "example": 5.25 }, "updatedAt": { "type": "string", @@ -3326,6 +3842,10 @@ "type": "string" }, "ReuseAction": { + "required": [ + "link", + "reusableUntil" + ], "type": "object", "properties": { "link": { @@ -3341,12 +3861,16 @@ "additionalProperties": false }, "Service": { + "required": [ + "capability", + "serviceName" + ], "type": "object", "properties": { "serviceName": { "type": "string", "description": "Name of the service. Supply this value when a ```ServiceName``` is required.", - "example": "myservice" + "example": "gmail" }, "capability": { "$ref": "#/components/schemas/ReservationCapability" @@ -3355,6 +3879,15 @@ "additionalProperties": false }, "Sms": { + "required": [ + "createdAt", + "encrypted", + "from", + "id", + "parsedCode", + "smsContent", + "to" + ], "type": "object", "properties": { "id": { @@ -3387,6 +3920,15 @@ "description": "Sms" }, "SmsWebhookEvent": { + "required": [ + "createdAt", + "encrypted", + "from", + "parsedCode", + "reservationId", + "smsContent", + "to" + ], "type": "object", "properties": { "from": { @@ -3420,6 +3962,20 @@ }, "additionalProperties": false }, + "TwilioCallingContextDto": { + "required": [ + "token" + ], + "type": "object", + "properties": { + "token": { + "type": "string", + "description": "Token used for establish a client call session.", + "nullable": true + } + }, + "additionalProperties": false + }, "UsageWindowEstimateRequest": { "required": [ "reservationId" @@ -3434,6 +3990,11 @@ "additionalProperties": false }, "UsageWindowEstimateResponse": { + "required": [ + "estimatedWindowEnd", + "estimatedWindowStart", + "reservationId" + ], "type": "object", "properties": { "estimatedWindowStart": { @@ -3456,6 +4017,17 @@ "additionalProperties": false }, "VerificationCompact": { + "required": [ + "calls", + "createdAt", + "id", + "link", + "number", + "serviceName", + "sms", + "state", + "totalCost" + ], "type": "object", "properties": { "createdAt": { @@ -3476,7 +4048,8 @@ }, "totalCost": { "type": "number", - "format": "double" + "format": "double", + "example": 0.95 }, "number": { "type": "string" @@ -3491,6 +4064,22 @@ "additionalProperties": false }, "VerificationExpanded": { + "required": [ + "calls", + "cancel", + "createdAt", + "endsAt", + "id", + "number", + "reactivate", + "report", + "reuse", + "sale", + "serviceName", + "sms", + "state", + "totalCost" + ], "type": "object", "properties": { "number": { @@ -3536,7 +4125,8 @@ }, "totalCost": { "type": "number", - "format": "double" + "format": "double", + "example": 0.95 } }, "additionalProperties": false @@ -3558,12 +4148,12 @@ }, "areaCode": { "type": "boolean", - "description": "", + "description": "Set to true if a specific area code will be requested when creating a verification, false if any area code can be used.", "example": true }, "carrier": { "type": "boolean", - "description": "", + "description": "Set to true if a specific carrier will be requested when creating a verification, false if any carrier can be used.", "example": true }, "numberType": { @@ -3589,6 +4179,13 @@ "additionalProperties": false }, "WakeResponse": { + "required": [ + "id", + "isScheduled", + "reservationId", + "usageWindowEnd", + "usageWindowStart" + ], "type": "object", "properties": { "id": { @@ -3620,6 +4217,13 @@ "additionalProperties": false }, "WebhookEvent.BackOrderReservationWebhookEvent": { + "required": [ + "attempt", + "data", + "event", + "id", + "occurredAt" + ], "type": "object", "properties": { "attempt": { @@ -3647,6 +4251,13 @@ "additionalProperties": false }, "WebhookEvent.BillingCycleWebhookEvent": { + "required": [ + "attempt", + "data", + "event", + "id", + "occurredAt" + ], "type": "object", "properties": { "attempt": { @@ -3674,6 +4285,13 @@ "additionalProperties": false }, "WebhookEvent.ReservationCreatedWebhookEvent": { + "required": [ + "attempt", + "data", + "event", + "id", + "occurredAt" + ], "type": "object", "properties": { "attempt": { @@ -3701,6 +4319,13 @@ "additionalProperties": false }, "WebhookEvent.SmsWebhookEvent": { + "required": [ + "attempt", + "data", + "event", + "id", + "occurredAt" + ], "type": "object", "properties": { "attempt": { diff --git a/tests/mock_endpoints_generated/api.pub.v2.account.me.json b/tests/mock_endpoints_generated/api.pub.v2.account.me.json index 6587b76..8200cac 100644 --- a/tests/mock_endpoints_generated/api.pub.v2.account.me.json +++ b/tests/mock_endpoints_generated/api.pub.v2.account.me.json @@ -1,8 +1,8 @@ { "get": { "response": { - "username": "string", - "currentBalance": 0.0 + "username": "john_doe@example.com", + "currentBalance": 1234.56 } } } \ No newline at end of file diff --git a/tests/mock_endpoints_generated/api.pub.v2.backorders.{id}.json b/tests/mock_endpoints_generated/api.pub.v2.backorders.{id}.json index 447b44f..00ec658 100644 --- a/tests/mock_endpoints_generated/api.pub.v2.backorders.{id}.json +++ b/tests/mock_endpoints_generated/api.pub.v2.backorders.{id}.json @@ -5,8 +5,8 @@ }, "response": { "reservation": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, "id": "string", "serviceName": "string", diff --git a/tests/mock_endpoints_generated/api.pub.v2.billing-cycles.json b/tests/mock_endpoints_generated/api.pub.v2.billing-cycles.json index c2d88c1..76a4b3a 100644 --- a/tests/mock_endpoints_generated/api.pub.v2.billing-cycles.json +++ b/tests/mock_endpoints_generated/api.pub.v2.billing-cycles.json @@ -20,8 +20,8 @@ "count": 0, "links": { "next": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." } } } diff --git a/tests/mock_endpoints_generated/api.pub.v2.billing-cycles.{id}.invoices.json b/tests/mock_endpoints_generated/api.pub.v2.billing-cycles.{id}.invoices.json index 7baa79a..f346b61 100644 --- a/tests/mock_endpoints_generated/api.pub.v2.billing-cycles.{id}.invoices.json +++ b/tests/mock_endpoints_generated/api.pub.v2.billing-cycles.{id}.invoices.json @@ -12,10 +12,10 @@ { "number": "string", "rental": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, - "renewalCost": 0.0, + "renewalCost": 5.95, "serviceName": "string", "alreadyRenewed": false, "includedAddOns": [ @@ -50,10 +50,10 @@ { "number": "string", "rental": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, - "renewalCost": 0.0, + "renewalCost": 5.95, "serviceName": "string", "alreadyRenewed": false, "includedAddOns": [ @@ -90,10 +90,10 @@ { "number": "string", "rental": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, - "renewalCost": 0.0, + "renewalCost": 5.95, "serviceName": "string", "alreadyRenewed": false, "includedAddOns": [ @@ -128,10 +128,10 @@ { "number": "string", "rental": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, - "renewalCost": 0.0, + "renewalCost": 5.95, "serviceName": "string", "alreadyRenewed": false, "includedAddOns": [ @@ -165,7 +165,7 @@ } ], "isPaidFor": false, - "totalCost": 0.0 + "totalCost": 11.9 }, { "createdAt": "1970-01-01T00:00:00+00:00", @@ -174,10 +174,10 @@ { "number": "string", "rental": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, - "renewalCost": 0.0, + "renewalCost": 5.95, "serviceName": "string", "alreadyRenewed": false, "includedAddOns": [ @@ -212,10 +212,10 @@ { "number": "string", "rental": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, - "renewalCost": 0.0, + "renewalCost": 5.95, "serviceName": "string", "alreadyRenewed": false, "includedAddOns": [ @@ -252,10 +252,10 @@ { "number": "string", "rental": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, - "renewalCost": 0.0, + "renewalCost": 5.95, "serviceName": "string", "alreadyRenewed": false, "includedAddOns": [ @@ -290,10 +290,10 @@ { "number": "string", "rental": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, - "renewalCost": 0.0, + "renewalCost": 5.95, "serviceName": "string", "alreadyRenewed": false, "includedAddOns": [ @@ -327,7 +327,7 @@ } ], "isPaidFor": false, - "totalCost": 0.0 + "totalCost": 11.9 } ], "hasNext": false, @@ -335,8 +335,8 @@ "count": 0, "links": { "next": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." } } } diff --git a/tests/mock_endpoints_generated/api.pub.v2.billing-cycles.{id}.json b/tests/mock_endpoints_generated/api.pub.v2.billing-cycles.{id}.json index 7f1d777..0a739e9 100644 --- a/tests/mock_endpoints_generated/api.pub.v2.billing-cycles.{id}.json +++ b/tests/mock_endpoints_generated/api.pub.v2.billing-cycles.{id}.json @@ -11,8 +11,8 @@ "emailNotificationsEnabled": false, "state": "string", "invoices": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." } } }, diff --git a/tests/mock_endpoints_generated/api.pub.v2.billing-cycles.{id}.next-invoice.json b/tests/mock_endpoints_generated/api.pub.v2.billing-cycles.{id}.next-invoice.json index 346cf0d..c4c2a56 100644 --- a/tests/mock_endpoints_generated/api.pub.v2.billing-cycles.{id}.next-invoice.json +++ b/tests/mock_endpoints_generated/api.pub.v2.billing-cycles.{id}.next-invoice.json @@ -12,10 +12,10 @@ { "number": "string", "rental": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, - "renewalCost": 0.0, + "renewalCost": 5.95, "serviceName": "string", "alreadyRenewed": false, "includedAddOns": [ @@ -50,10 +50,10 @@ { "number": "string", "rental": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, - "renewalCost": 0.0, + "renewalCost": 5.95, "serviceName": "string", "alreadyRenewed": false, "includedAddOns": [ @@ -90,10 +90,10 @@ { "number": "string", "rental": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, - "renewalCost": 0.0, + "renewalCost": 5.95, "serviceName": "string", "alreadyRenewed": false, "includedAddOns": [ @@ -128,10 +128,10 @@ { "number": "string", "rental": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, - "renewalCost": 0.0, + "renewalCost": 5.95, "serviceName": "string", "alreadyRenewed": false, "includedAddOns": [ @@ -165,7 +165,7 @@ } ], "isPaidFor": false, - "totalCost": 0.0 + "totalCost": 11.9 } } } diff --git a/tests/mock_endpoints_generated/api.pub.v2.billing-cycles.{id}.renew.json b/tests/mock_endpoints_generated/api.pub.v2.billing-cycles.{id}.renew.json index b902058..5242c69 100644 --- a/tests/mock_endpoints_generated/api.pub.v2.billing-cycles.{id}.renew.json +++ b/tests/mock_endpoints_generated/api.pub.v2.billing-cycles.{id}.renew.json @@ -10,10 +10,10 @@ { "number": "string", "rental": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, - "renewalCost": 0.0, + "renewalCost": 5.95, "serviceName": "string", "alreadyRenewed": false, "includedAddOns": [ @@ -48,10 +48,10 @@ { "number": "string", "rental": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, - "renewalCost": 0.0, + "renewalCost": 5.95, "serviceName": "string", "alreadyRenewed": false, "includedAddOns": [ @@ -88,10 +88,10 @@ { "number": "string", "rental": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, - "renewalCost": 0.0, + "renewalCost": 5.95, "serviceName": "string", "alreadyRenewed": false, "includedAddOns": [ @@ -126,10 +126,10 @@ { "number": "string", "rental": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, - "renewalCost": 0.0, + "renewalCost": 5.95, "serviceName": "string", "alreadyRenewed": false, "includedAddOns": [ @@ -163,7 +163,7 @@ } ], "isPaidFor": false, - "totalCost": 0.0 + "totalCost": 11.9 } } } \ No newline at end of file diff --git a/tests/mock_endpoints_generated/api.pub.v2.calls.access-token.json b/tests/mock_endpoints_generated/api.pub.v2.calls.access-token.json new file mode 100644 index 0000000..367aa80 --- /dev/null +++ b/tests/mock_endpoints_generated/api.pub.v2.calls.access-token.json @@ -0,0 +1,10 @@ +{ + "post": { + "response": { + "reservationId": "string", + "twilioContext": { + "token": "string" + } + } + } +} \ No newline at end of file diff --git a/tests/mock_endpoints_generated/api.pub.v2.calls.json b/tests/mock_endpoints_generated/api.pub.v2.calls.json new file mode 100644 index 0000000..5cca2b5 --- /dev/null +++ b/tests/mock_endpoints_generated/api.pub.v2.calls.json @@ -0,0 +1,36 @@ +{ + "get": { + "query_params": { + "to": "string", + "reservationId": "string", + "reservationType": "renewable" + }, + "response": { + "data": [ + { + "from": "string", + "to": "string", + "createdAt": "1970-01-01T00:00:00+00:00", + "id": "string", + "recordingUri": "string" + }, + { + "from": "string", + "to": "string", + "createdAt": "1970-01-01T00:00:00+00:00", + "id": "string", + "recordingUri": "string" + } + ], + "hasNext": false, + "hasPrevious": false, + "count": 0, + "links": { + "next": { + "method": "GET|POST", + "href": "https://...." + } + } + } + } +} \ No newline at end of file diff --git a/tests/mock_endpoints_generated/api.pub.v2.pricing.rentals.json b/tests/mock_endpoints_generated/api.pub.v2.pricing.rentals.json index 3df4759..8479e06 100644 --- a/tests/mock_endpoints_generated/api.pub.v2.pricing.rentals.json +++ b/tests/mock_endpoints_generated/api.pub.v2.pricing.rentals.json @@ -1,7 +1,7 @@ { "post": { "response": { - "serviceName": "myservice", + "serviceName": "hotmail", "price": 5.0 } } diff --git a/tests/mock_endpoints_generated/api.pub.v2.pricing.verifications.json b/tests/mock_endpoints_generated/api.pub.v2.pricing.verifications.json index 3df4759..8479e06 100644 --- a/tests/mock_endpoints_generated/api.pub.v2.pricing.verifications.json +++ b/tests/mock_endpoints_generated/api.pub.v2.pricing.verifications.json @@ -1,7 +1,7 @@ { "post": { "response": { - "serviceName": "myservice", + "serviceName": "hotmail", "price": 5.0 } } diff --git a/tests/mock_endpoints_generated/api.pub.v2.reservations.rental.json b/tests/mock_endpoints_generated/api.pub.v2.reservations.rental.json index 4a64f42..32c10d0 100644 --- a/tests/mock_endpoints_generated/api.pub.v2.reservations.rental.json +++ b/tests/mock_endpoints_generated/api.pub.v2.reservations.rental.json @@ -1,8 +1,8 @@ { "post": { "response": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." } } } \ No newline at end of file diff --git a/tests/mock_endpoints_generated/api.pub.v2.reservations.rental.nonrenewable.json b/tests/mock_endpoints_generated/api.pub.v2.reservations.rental.nonrenewable.json index 641ea33..500fb06 100644 --- a/tests/mock_endpoints_generated/api.pub.v2.reservations.rental.nonrenewable.json +++ b/tests/mock_endpoints_generated/api.pub.v2.reservations.rental.nonrenewable.json @@ -6,8 +6,8 @@ "createdAt": "1970-01-01T00:00:00+00:00", "id": "string", "sale": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, "saleId": "string", "serviceName": "string", @@ -19,8 +19,8 @@ "createdAt": "1970-01-01T00:00:00+00:00", "id": "string", "sale": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, "saleId": "string", "serviceName": "string", @@ -34,8 +34,8 @@ "count": 0, "links": { "next": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." } } } diff --git a/tests/mock_endpoints_generated/api.pub.v2.reservations.rental.nonrenewable.{id}.json b/tests/mock_endpoints_generated/api.pub.v2.reservations.rental.nonrenewable.{id}.json index 840209d..5cbe506 100644 --- a/tests/mock_endpoints_generated/api.pub.v2.reservations.rental.nonrenewable.{id}.json +++ b/tests/mock_endpoints_generated/api.pub.v2.reservations.rental.nonrenewable.{id}.json @@ -5,8 +5,8 @@ }, "response": { "sms": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, "createdAt": "1970-01-01T00:00:00+00:00", "endsAt": "1970-01-01T00:00:00+00:00", @@ -14,8 +14,8 @@ "refund": { "canRefund": false, "link": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, "refundableUntil": "1970-01-01T00:00:00+00:00" }, diff --git a/tests/mock_endpoints_generated/api.pub.v2.reservations.rental.renewable.json b/tests/mock_endpoints_generated/api.pub.v2.reservations.rental.renewable.json index 2d13a03..a2d1382 100644 --- a/tests/mock_endpoints_generated/api.pub.v2.reservations.rental.renewable.json +++ b/tests/mock_endpoints_generated/api.pub.v2.reservations.rental.renewable.json @@ -9,30 +9,30 @@ "createdAt": "1970-01-01T00:00:00+00:00", "id": "string", "billingCycle": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, "saleId": "string", - "serviceName": "string", + "serviceName": "yahoo", "state": "verificationPending", "billingCycleId": "string", "isIncludedForNextRenewal": false, - "number": "string", + "number": "2223334444", "alwaysOn": false }, { "createdAt": "1970-01-01T00:00:00+00:00", "id": "string", "billingCycle": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, "saleId": "string", - "serviceName": "string", + "serviceName": "yahoo", "state": "verificationPending", "billingCycleId": "string", "isIncludedForNextRenewal": false, - "number": "string", + "number": "2223334444", "alwaysOn": false } ], @@ -41,8 +41,8 @@ "count": 0, "links": { "next": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." } } } diff --git a/tests/mock_endpoints_generated/api.pub.v2.reservations.rental.renewable.{id}.json b/tests/mock_endpoints_generated/api.pub.v2.reservations.rental.renewable.{id}.json index ecfec62..3435a24 100644 --- a/tests/mock_endpoints_generated/api.pub.v2.reservations.rental.renewable.{id}.json +++ b/tests/mock_endpoints_generated/api.pub.v2.reservations.rental.renewable.{id}.json @@ -5,16 +5,16 @@ }, "response": { "billingCycle": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, "createdAt": "1970-01-01T00:00:00+00:00", "id": "string", "refund": { "canRefund": false, "link": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, "refundableUntil": "1970-01-01T00:00:00+00:00" }, @@ -23,7 +23,7 @@ "state": "verificationPending", "billingCycleId": "string", "isIncludedForNextRenewal": false, - "number": "string", + "number": "2223334444", "alwaysOn": false } }, diff --git a/tests/mock_endpoints_generated/api.pub.v2.reservations.{id}.health.json b/tests/mock_endpoints_generated/api.pub.v2.reservations.{id}.health.json index b11f437..a2db518 100644 --- a/tests/mock_endpoints_generated/api.pub.v2.reservations.{id}.health.json +++ b/tests/mock_endpoints_generated/api.pub.v2.reservations.{id}.health.json @@ -4,7 +4,7 @@ "id": "string" }, "response": { - "lineNumber": "string", + "lineNumber": "2223334444", "checkedAt": "1970-01-01T00:00:00+00:00" } } diff --git a/tests/mock_endpoints_generated/api.pub.v2.reservations.{id}.json b/tests/mock_endpoints_generated/api.pub.v2.reservations.{id}.json index 772f6c0..b5206d3 100644 --- a/tests/mock_endpoints_generated/api.pub.v2.reservations.{id}.json +++ b/tests/mock_endpoints_generated/api.pub.v2.reservations.{id}.json @@ -7,8 +7,8 @@ "type": "renewable" }, "response": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." } } } \ No newline at end of file diff --git a/tests/mock_endpoints_generated/api.pub.v2.sales.json b/tests/mock_endpoints_generated/api.pub.v2.sales.json index bc2a3f8..329b0d9 100644 --- a/tests/mock_endpoints_generated/api.pub.v2.sales.json +++ b/tests/mock_endpoints_generated/api.pub.v2.sales.json @@ -6,22 +6,22 @@ "createdAt": "1970-01-01T00:00:00+00:00", "id": "string", "link": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, "state": "created", - "totalCost": 0.0, + "totalCost": 0.95, "updatedAt": "1970-01-01T00:00:00+00:00" }, { "createdAt": "1970-01-01T00:00:00+00:00", "id": "string", "link": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, "state": "created", - "totalCost": 0.0, + "totalCost": 0.95, "updatedAt": "1970-01-01T00:00:00+00:00" } ], @@ -30,8 +30,8 @@ "count": 0, "links": { "next": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." } } } diff --git a/tests/mock_endpoints_generated/api.pub.v2.sales.{id}.json b/tests/mock_endpoints_generated/api.pub.v2.sales.{id}.json index 0e4fc5e..d4ace11 100644 --- a/tests/mock_endpoints_generated/api.pub.v2.sales.{id}.json +++ b/tests/mock_endpoints_generated/api.pub.v2.sales.{id}.json @@ -9,20 +9,20 @@ "backOrderReservations": [ { "link": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, "id": "string", - "serviceName": "string", + "serviceName": "yahoo", "status": "created" }, { "link": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, "id": "string", - "serviceName": "string", + "serviceName": "yahoo", "status": "created" } ], @@ -30,24 +30,24 @@ { "id": "string", "link": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, "reservationType": "renewable", - "serviceName": "string" + "serviceName": "yahoo" }, { "id": "string", "link": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, "reservationType": "renewable", - "serviceName": "string" + "serviceName": "yahoo" } ], "state": "created", - "total": 0.0, + "total": 5.25, "updatedAt": "1970-01-01T00:00:00+00:00" } } diff --git a/tests/mock_endpoints_generated/api.pub.v2.services.json b/tests/mock_endpoints_generated/api.pub.v2.services.json index 37fcfae..0597cd4 100644 --- a/tests/mock_endpoints_generated/api.pub.v2.services.json +++ b/tests/mock_endpoints_generated/api.pub.v2.services.json @@ -6,11 +6,11 @@ }, "response": [ { - "serviceName": "myservice", + "serviceName": "gmail", "capability": "sms" }, { - "serviceName": "myservice", + "serviceName": "gmail", "capability": "sms" } ] diff --git a/tests/mock_endpoints_generated/api.pub.v2.sms.json b/tests/mock_endpoints_generated/api.pub.v2.sms.json index de99076..690a735 100644 --- a/tests/mock_endpoints_generated/api.pub.v2.sms.json +++ b/tests/mock_endpoints_generated/api.pub.v2.sms.json @@ -31,8 +31,8 @@ "count": 0, "links": { "next": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." } } } diff --git a/tests/mock_endpoints_generated/api.pub.v2.verifications.json b/tests/mock_endpoints_generated/api.pub.v2.verifications.json index b3e3f01..957e66c 100644 --- a/tests/mock_endpoints_generated/api.pub.v2.verifications.json +++ b/tests/mock_endpoints_generated/api.pub.v2.verifications.json @@ -1,8 +1,8 @@ { "post": { "response": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." } }, "get": { @@ -12,24 +12,24 @@ "createdAt": "1970-01-01T00:00:00+00:00", "id": "string", "calls": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, "serviceName": "string", "state": "verificationPending", - "totalCost": 0.0, + "totalCost": 0.95, "number": "string" }, { "createdAt": "1970-01-01T00:00:00+00:00", "id": "string", "calls": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, "serviceName": "string", "state": "verificationPending", - "totalCost": 0.0, + "totalCost": 0.95, "number": "string" } ], @@ -38,8 +38,8 @@ "count": 0, "links": { "next": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." } } } diff --git a/tests/mock_endpoints_generated/api.pub.v2.verifications.{id}.json b/tests/mock_endpoints_generated/api.pub.v2.verifications.{id}.json index 57ff426..e97f608 100644 --- a/tests/mock_endpoints_generated/api.pub.v2.verifications.{id}.json +++ b/tests/mock_endpoints_generated/api.pub.v2.verifications.{id}.json @@ -6,8 +6,8 @@ "response": { "number": "string", "sale": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, "createdAt": "1970-01-01T00:00:00+00:00", "endsAt": "1970-01-01T00:00:00+00:00", @@ -15,34 +15,34 @@ "cancel": { "canCancel": false, "link": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." } }, "reactivate": { "canReactivate": false, "link": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." } }, "report": { "canReport": false, "link": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." } }, "reuse": { "link": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." }, "reusableUntil": "1970-01-01T00:00:00+00:00" }, "serviceName": "string", "state": "verificationPending", - "totalCost": 0.0 + "totalCost": 0.95 } } } \ No newline at end of file diff --git a/tests/mock_endpoints_generated/api.pub.v2.verifications.{id}.reactivate.json b/tests/mock_endpoints_generated/api.pub.v2.verifications.{id}.reactivate.json index 36860ff..2b0d51b 100644 --- a/tests/mock_endpoints_generated/api.pub.v2.verifications.{id}.reactivate.json +++ b/tests/mock_endpoints_generated/api.pub.v2.verifications.{id}.reactivate.json @@ -4,8 +4,8 @@ "id": "string" }, "response": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." } } } \ No newline at end of file diff --git a/tests/mock_endpoints_generated/api.pub.v2.verifications.{id}.reuse.json b/tests/mock_endpoints_generated/api.pub.v2.verifications.{id}.reuse.json index 36860ff..2b0d51b 100644 --- a/tests/mock_endpoints_generated/api.pub.v2.verifications.{id}.reuse.json +++ b/tests/mock_endpoints_generated/api.pub.v2.verifications.{id}.reuse.json @@ -4,8 +4,8 @@ "id": "string" }, "response": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." } } } \ No newline at end of file diff --git a/tests/mock_endpoints_generated/api.pub.v2.wake-requests.json b/tests/mock_endpoints_generated/api.pub.v2.wake-requests.json index 4a64f42..32c10d0 100644 --- a/tests/mock_endpoints_generated/api.pub.v2.wake-requests.json +++ b/tests/mock_endpoints_generated/api.pub.v2.wake-requests.json @@ -1,8 +1,8 @@ { "post": { "response": { - "method": "string", - "href": "string" + "method": "GET|POST", + "href": "https://...." } } } \ No newline at end of file From 721f0f1dc7027714b8ad6ecf9bf9bffd0887a0c9 Mon Sep 17 00:00:00 2001 From: Leon Leibmann Date: Tue, 5 Aug 2025 19:01:06 -0700 Subject: [PATCH 04/11] format --- tests/test_calls.py | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/tests/test_calls.py b/tests/test_calls.py index 03c791d..e044942 100644 --- a/tests/test_calls.py +++ b/tests/test_calls.py @@ -34,12 +34,6 @@ def test_list_calls_by_to_number(tv, mock_http_from_disk): calls_list = tv.calls.list(to_number="+1234567890") call_messages = [x.to_api() for x in calls_list] - print("truth", mock_http_from_disk.last_response["data"][0]) - print("test", call_messages[0]) - print([ - dict_subset(call_test, call_truth) - for call_test, call_truth in zip(call_messages, mock_http_from_disk.last_response["data"]) - ]) assert all( dict_subset(call_test, call_truth) is None for call_test, call_truth in zip(call_messages, mock_http_from_disk.last_response["data"]) @@ -141,7 +135,7 @@ def test_list_calls_with_reservation_object(tv, mock_http_from_disk): reservation_type=ReservationType.RENEWABLE, service_name="allservices", ) - + calls_list = tv.calls.list(data=reservation) call_messages = [x.to_api() for x in calls_list] @@ -164,7 +158,7 @@ def test_list_calls_error_data_and_to_number(tv): number="+1234567890", always_on=True, ) - + with pytest.raises(ValueError, match="Cannot specify both rental/verification data and to_number"): tv.calls.list(data=rental, to_number="+0987654321") @@ -182,7 +176,7 @@ def test_list_calls_error_reservation_type_with_data(tv): number="+1234567890", always_on=True, ) - + with pytest.raises(ValueError, match="Cannot specify reservation_type when using a rental or verification object"): tv.calls.list(data=rental, reservation_type=ReservationType.RENEWABLE) @@ -190,9 +184,9 @@ def test_list_calls_error_reservation_type_with_data(tv): def test_open_call_session_with_reservation_id(tv, mock_http_from_disk): """Test opening a call session with a reservation ID string.""" reservation_id = "reservation_123" - + twilio_context = tv.calls.open_call_session(reservation_id) - + assert isinstance(twilio_context, TwilioCallingContextDto) # Verify the request was made with correct reservation ID assert mock_http_from_disk.last_body_params["reservationId"] == reservation_id @@ -201,7 +195,7 @@ def test_open_call_session_with_reservation_id(tv, mock_http_from_disk): def test_open_call_session_with_renewable_rental_compact(tv, mock_http_from_disk, renewable_rental_compact): """Test opening a call session with a RenewableRentalCompact object.""" twilio_context = tv.calls.open_call_session(renewable_rental_compact) - + assert isinstance(twilio_context, TwilioCallingContextDto) # Verify the request was made with correct reservation ID assert mock_http_from_disk.last_body_params["reservationId"] == renewable_rental_compact.id @@ -210,7 +204,7 @@ def test_open_call_session_with_renewable_rental_compact(tv, mock_http_from_disk def test_open_call_session_with_renewable_rental_expanded(tv, mock_http_from_disk, renewable_rental_expanded): """Test opening a call session with a RenewableRentalExpanded object.""" twilio_context = tv.calls.open_call_session(renewable_rental_expanded) - + assert isinstance(twilio_context, TwilioCallingContextDto) # Verify the request was made with correct reservation ID assert mock_http_from_disk.last_body_params["reservationId"] == renewable_rental_expanded.id @@ -219,7 +213,7 @@ def test_open_call_session_with_renewable_rental_expanded(tv, mock_http_from_dis def test_open_call_session_with_nonrenewable_rental_compact(tv, mock_http_from_disk, nonrenewable_rental_compact): """Test opening a call session with a NonrenewableRentalCompact object.""" twilio_context = tv.calls.open_call_session(nonrenewable_rental_compact) - + assert isinstance(twilio_context, TwilioCallingContextDto) # Verify the request was made with correct reservation ID assert mock_http_from_disk.last_body_params["reservationId"] == nonrenewable_rental_compact.id @@ -228,7 +222,7 @@ def test_open_call_session_with_nonrenewable_rental_compact(tv, mock_http_from_d def test_open_call_session_with_nonrenewable_rental_expanded(tv, mock_http_from_disk, nonrenewable_rental_expanded): """Test opening a call session with a NonrenewableRentalExpanded object.""" twilio_context = tv.calls.open_call_session(nonrenewable_rental_expanded) - + assert isinstance(twilio_context, TwilioCallingContextDto) # Verify the request was made with correct reservation ID assert mock_http_from_disk.last_body_params["reservationId"] == nonrenewable_rental_expanded.id @@ -237,7 +231,7 @@ def test_open_call_session_with_nonrenewable_rental_expanded(tv, mock_http_from_ def test_open_call_session_with_verification_compact(tv, mock_http_from_disk, verification_compact): """Test opening a call session with a VerificationCompact object.""" twilio_context = tv.calls.open_call_session(verification_compact) - + assert isinstance(twilio_context, TwilioCallingContextDto) # Verify the request was made with correct reservation ID assert mock_http_from_disk.last_body_params["reservationId"] == verification_compact.id @@ -246,7 +240,7 @@ def test_open_call_session_with_verification_compact(tv, mock_http_from_disk, ve def test_open_call_session_with_verification_expanded(tv, mock_http_from_disk, verification_expanded): """Test opening a call session with a VerificationExpanded object.""" twilio_context = tv.calls.open_call_session(verification_expanded) - + assert isinstance(twilio_context, TwilioCallingContextDto) # Verify the request was made with correct reservation ID assert mock_http_from_disk.last_body_params["reservationId"] == verification_expanded.id @@ -259,9 +253,9 @@ def test_open_call_session_with_reservation_object(tv, mock_http_from_disk): reservation_type=ReservationType.RENEWABLE, service_name="allservices", ) - + twilio_context = tv.calls.open_call_session(reservation) - + assert isinstance(twilio_context, TwilioCallingContextDto) # Verify the request was made with correct reservation ID assert mock_http_from_disk.last_body_params["reservationId"] == reservation.id From 359fc2f94868148986bb5809202e155848d3a0d7 Mon Sep 17 00:00:00 2001 From: Leon Leibmann Date: Tue, 5 Aug 2025 19:12:05 -0700 Subject: [PATCH 05/11] correct isinstance check to catch empty strings --- textverified/billing_cycle_api.py | 8 ++++---- textverified/call_api.py | 4 ++-- textverified/reservations_api.py | 20 ++++++++++---------- textverified/sms_api.py | 2 +- textverified/verifications_api.py | 10 +++++----- textverified/wake_api.py | 6 +++--- 6 files changed, 25 insertions(+), 25 deletions(-) diff --git a/textverified/billing_cycle_api.py b/textverified/billing_cycle_api.py index 138b45d..f3ce269 100644 --- a/textverified/billing_cycle_api.py +++ b/textverified/billing_cycle_api.py @@ -79,7 +79,7 @@ def update( else BillingCycleUpdateRequest(reminders_enabled=reminders_enabled, nickname=nickname) ) - if not billing_cycle_id or not isinstance(billing_cycle_id, str): + if not isinstance(billing_cycle_id, str) or not billing_cycle_id.strip(): raise ValueError("billing_cycle must be a valid ID or instance of BillingCycleCompact/Expanded.") if not update_request or (not update_request.reminders_enabled and not update_request.nickname): @@ -111,7 +111,7 @@ def invoices( else billing_cycle_id ) - if not billing_cycle_id or not isinstance(billing_cycle_id, str): + if not isinstance(billing_cycle_id, str) or not billing_cycle_id.strip(): raise ValueError("billing_cycle_id must be a valid ID or instance of BillingCycleCompact/Expanded.") action = _Action(method="GET", href=f"/api/pub/v2/billing-cycles/{billing_cycle_id}/invoices") @@ -142,7 +142,7 @@ def preview( else billing_cycle_id ) - if not billing_cycle_id or not isinstance(billing_cycle_id, str): + if not isinstance(billing_cycle_id, str) or not billing_cycle_id.strip(): raise ValueError("billing_cycle_id must be a valid ID or instance of BillingCycleCompact/Expanded.") action = _Action(method="POST", href=f"/api/pub/v2/billing-cycles/{billing_cycle_id}/next-invoice") @@ -169,7 +169,7 @@ def renew(self, billing_cycle_id: Union[str, BillingCycleCompact, BillingCycleEx else billing_cycle_id ) - if not billing_cycle_id or not isinstance(billing_cycle_id, str): + if not isinstance(billing_cycle_id, str) or not billing_cycle_id.strip(): raise ValueError("billing_cycle_id must be a valid ID or instance of BillingCycleCompact/Expanded.") action = _Action(method="POST", href=f"/api/pub/v2/billing-cycles/{billing_cycle_id}/renew") diff --git a/textverified/call_api.py b/textverified/call_api.py index 691f4bf..2579abf 100644 --- a/textverified/call_api.py +++ b/textverified/call_api.py @@ -66,7 +66,7 @@ def list( VerificationExpanded, ), ): - if hasattr(data, "number") and to_number: + if to_number: raise ValueError("Cannot specify both rental/verification data and to_number.") to_number = data.number @@ -156,7 +156,7 @@ def open_call_session( else reservation ) - if not reservation_id or not isinstance(reservation_id, str): + if not isinstance(reservation_id, str) or not reservation_id.strip(): raise ValueError("reservation_id must be a valid ID or instance of Reservation/Verification.") action = _Action(method="POST", href="/api/pub/v2/calls/access-token") diff --git a/textverified/reservations_api.py b/textverified/reservations_api.py index 92bc3dc..54d0c2c 100644 --- a/textverified/reservations_api.py +++ b/textverified/reservations_api.py @@ -244,7 +244,7 @@ def backorder( else reservation_id ) - if not reservation_id or not isinstance(reservation_id, str): + if not isinstance(reservation_id, str) or not reservation_id.strip(): raise ValueError("reservation_id must be a valid ID or instance of BackOrderReservationCompact/Expanded.") action = _Action(method="GET", href=f"/api/pub/v2/backorders/{reservation_id}") @@ -288,7 +288,7 @@ def details( else reservation_id ) - if not reservation_id or not isinstance(reservation_id, str): + if not isinstance(reservation_id, str) or not reservation_id.strip(): raise ValueError("reservation_id must be a valid ID or instance of Reservation.") action = _Action(method="GET", href=f"/api/pub/v2/reservations/{reservation_id}") @@ -351,7 +351,7 @@ def renewable_details( else reservation_id ) - if not reservation_id or not isinstance(reservation_id, str): + if not isinstance(reservation_id, str) or not reservation_id.strip(): raise ValueError("reservation_id must be a valid ID or instance of RenewableRentalCompact/Expanded.") action = _Action(method="GET", href=f"/api/pub/v2/reservations/rental/renewable/{reservation_id}") @@ -379,7 +379,7 @@ def nonrenewable_details( else reservation_id ) - if not reservation_id or not isinstance(reservation_id, str): + if not isinstance(reservation_id, str) or not reservation_id.strip(): raise ValueError("reservation_id must be a valid ID or instance of NonrenewableRentalCompact/Expanded.") action = _Action(method="GET", href=f"/api/pub/v2/reservations/rental/nonrenewable/{reservation_id}") @@ -424,7 +424,7 @@ def check_health( else reservation_id ) - if not reservation_id or not isinstance(reservation_id, str): + if not isinstance(reservation_id, str) or not reservation_id.strip(): raise ValueError( "reservation_id must be a valid ID or instance of RenewableRentalCompact/Expanded or NonrenewableRentalCompact/Expanded." ) @@ -464,7 +464,7 @@ def update_renewable( else reservation_id ) - if not reservation_id or not isinstance(reservation_id, str): + if not isinstance(reservation_id, str) or not reservation_id.strip(): raise ValueError("reservation_id must be a valid ID or instance of RenewableRentalCompact/Expanded.") update_request = ( @@ -525,7 +525,7 @@ def update_nonrenewable( else reservation_id ) - if not reservation_id or not isinstance(reservation_id, str): + if not isinstance(reservation_id, str) or not reservation_id.strip(): raise ValueError("reservation_id must be a valid ID or instance of NonrenewableRentalCompact/Expanded.") update_request = ( @@ -563,7 +563,7 @@ def refund_renewable(self, reservation_id: Union[str, RenewableRentalCompact, Re else reservation_id ) - if not reservation_id or not isinstance(reservation_id, str): + if not isinstance(reservation_id, str) or not reservation_id.strip(): raise ValueError("reservation_id must be a valid ID or instance of RenewableRentalCompact/Expanded.") action = _Action(method="POST", href=f"/api/pub/v2/reservations/rental/renewable/{reservation_id}/refund") @@ -591,7 +591,7 @@ def refund_nonrenewable( else reservation_id ) - if not reservation_id or not isinstance(reservation_id, str): + if not isinstance(reservation_id, str) or not reservation_id.strip(): raise ValueError("reservation_id must be a valid ID or instance of NonrenewableRentalCompact/Expanded.") action = _Action(method="POST", href=f"/api/pub/v2/reservations/rental/nonrenewable/{reservation_id}/refund") @@ -622,7 +622,7 @@ def renew_overdue(self, reservation_id: Union[str, RenewableRentalCompact, Renew else reservation_id ) - if not reservation_id or not isinstance(reservation_id, str): + if not isinstance(reservation_id, str) or not reservation_id.strip(): raise ValueError("reservation_id must be a valid ID or instance of RenewableRentalCompact/Expanded.") action = _Action(method="POST", href=f"/api/pub/v2/reservations/rental/renewable/{reservation_id}/renew") diff --git a/textverified/sms_api.py b/textverified/sms_api.py index 3ec21d1..de357a2 100644 --- a/textverified/sms_api.py +++ b/textverified/sms_api.py @@ -71,7 +71,7 @@ def list( VerificationExpanded, ), ): - if hasattr(data, "number") and to_number: + if to_number: raise ValueError("Cannot specify both rental/verification data and to_number.") to_number = data.number diff --git a/textverified/verifications_api.py b/textverified/verifications_api.py index 7925b8a..ec9f7c1 100644 --- a/textverified/verifications_api.py +++ b/textverified/verifications_api.py @@ -190,7 +190,7 @@ def details(self, verification_id: Union[str, VerificationCompact, VerificationE else verification_id ) - if not verification_id or not isinstance(verification_id, str): + if not isinstance(verification_id, str) or not verification_id.strip(): raise ValueError("verification_id must be a valid ID or instance of VerificationCompact/Expanded.") action = _Action(method="GET", href=f"/api/pub/v2/verifications/{verification_id}") @@ -233,7 +233,7 @@ def cancel(self, verification_id: Union[str, VerificationCompact, VerificationEx else verification_id ) - if not verification_id or not isinstance(verification_id, str): + if not isinstance(verification_id, str) or not verification_id.strip(): raise ValueError("verification_id must be a valid ID or instance of VerificationCompact/Expanded.") action = _Action(method="POST", href=f"/api/pub/v2/verifications/{verification_id}/cancel") @@ -266,7 +266,7 @@ def reactivate(self, verification_id: Union[str, VerificationCompact, Verificati else verification_id ) - if not verification_id or not isinstance(verification_id, str): + if not isinstance(verification_id, str) or not verification_id.strip(): raise ValueError("verification_id must be a valid ID or instance of VerificationCompact/Expanded.") action = _Action(method="POST", href=f"/api/pub/v2/verifications/{verification_id}/reactivate") @@ -299,7 +299,7 @@ def reuse(self, verification_id: Union[str, VerificationCompact, VerificationExp else verification_id ) - if not verification_id or not isinstance(verification_id, str): + if not isinstance(verification_id, str) or not verification_id.strip(): raise ValueError("verification_id must be a valid ID or instance of VerificationCompact/Expanded.") action = _Action(method="POST", href=f"/api/pub/v2/verifications/{verification_id}/reuse") @@ -329,7 +329,7 @@ def report(self, verification_id: Union[str, VerificationCompact, VerificationEx else verification_id ) - if not verification_id or not isinstance(verification_id, str): + if not isinstance(verification_id, str) or not verification_id.strip(): raise ValueError("verification_id must be a valid ID or instance of VerificationCompact/Expanded.") action = _Action(method="POST", href=f"/api/pub/v2/verifications/{verification_id}/report") diff --git a/textverified/wake_api.py b/textverified/wake_api.py index 48589a5..9f30bc7 100644 --- a/textverified/wake_api.py +++ b/textverified/wake_api.py @@ -61,7 +61,7 @@ def create( else reservation_id ) - if not reservation_id or not isinstance(reservation_id, str): + if not isinstance(reservation_id, str) or not reservation_id.strip(): raise ValueError("reservation_id must be a valid ID or instance of RenewableRentalCompact/Expanded.") # Actually takes in a WakeRequest, may need to change this later if API spec changes @@ -90,7 +90,7 @@ def get(self, wake_request_id: Union[str, WakeResponse]) -> WakeResponse: """ wake_request_id = wake_request_id.id if isinstance(wake_request_id, WakeResponse) else wake_request_id - if not wake_request_id or not isinstance(wake_request_id, str): + if not isinstance(wake_request_id, str) or not wake_request_id.strip(): raise ValueError("wake_request_id must be a valid ID or instance of WakeResponse.") action = _Action(method="GET", href=f"/api/pub/v2/wake-requests/{wake_request_id}") @@ -138,7 +138,7 @@ def estimate_usage_window( else reservation_id ) - if not reservation_id or not isinstance(reservation_id, str): + if not isinstance(reservation_id, str) or not reservation_id.strip(): raise ValueError("reservation_id must be a valid ID or instance of RenewableRentalCompact/Expanded.") action = _Action(method="POST", href="/api/pub/v2/wake-requests/estimate") From 21d1ac3e54499c565c62d81e4b5be096d1b6b134 Mon Sep 17 00:00:00 2001 From: Leon Leibmann Date: Thu, 25 Sep 2025 13:19:55 -0700 Subject: [PATCH 06/11] correct error state --- textverified/reservations_api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/textverified/reservations_api.py b/textverified/reservations_api.py index 54d0c2c..1f58565 100644 --- a/textverified/reservations_api.py +++ b/textverified/reservations_api.py @@ -304,6 +304,8 @@ def details( elif "reservations/rental/renewable/" in action.href: return RenewableRentalExpanded.from_api(response.data) + + raise ValueError("Unexpected reservation type in response.") def list_renewable(self) -> PaginatedList[RenewableRentalCompact]: """Get a paginated list of all renewable reservations associated with this account. From 3f707afc7bf39798a77bfef7b440595f7d450dec Mon Sep 17 00:00:00 2001 From: Leon Leibmann Date: Fri, 26 Sep 2025 12:58:35 -0700 Subject: [PATCH 07/11] fixtures correctly support path params --- tests/fixtures.py | 19 +++++++++- ...reservations.rental.nonrenewable.{id}.json | 35 ++++++++++++++++++ ...v2.reservations.rental.renewable.{id}.json | 36 +++++++++++++++++++ .../api.pub.v2.reservations.{id}.json | 14 ++++++++ 4 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 tests/mock_endpoints/api.pub.v2.reservations.rental.nonrenewable.{id}.json create mode 100644 tests/mock_endpoints/api.pub.v2.reservations.rental.renewable.{id}.json create mode 100644 tests/mock_endpoints/api.pub.v2.reservations.{id}.json diff --git a/tests/fixtures.py b/tests/fixtures.py index c7b600e..e6be0ef 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -272,7 +272,7 @@ def _find_mock_file(request_path: str, file_list: List[Path]) -> Optional[Dict]: file_name = file_name.replace(".", "\\.") # Escape dots for regex path_param_keys = re.findall(r"\{([^}/?&]+)\}", str(file_name)) # 1 group per key of path parameters file_path_pattern = ( - "^" + re.sub(r"\{([^}/?&]+)\}", r"[^\/}?&.]+", str(file_name)) + "$" + "^" + re.sub(r"\{([^}/?&]+)\}", r"([^\/}?&.]+)", str(file_name)) + "$" ) # file path with 1 group for each path parameter if re.fullmatch(file_path_pattern, request_filename): @@ -301,6 +301,18 @@ def _load_mock_data(mock_file_path, method): return mock_data[method_lower] +def _replace_placeholders(obj, replacements): + if isinstance(obj, dict): + for key in obj: + obj[key] = _replace_placeholders(obj[key], replacements) + elif isinstance(obj, list): + for i in range(len(obj)): + obj[i] = _replace_placeholders(obj[i], replacements) + elif isinstance(obj, str): + for placeholder, value in replacements.items(): + obj = obj.replace(placeholder, value) + return obj + @pytest.fixture def mock_http_from_disk(): """ @@ -336,6 +348,11 @@ def mock_request(method, url, **kwargs): # Load mock data response_data = _load_mock_data(mock_file_data.get("path"), method) + # Replace parameters in response + if "response" in response_data: + replacements = {f"{{{k}}}": v for k, v in mock_file_data.get("path_params", {}).items()} + response_data["response"] = _replace_placeholders(response_data["response"], replacements) + # Apply hooks if any for hook in mock.hooks: response_data["response"] = hook(response_data["response"], method, url, **kwargs) diff --git a/tests/mock_endpoints/api.pub.v2.reservations.rental.nonrenewable.{id}.json b/tests/mock_endpoints/api.pub.v2.reservations.rental.nonrenewable.{id}.json new file mode 100644 index 0000000..a8952bb --- /dev/null +++ b/tests/mock_endpoints/api.pub.v2.reservations.rental.nonrenewable.{id}.json @@ -0,0 +1,35 @@ +{ + "get": { + "path_params": { + "id": "string" + }, + "response": { + "sms": { + "method": "GET", + "href": "/api/pub/v2/sms" + }, + "createdAt": "1970-01-01T00:00:00+00:00", + "endsAt": "1970-01-01T00:00:00+00:00", + "id": "string", + "refund": { + "canRefund": false, + "link": { + "method": "POST", + "href": "/api/pub/v2/reservations/rental/nonrenewable/{id}/refund" + }, + "refundableUntil": "1970-01-01T00:00:00+00:00" + }, + "saleId": "string", + "serviceName": "string", + "state": "verificationPending", + "number": "string", + "alwaysOn": false + } + }, + "post": { + "path_params": { + "id": "string" + }, + "response": {} + } +} \ No newline at end of file diff --git a/tests/mock_endpoints/api.pub.v2.reservations.rental.renewable.{id}.json b/tests/mock_endpoints/api.pub.v2.reservations.rental.renewable.{id}.json new file mode 100644 index 0000000..a81eff4 --- /dev/null +++ b/tests/mock_endpoints/api.pub.v2.reservations.rental.renewable.{id}.json @@ -0,0 +1,36 @@ +{ + "get": { + "path_params": { + "id": "string" + }, + "response": { + "billingCycle": { + "method": "GET", + "href": "/api/pub/v2/billing-cycles/{id}" + }, + "createdAt": "1970-01-01T00:00:00+00:00", + "id": "string", + "refund": { + "canRefund": false, + "link": { + "method": "POST", + "href": "/api/pub/v2/reservations/rental/renewable/{id}/refund" + }, + "refundableUntil": "1970-01-01T00:00:00+00:00" + }, + "saleId": "string", + "serviceName": "string", + "state": "verificationPending", + "billingCycleId": "string", + "isIncludedForNextRenewal": false, + "number": "2223334444", + "alwaysOn": false + } + }, + "post": { + "path_params": { + "id": "string" + }, + "response": {} + } +} \ No newline at end of file diff --git a/tests/mock_endpoints/api.pub.v2.reservations.{id}.json b/tests/mock_endpoints/api.pub.v2.reservations.{id}.json new file mode 100644 index 0000000..3a7ffeb --- /dev/null +++ b/tests/mock_endpoints/api.pub.v2.reservations.{id}.json @@ -0,0 +1,14 @@ +{ + "get": { + "path_params": { + "id": "string" + }, + "query_params": { + "type": "renewable" + }, + "response": { + "method": "GET", + "href": "/api/pub/v2/reservations/rental/nonrenewable/{id}" + } + } +} \ No newline at end of file From 11327a7258cb2bb8b4111d0abcfea66e842df662 Mon Sep 17 00:00:00 2001 From: Leon Leibmann Date: Fri, 26 Sep 2025 12:58:52 -0700 Subject: [PATCH 08/11] implement mocking portion of api --- tests/test_mocking.py | 169 +++++++++++++++++++++++++++ textverified/__init__.py | 21 ++++ textverified/mocking.py | 218 +++++++++++++++++++++++++++++++++++ textverified/textverified.py | 15 ++- 4 files changed, 421 insertions(+), 2 deletions(-) create mode 100644 tests/test_mocking.py create mode 100644 textverified/mocking.py diff --git a/tests/test_mocking.py b/tests/test_mocking.py new file mode 100644 index 0000000..594cfb1 --- /dev/null +++ b/tests/test_mocking.py @@ -0,0 +1,169 @@ +import pytest +from .fixtures import tv, mock_http_from_disk +from textverified.mocking import Mocking, MockBehavior, MockReceivePolicy, MockObject +from textverified.data import ( + VerificationExpanded, + ReservationSaleExpanded, + BillingCycleExpanded, + WakeResponse, + RenewableRentalExpanded, + NonrenewableRentalExpanded, + BackOrderReservationExpanded, +) + + +def test_target_returns_string_with_separator(tv): + """Test that target() returns a string containing '_$_'""" + result = tv.mocking.target() + assert isinstance(result, str) + assert "_$_" in result + + +def test_service_returns_string_with_separator(tv): + """Test that service() returns a string containing '_$_'""" + result = tv.mocking.service() + assert isinstance(result, str) + assert "_$_" in result + + +def test_extension_returns_mock_object_with_separator(tv): + """Test that extension() returns a MockObject containing '_$_'""" + mock_obj = tv.mocking.extension() + assert isinstance(mock_obj, MockObject) + assert "_$_" in mock_obj.id + + +def test_verification_returns_mock_object(tv, mock_http_from_disk): + """Test that verification() returns a MockObject that can get a VerificationExpanded""" + mock_obj = tv.mocking.verification() + assert isinstance(mock_obj, MockObject) + assert "_$_" in mock_obj.id + + # Get the verification details + verification = mock_obj.get() + assert isinstance(verification, VerificationExpanded) + + +def test_sale_returns_mock_object(tv, mock_http_from_disk): + """Test that sale() returns a MockObject that can get a ReservationSaleExpanded""" + mock_obj = tv.mocking.sale() + assert isinstance(mock_obj, MockObject) + assert "_$_" in mock_obj.id + + # Get the sale details + sale = mock_obj.get() + assert isinstance(sale, ReservationSaleExpanded) + + +def test_billing_cycle_returns_mock_object(tv, mock_http_from_disk): + """Test that billing_cycle() returns a MockObject that can get a BillingCycleExpanded""" + mock_obj = tv.mocking.billing_cycle() + assert isinstance(mock_obj, MockObject) + assert "_$_" in mock_obj.id + + # Get the billing cycle details + billing_cycle = mock_obj.get() + assert isinstance(billing_cycle, BillingCycleExpanded) + + +def test_wake_request_returns_mock_object(tv, mock_http_from_disk): + """Test that wake_request() returns a MockObject that can get a WakeResponse""" + mock_obj = tv.mocking.wake_request() + assert isinstance(mock_obj, MockObject) + assert "_$_" in mock_obj.id + + # Get the wake request details + wake_response = mock_obj.get() + assert isinstance(wake_response, WakeResponse) + + +def test_reservation_returns_mock_object(tv, mock_http_from_disk): + """Test that reservation() returns a MockObject that can get rental details""" + mock_obj = tv.mocking.reservation() + assert isinstance(mock_obj, MockObject) + assert "_$_" in mock_obj.id + + # Get the reservation details + reservation = mock_obj.get() + assert isinstance(reservation, (RenewableRentalExpanded, NonrenewableRentalExpanded)) + + +def test_rental_returns_mock_object(tv, mock_http_from_disk): + """Test that rental() returns a MockObject that can get rental details""" + mock_obj = tv.mocking.rental() + assert isinstance(mock_obj, MockObject) + assert "_$_" in mock_obj.id + + # Get the rental details + rental = mock_obj.get() + assert isinstance(rental, (RenewableRentalExpanded, NonrenewableRentalExpanded)) + + +def test_backorder_sale_returns_mock_object(tv, mock_http_from_disk): + """Test that backorder_sale() returns a MockObject that can get backorder details""" + mock_obj = tv.mocking.backorder_sale() + assert isinstance(mock_obj, MockObject) + assert "_$_" in mock_obj.id + + # Get the backorder details + backorder = mock_obj.get() + assert isinstance(backorder, BackOrderReservationExpanded) + + +def test_mock_object_equality(): + """Test that MockObject equality works based on id""" + obj1 = MockObject(id="test_id", _get_function=lambda: "data") + obj2 = MockObject(id="test_id", _get_function=lambda: "other_data") + obj3 = MockObject(id="different_id", _get_function=lambda: "data") + + assert obj1 == obj2 + assert obj1 != obj3 + assert obj2 != obj3 + + +def test_mock_object_hash(): + """Test that MockObject hash works based on id""" + obj1 = MockObject(id="test_id", _get_function=lambda: "data") + obj2 = MockObject(id="test_id", _get_function=lambda: "other_data") + obj3 = MockObject(id="different_id", _get_function=lambda: "data") + + assert hash(obj1) == hash(obj2) + assert hash(obj1) != hash(obj3) + assert hash(obj2) != hash(obj3) + + +def test_mock_object_name_property(): + """Test that MockObject name property returns the id""" + obj = MockObject(id="test_id", _get_function=lambda: "data") + assert obj.name == "test_id" + + +def test_all_mock_ids_contain_separator(tv): + """Test that all mock ID generation methods produce IDs with '_$_' separator""" + methods_returning_strings = [ + tv.mocking.target, + tv.mocking.service + ] + + methods_returning_mock_objects = [ + tv.mocking.verification, + tv.mocking.sale, + tv.mocking.billing_cycle, + tv.mocking.wake_request, + tv.mocking.reservation, + tv.mocking.rental, + tv.mocking.backorder_sale, + tv.mocking.extension, + ] + + # Test string-returning methods + for method in methods_returning_strings: + result = method() + assert isinstance(result, str), f"{method.__name__} should return a string" + assert "_$_" in result, f"{method.__name__} should contain '_$_' in the result" + + # Test MockObject-returning methods + for method in methods_returning_mock_objects: + mock_obj = method() + assert isinstance(mock_obj, MockObject), f"{method.__name__} should return a MockObject" + assert "_$_" in mock_obj.id, f"{method.__name__} should have '_$_' in the id" \ No newline at end of file diff --git a/textverified/__init__.py b/textverified/__init__.py index 936bd53..ee0efce 100644 --- a/textverified/__init__.py +++ b/textverified/__init__.py @@ -277,6 +277,26 @@ def __call__(self, *args, **kwargs): """, ) +mocking = _LazyAPI( + "mocking", + """ +Static access to mocking functionality for testing. + +Provides methods for creating mock identifiers and objects for testing purposes. +This is a static wrapper around the Mocking class that uses the globally +configured TextVerified instance. + +Example: + from textverified import mocking + + # Create a mock verification + mock_verification = mocking.verification() + + # Get verification details + details = mock_verification.get() +""", +) + # Available for import: __all__ = [ # Main classes @@ -296,6 +316,7 @@ def __call__(self, *args, **kwargs): "wake_requests", "sms", "calls", + "mocking", # API classes (for direct instantiation if needed) "AccountAPI", "BillingCycleAPI", diff --git a/textverified/mocking.py b/textverified/mocking.py new file mode 100644 index 0000000..c8aa540 --- /dev/null +++ b/textverified/mocking.py @@ -0,0 +1,218 @@ +from .action import _ActionPerformer, _Action +from typing import List, Union, Generic, TypeVar, Callable +from enum import Enum +from .paginated_list import PaginatedList +from dataclasses import dataclass +from .data import ( + LineReservationType, + ReservationType, + Reservation, + NonrenewableRentalExpanded, + RenewableRentalExpanded, + VerificationExpanded, + ReservationSaleExpanded, + BillingCycleExpanded, + WakeResponse, + BackOrderReservationExpanded, + RentalExtensionRequest, + RentalDuration +) +from .verifications_api import VerificationsAPI +from .reservations_api import ReservationsAPI +from .billing_cycle_api import BillingCycleAPI +from .sales_api import SalesAPI +from .wake_api import WakeAPI + +class MockIdentifierType(Enum): + """Mock identifier types""" + MockWakeRequest = 1, + MockSale = 2, + MockBackorderReservation = 3, + # MockReservationRequest = 4, -- not needed for api calls + MockBillingCycle = 5, + MockReservation = 6, + MockVerification = 7, + MockRental = 8, + MockTarget = 9, + MockExtension = 10 + +class MockReceivePolicy(Enum): + """MockReceivePolicy: determines what should be returned when polling for messages or calls""" + NoIncoming = 0 + IncomingText = 1 + IncomingCall = 2 + IncomingTextAndCall = 3 + +class MockBehavior(Enum): + """MockBehavior: determines how the mock should behave. If set to FailRandomly, will fail 1/3 of the time.""" + Succeeds = 0 + AlwaysFails = 1 + FailsRandomly = 2 + +def _construct_mock(*args): + assert len(args) > 0 + return "_$_".join(str(arg) for arg in args) + +T = TypeVar('T') + +@dataclass(frozen=True) +class MockObject(Generic[T]): + """Base class for mock objects.""" + id: str + _get_function: Callable[[], T] + + @property + def name(self) -> str: + return self.id + + def get(self) -> T: + return self._get_function() + + def __eq__(self, value): + if not isinstance(value, MockObject): + return NotImplemented + return self.id == value.id + + def __hash__(self): + return hash(self.id) + + +class Mocking: + """Constructs mock identifiers for use with the TextVerified API.""" + + def __init__(self, verifications_api: VerificationsAPI, reservations_api: ReservationsAPI, billing_cycle_api: BillingCycleAPI, sales_api: SalesAPI, wake_api: WakeAPI): + self.verifications_api = verifications_api + self.reservations_api = reservations_api + self.billing_cycle_api = billing_cycle_api + self.sales_api = sales_api + self.wake_api = wake_api + + def target(self, behavior: MockBehavior = MockBehavior.Succeeds, receive_policy: MockReceivePolicy = MockReceivePolicy.IncomingTextAndCall) -> str: + """Constructs a mock target identifier. + + Args: + behavior (MockBehavior, optional): Determines how the mock should behave. Defaults to MockBehavior.Succeeds. + receive_policy (MockReceivePolicy, optional): Determines what should be returned when polling for messages or calls. Defaults to MockReceivePolicy.IncomingTextAndCall. + + Returns: + str: The constructed mock target identifier. + """ + return _construct_mock(MockIdentifierType.MockTarget.name, behavior.name, receive_policy.name) + + def service(self, behavior: MockBehavior = MockBehavior.Succeeds, receive_policy: MockReceivePolicy = MockReceivePolicy.IncomingTextAndCall) -> str: + """Constructs a mock service name identifier. Identical to Mocking.target. + + Args: + behavior (MockBehavior, optional): Determines how the mock should behave. Defaults to MockBehavior.Succeeds. + receive_policy (MockReceivePolicy, optional): Determines what should be returned when polling for messages or calls. Defaults to MockReceivePolicy.IncomingTextAndCall. + + Returns: + str: The constructed mock service name identifier. + """ + return self.target(behavior, receive_policy) + + def reservation(self, behavior: MockBehavior = MockBehavior.Succeeds, receive_policy: MockReceivePolicy = MockReceivePolicy.IncomingTextAndCall, reservation_type: ReservationType = ReservationType.NONRENEWABLE, ) -> MockObject[Union[RenewableRentalExpanded, NonrenewableRentalExpanded]]: + line_type = LineReservationType.VERIFICATION if reservation_type == ReservationType.VERIFICATION else LineReservationType.RENTAL + mock_id = _construct_mock(MockIdentifierType.MockReservation.name, behavior.name, line_type.name, reservation_type.name, receive_policy.name) + return MockObject[Union[RenewableRentalExpanded, NonrenewableRentalExpanded]](id=mock_id, _get_function=lambda: self.reservations_api.details(mock_id)) + + def rental(self, behavior: MockBehavior = MockBehavior.Succeeds, receive_policy: MockReceivePolicy = MockReceivePolicy.IncomingTextAndCall, reservation_type: ReservationType = ReservationType.NONRENEWABLE,) -> MockObject[Union[RenewableRentalExpanded, NonrenewableRentalExpanded]]: + """Constructs a mock rental identifier that returns rental details when get() is called. + + Args: + behavior (MockBehavior, optional): Determines how the mock should behave. Defaults to MockBehavior.Succeeds. + receive_policy (MockReceivePolicy, optional): Determines what should be returned when polling for messages or calls. Defaults to MockReceivePolicy.IncomingTextAndCall. + reservation_type (ReservationType, optional): The type of rental. Defaults to ReservationType.NONRENEWABLE. + + Returns: + MockObject[Union[RenewableRentalExpanded, NonrenewableRentalExpanded]]: A mock object that can retrieve rental details. + """ + line_type = LineReservationType.VERIFICATION if reservation_type == ReservationType.VERIFICATION else LineReservationType.RENTAL + mock_id = _construct_mock(MockIdentifierType.MockRental.name, behavior.name, line_type.name, reservation_type.name, receive_policy.name) + return MockObject[Union[RenewableRentalExpanded, NonrenewableRentalExpanded]](id=mock_id, _get_function=lambda: self.reservations_api.details(mock_id)) + + def verification(self, behavior: MockBehavior = MockBehavior.Succeeds, receive_policy: MockReceivePolicy = MockReceivePolicy.IncomingTextAndCall) -> MockObject[VerificationExpanded]: + """Constructs a mock verification identifier that returns verification details when get() is called. + + Args: + behavior (MockBehavior, optional): Determines how the mock should behave. Defaults to MockBehavior.Succeeds. + receive_policy (MockReceivePolicy, optional): Determines what should be returned when polling for messages or calls. Defaults to MockReceivePolicy.IncomingTextAndCall. + + Returns: + MockObject[VerificationExpanded]: A mock object that can retrieve verification details. + """ + mock_id = _construct_mock(MockIdentifierType.MockVerification.name, behavior.name, LineReservationType.VERIFICATION.name, ReservationType.VERIFICATION.name, receive_policy.name) + return MockObject[VerificationExpanded](id=mock_id, _get_function=lambda: self.verifications_api.details(mock_id)) + + def sale(self, behavior: MockBehavior = MockBehavior.Succeeds, receive_policy: MockReceivePolicy = MockReceivePolicy.IncomingTextAndCall, reservation_type: ReservationType = ReservationType.NONRENEWABLE,) -> MockObject[ReservationSaleExpanded]: + """Constructs a mock sale identifier that returns sale details when get() is called. + + Args: + behavior (MockBehavior, optional): Determines how the mock should behave. Defaults to MockBehavior.Succeeds. + receive_policy (MockReceivePolicy, optional): Determines what should be returned when polling for messages or calls. Defaults to MockReceivePolicy.IncomingTextAndCall. + reservation_type (ReservationType, optional): The type of reservation for the sale. Defaults to ReservationType.NONRENEWABLE. + + Returns: + MockObject[ReservationSaleExpanded]: A mock object that can retrieve sale details. + """ + line_type = LineReservationType.VERIFICATION if reservation_type == ReservationType.VERIFICATION else LineReservationType.RENTAL + mock_id = _construct_mock(MockIdentifierType.MockSale.name, behavior.name, line_type.name, reservation_type.name, receive_policy.name) + return MockObject[ReservationSaleExpanded](id=mock_id, _get_function=lambda: self.sales_api.get(mock_id)) + + def backorder_sale(self, behavior: MockBehavior = MockBehavior.Succeeds, receive_policy: MockReceivePolicy = MockReceivePolicy.IncomingTextAndCall, reservation_type: ReservationType = ReservationType.NONRENEWABLE,) -> MockObject[BackOrderReservationExpanded]: + """Constructs a mock backorder sale identifier that returns backorder details when get() is called. + + Args: + behavior (MockBehavior, optional): Determines how the mock should behave. Defaults to MockBehavior.Succeeds. + receive_policy (MockReceivePolicy, optional): Determines what should be returned when polling for messages or calls. Defaults to MockReceivePolicy.IncomingTextAndCall. + reservation_type (ReservationType, optional): The type of reservation for the backorder. Defaults to ReservationType.NONRENEWABLE. + + Returns: + MockObject[BackOrderReservationExpanded]: A mock object that can retrieve backorder details. + """ + line_type = LineReservationType.VERIFICATION if reservation_type == ReservationType.VERIFICATION else LineReservationType.RENTAL + mock_id = _construct_mock(MockIdentifierType.MockBackorderReservation.name, behavior.name, line_type.name, reservation_type.name, receive_policy.name) + return MockObject[BackOrderReservationExpanded](id=mock_id, _get_function=lambda: self.reservations_api.backorder(mock_id)) + + def billing_cycle(self, behavior: MockBehavior = MockBehavior.Succeeds) -> MockObject[BillingCycleExpanded]: + """Constructs a mock billing cycle identifier that returns billing cycle details when get() is called. + + Args: + behavior (MockBehavior, optional): Determines how the mock should behave. Defaults to MockBehavior.Succeeds. + + Returns: + MockObject[BillingCycleExpanded]: A mock object that can retrieve billing cycle details. + """ + mock_id = _construct_mock(MockIdentifierType.MockBillingCycle.name, behavior.name) + return MockObject[BillingCycleExpanded](id=mock_id, _get_function=lambda: self.billing_cycle_api.get(mock_id)) + + def extension(self, behavior: MockBehavior = MockBehavior.Succeeds) -> MockObject[RentalExtensionRequest]: + """Constructs a mock extension identifier. + + Args: + behavior (MockBehavior, optional): Determines how the mock should behave. Defaults to MockBehavior.Succeeds. + + Returns: + MockObject[RentalExtensionRequest]: A mock object that can retrieve a rental extension request. + """ + mock_id = _construct_mock(MockIdentifierType.MockExtension.name, behavior.name) + return MockObject[RentalExtensionRequest](id=mock_id, _get_function=lambda: RentalExtensionRequest( + mock_id, + extension_duration=RentalDuration.THIRTY_DAY + )) + + def wake_request(self, behavior: MockBehavior = MockBehavior.Succeeds, receive_policy: MockReceivePolicy = MockReceivePolicy.IncomingTextAndCall, reservation_type: ReservationType = ReservationType.NONRENEWABLE,) -> MockObject[WakeResponse]: + """Constructs a mock wake request identifier that returns wake request details when get() is called. + + Args: + behavior (MockBehavior, optional): Determines how the mock should behave. Defaults to MockBehavior.Succeeds. + receive_policy (MockReceivePolicy, optional): Determines what should be returned when polling for messages or calls. Defaults to MockReceivePolicy.IncomingTextAndCall. + reservation_type (ReservationType, optional): The type of reservation for the wake request. Defaults to ReservationType.NONRENEWABLE. + + Returns: + MockObject[WakeResponse]: A mock object that can retrieve wake request details. + """ + line_type = LineReservationType.VERIFICATION if reservation_type == ReservationType.VERIFICATION else LineReservationType.RENTAL + mock_id = _construct_mock(MockIdentifierType.MockWakeRequest.name, behavior.name, line_type.name, reservation_type.name, receive_policy.name) + return MockObject[WakeResponse](id=mock_id, _get_function=lambda: self.wake_api.get(mock_id)) + diff --git a/textverified/textverified.py b/textverified/textverified.py index 1947f63..668108e 100644 --- a/textverified/textverified.py +++ b/textverified/textverified.py @@ -11,6 +11,7 @@ from .verifications_api import VerificationsAPI from .wake_api import WakeAPI from .call_api import CallAPI +from .mocking import Mocking import requests import datetime from requests.adapters import HTTPAdapter @@ -80,6 +81,16 @@ def sms(self) -> SMSApi: def calls(self) -> CallAPI: return CallAPI(self) + @property + def mocking(self) -> Mocking: + return Mocking( + verifications_api=self.verifications, + reservations_api=self.reservations, + billing_cycle_api=self.billing_cycles, + sales_api=self.sales, + wake_api=self.wake_requests, + ) + def __post_init__(self): self.bearer = None self.base_url = self.base_url.rstrip("/") @@ -128,7 +139,7 @@ def _perform_action(self, action: _Action, **kwargs) -> _ActionResponse: return self.__perform_action_internal(action.method, href, **kwargs) def __perform_action_internal(self, method: str, href: str, **kwargs) -> _ActionResponse: - """Internal action performance with authorization""" + """Internal action (to localhost or base_url) with authorization""" # Check if bearer token is set and valid self.refresh_bearer() @@ -144,7 +155,7 @@ def __perform_action_internal(self, method: str, href: str, **kwargs) -> _Action return _ActionResponse(data=response.json() if response.text else {}, headers=response.headers) def __perform_action_external(self, method: str, href: str, **kwargs) -> _ActionResponse: - """External action performance without authorization""" + """External action (to unknown domain) without authorization""" # Allow unverified certificates for localhost verify = not href.startswith("http://localhost") and not href.startswith("https://localhost") From 4c8b1dcb5a9ebebed5dfeb54d36e2254a0f82ddb Mon Sep 17 00:00:00 2001 From: Leon Leibmann Date: Fri, 26 Sep 2025 13:35:02 -0700 Subject: [PATCH 09/11] create mocking documentation --- docs/api_reference.rst | 6 ++ docs/examples.rst | 127 +++++++++++++++++++++++-- examples/credential_set.py | 5 + examples/mocking.py | 185 +++++++++++++++++++++++++++++++++++++ examples/wake_rental.py | 7 +- 5 files changed, 319 insertions(+), 11 deletions(-) create mode 100644 examples/mocking.py diff --git a/docs/api_reference.rst b/docs/api_reference.rst index f5535e8..9fce384 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -73,6 +73,12 @@ Wake API .. automodule:: textverified.wake_api :members: +Mocking API +~~~~~~~~~~~ + +.. automodule:: textverified.mocking + :members: + PaginatedList --------------- diff --git a/docs/examples.rst b/docs/examples.rst index b16cd9a..4acc208 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -206,17 +206,10 @@ Wakeable Rental Example # 2. Start a wake request for the rental print("Sending wake request and waiting for active window...") wake_request = wake_requests.create(rental) - duration = wake_request.usage_window_end - wake_request.usage_window_start - print( - f"Number {rental.number} is active from {wake_request.usage_window_start}" - f" to {wake_request.usage_window_end} (duration: {duration})" - ) - - # 3. Wait for the wake request to complete - time_until_start = wake_request.usage_window_start - datetime.datetime.now(datetime.timezone.utc) - print(f"Waiting for the number to become active... ({time_until_start})") wake_response = wake_requests.wait_for_wake_request(wake_request) + duration = wake_response.usage_window_end - wake_response.usage_window_start + print(f"Number {rental.number} is now active until {wake_response.usage_window_end} (duration: {duration})") # 3. Poll for SMS messages on the awakened number print(f"Polling SMS messages for number {rental.number}...") @@ -260,3 +253,119 @@ Proper error handling for production use: Note that all API requests use exponential backoff for retries, and retries on connection error or ratelimit errors. +Mocking Examples +---------------- + +The mocking feature allows you to create mock objects for testing and development without making actual API calls. + +Basic Verification Workflow with Mock ID +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from textverified import TextVerified, mocking + + # Create a mock verification + mock_verification = mocking.verification() + print(f"Mock verification ID: {mock_verification.id}") + + client = TextVerified(api_key="your_key", api_username="your_username") + + # Use mock ID with real API methods + verification = client.verifications.details(mock_verification.id) + print(f"Verification number: {verification.number}") + + # Poll for SMS messages + messages = client.sms.incoming(verification, timeout=10) + for message in messages: + print(f"Received code: {message.parsed_code}") + break + +Verification Mock Purchase +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from textverified import TextVerified, mocking, ReservationCapability, NewVerificationRequest + + # Create a mock service name + mock_service = mocking.target() + print(f"Mock service: {mock_service}") + + client = TextVerified(api_key="your_key", api_username="your_username") + + # Create verification request with mock service + request = NewVerificationRequest( + service_name=mock_service, + capability=ReservationCapability.SMS + ) + + # Get pricing and create verification + price_snapshot = client.verifications.pricing(request) + assert price_snapshot.price == 0.0 # will be 0.0 for mock + verification = client.verifications.create(request) + print(f"Created verification: {verification.id}") + +Testing Error Handling with Random Failures +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from textverified import TextVerified, mocking, MockBehavior + + # Create mock that fails randomly (helps test error handling) + mock_verification = mocking.verification(behavior=MockBehavior.FailsRandomly) + + client = TextVerified(api_key="your_key", api_username="your_username") + + # API calls will fail ~ 33% of the time, use this to test your error handling + try: + verification = client.verifications.details(mock_verification.id) + print("Verification succeeded") + except Exception as e: + print(f"Verification failed: {e}") + +Rental Workflow with Mock ID +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from textverified import TextVerified, mocking + + # Create a mock rental + mock_rental = mocking.rental() + print(f"Mock rental ID: {mock_rental.id}") + + client = TextVerified(api_key="your_key", api_username="your_username") + + # Get rental details + rental = client.reservations.details(mock_rental.id) + print(f"Rental number: {rental.number}") + + # List SMS messages + messages = client.sms.list() + print(f"Found {len(messages)} messages") + +Wake Rental Workflow with Mock ID +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from textverified import TextVerified, mocking + + # Create mock rental for wake testing + mock_rental = mocking.rental() + + client = TextVerified(api_key="your_key", api_username="your_username") + + # Get rental and create wake request + rental = client.reservations.details(mock_rental.id) + wake_request = client.wake_requests.create(rental) + + # Poll for messages during wake window + wake_request.wait_for_wake_request() # This will block for mocks + messages = client.sms.incoming(rental, timeout=10) + for message in messages: + print(f"Received: {message.sms_content}") + break + diff --git a/examples/credential_set.py b/examples/credential_set.py index ac6d049..2e630a3 100644 --- a/examples/credential_set.py +++ b/examples/credential_set.py @@ -11,6 +11,11 @@ textverified.configure(api_key="your_api_key", api_username="your_username") +# Now we can call any API method as usual. + +# The order of imports is not important - +# this import could have been before .configure + from textverified import account as tv_account # Get account details diff --git a/examples/mocking.py b/examples/mocking.py new file mode 100644 index 0000000..9b732e6 --- /dev/null +++ b/examples/mocking.py @@ -0,0 +1,185 @@ +""" +Mocking Examples for TextVerified Python Client + +This example demonstrates how to use mock verification and rental IDs +in real API workflows for testing and development. +""" + +from textverified import TextVerified, mocking, MockBehavior, MockReceivePolicy, ReservationCapability, NewVerificationRequest +import time + +client = TextVerified(api_key="YOUR_KEY", api_username="YOUR_EMAIL") + +def mock_verification_workflow(): + """Complete verification workflow using a mock verification ID.""" + print("Mock Verification Workflow") + print("=" * 30) + + # Create a mock verification + mock_verification = mocking.verification(receive_policy=MockReceivePolicy.IncomingText) # default behavior, but explicitly specified + print(f"Created mock verification: {mock_verification.id}") + + try: + # Use the real client for verification details and state + verification = client.verifications.details(mock_verification.id) + print(f"Verification number: {verification.number}") + print(f"Verification state: {verification.state}") + + # Poll for SMS messages with base + print("Polling for SMS messages...") + messages = client.sms.incoming(verification, timeout=10) + for message in messages: + print(f"Received SMS: {message.sms_content}") + print(f"Parsed code: {message.parsed_code}") + + except Exception as e: + print(f"API call failed (expected with mock credentials): {type(e).__name__}") + + print() + + +def mock_verification_purchase_workflow(): + """Verification purchase workflow using a mock service name.""" + print("Mock Verification Purchase Workflow") + print("=" * 30) + + # Create a mock service name (target) + # You can also use mocking.service(...) as an alias + mock_service = mocking.target(behavior=MockBehavior.Succeeds) + print(f"Created mock service: {mock_service}") + + try: + # Create a verification request using the mock service name + request = NewVerificationRequest( + service_name=mock_service, + capability=ReservationCapability.SMS + ) + + # Get pricing for the mock service + price = client.verifications.pricing(request) + print(f"Pricing for mock service: ${price.price}") + + # Create the verification + verification = client.verifications.create(request) + print(f"Verification created: {verification.id}") + print(f"Verification number: {verification.number}") + + # Poll for SMS messages + print("Polling for verification code...") + for message in client.sms.incoming(verification, timeout=10): + print(f"Received SMS: {message.sms_content}") + print(f"Parsed verification code: {message.parsed_code}") + break + + except Exception as e: + print(f"API call failed (expected with mock credentials): {type(e).__name__}") + + print() + + +def mock_verification_with_random_failures(): + """Verification workflow that may fail randomly to test error handling.""" + print("Mock Verification with Random Failures") + print("=" * 30) + + # Create a mock verification that fails randomly (1/3 of the time) + # This helps test unexpected behavior and error handling + mock_verification = mocking.verification( + behavior=MockBehavior.FailsRandomly, + receive_policy=MockReceivePolicy.IncomingText + ) + print(f"Created mock verification: {mock_verification.id}") + + try: + verification = client.verifications.details(mock_verification.id) + print(f"Verification succeeded: {verification.number}") + + # This might succeed or fail, testing your error handling + for message in client.sms.incoming(verification, timeout=10): + print(f"SMS received: {message.parsed_code}") + + except Exception as e: + print(f"Verification failed (this may happen randomly): {type(e).__name__}") + + print() + + +def mock_rental_workflow(): + """Rental workflow using a mock rental ID.""" + print("Mock Rental Workflow") + print("=" * 30) + + # Create a mock rental + mock_rental = mocking.rental(behavior=MockBehavior.Succeeds) + print(f"Created mock rental: {mock_rental.id}") + + client = TextVerified(api_key="mock", api_username="mock") + + try: + # Get rental details + rental = client.reservations.details(mock_rental.id) + print(f"Rental number: {rental.number}") + print(f"Rental state: {rental.state}") + + # List SMS messages for the rental + messages = client.sms.list() + print(f"Found {len(messages)} SMS messages") + + except Exception as e: + print(f"API call failed (expected with mock credentials): {type(e).__name__}") + + print() + + +def mock_wake_rental_workflow(): + """Wake rental workflow using a mock rental ID.""" + print("Mock Wake Rental Workflow") + print("=" * 30) + + # Create a mock rental for wake testing + mock_rental = mocking.rental( + behavior=MockBehavior.Succeeds, + receive_policy=MockReceivePolicy.IncomingTextAndCall + ) + print(f"Created mock rental: {mock_rental.id}") + + try: + # Get rental details + rental = client.reservations.details(mock_rental.id) + print(f"Rental number: {rental.number}") + + # Create a wake request for the rental + wake_request = client.wake_requests.create(rental) + print(f"Wake request created: {wake_request.id}") + print(f"Active from {wake_request.usage_window_start} to {wake_request.usage_window_end}") + + # Poll for messages during the wake window + duration = wake_request.usage_window_end - wake_request.usage_window_start + print("Polling for messages during wake window...") + messages = client.sms.incoming(rental, timeout=duration.total_seconds()) + + for message in messages: + print(f"Received: {message.sms_content}") + break # Just show first message + + except Exception as e: + print(f"API call failed (expected with mock credentials): {type(e).__name__}") + + print() + + +if __name__ == "__main__": + print("TextVerified Mocking Examples") + print("=" * 30) + print() + + # Run all examples + mock_verification_workflow() + mock_verification_purchase_workflow() + mock_verification_with_random_failures() + mock_rental_workflow() + mock_wake_rental_workflow() + + print("Examples completed.") + print("Note: API calls use mock credentials and may fail - this demonstrates") + print("how mock IDs integrate with real API workflows for testing.") diff --git a/examples/wake_rental.py b/examples/wake_rental.py index 6871236..3b9926f 100644 --- a/examples/wake_rental.py +++ b/examples/wake_rental.py @@ -19,15 +19,18 @@ wake_request = wake_requests.create(rental) duration = wake_request.usage_window_end - wake_request.usage_window_start print( - f"Number {rental.number} is active from {wake_request.usage_window_start}" + f"Number {rental.number} is estimated to be active from {wake_request.usage_window_start}" f" to {wake_request.usage_window_end} (duration: {duration})" ) # 3. Wait for the wake request to complete time_until_start = wake_request.usage_window_start - datetime.datetime.now(datetime.timezone.utc) -print(f"Waiting for the number to become active... ({time_until_start})") +print(f"Waiting for the number to become active... (estimate: {time_until_start})") wake_response = wake_requests.wait_for_wake_request(wake_request) +# Refresh duration based on actual active window +duration = wake_response.usage_window_end - wake_response.usage_window_start +print(f"Number {rental.number} is now active until {wake_response.usage_window_end} (duration: {duration})") # 3. Poll for SMS messages on the awakened number print(f"Polling SMS messages for number {rental.number}...") From 7f32cae451a92050dee3a64de9c67d8ed5c4a751 Mon Sep 17 00:00:00 2001 From: Leon Leibmann Date: Fri, 26 Sep 2025 14:30:34 -0700 Subject: [PATCH 10/11] format & set formatter to ignore examples --- pyproject.toml | 1 + tests/fixtures.py | 1 + tests/test_mocking.py | 7 +- textverified/mocking.py | 220 +++++++++++++++++++++++-------- textverified/reservations_api.py | 2 +- 5 files changed, 170 insertions(+), 61 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3368355..ed286c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ extend-exclude = ''' | \.venv | build | dist + | examples | textverified/data )/ ''' diff --git a/tests/fixtures.py b/tests/fixtures.py index e6be0ef..c17a012 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -313,6 +313,7 @@ def _replace_placeholders(obj, replacements): obj = obj.replace(placeholder, value) return obj + @pytest.fixture def mock_http_from_disk(): """ diff --git a/tests/test_mocking.py b/tests/test_mocking.py index 594cfb1..47f73bc 100644 --- a/tests/test_mocking.py +++ b/tests/test_mocking.py @@ -140,10 +140,7 @@ def test_mock_object_name_property(): def test_all_mock_ids_contain_separator(tv): """Test that all mock ID generation methods produce IDs with '_$_' separator""" - methods_returning_strings = [ - tv.mocking.target, - tv.mocking.service - ] + methods_returning_strings = [tv.mocking.target, tv.mocking.service] methods_returning_mock_objects = [ tv.mocking.verification, @@ -166,4 +163,4 @@ def test_all_mock_ids_contain_separator(tv): for method in methods_returning_mock_objects: mock_obj = method() assert isinstance(mock_obj, MockObject), f"{method.__name__} should return a MockObject" - assert "_$_" in mock_obj.id, f"{method.__name__} should have '_$_' in the id" \ No newline at end of file + assert "_$_" in mock_obj.id, f"{method.__name__} should have '_$_' in the id" diff --git a/textverified/mocking.py b/textverified/mocking.py index c8aa540..cd99acf 100644 --- a/textverified/mocking.py +++ b/textverified/mocking.py @@ -1,12 +1,9 @@ -from .action import _ActionPerformer, _Action -from typing import List, Union, Generic, TypeVar, Callable +from typing import Union, Generic, TypeVar, Callable from enum import Enum -from .paginated_list import PaginatedList from dataclasses import dataclass from .data import ( LineReservationType, ReservationType, - Reservation, NonrenewableRentalExpanded, RenewableRentalExpanded, VerificationExpanded, @@ -15,7 +12,7 @@ WakeResponse, BackOrderReservationExpanded, RentalExtensionRequest, - RentalDuration + RentalDuration, ) from .verifications_api import VerificationsAPI from .reservations_api import ReservationsAPI @@ -23,56 +20,66 @@ from .sales_api import SalesAPI from .wake_api import WakeAPI + class MockIdentifierType(Enum): """Mock identifier types""" - MockWakeRequest = 1, - MockSale = 2, - MockBackorderReservation = 3, + + MockWakeRequest = 1 + MockSale = 2 + MockBackorderReservation = 3 # MockReservationRequest = 4, -- not needed for api calls - MockBillingCycle = 5, - MockReservation = 6, - MockVerification = 7, - MockRental = 8, - MockTarget = 9, + MockBillingCycle = 5 + MockReservation = 6 + MockVerification = 7 + MockRental = 8 + MockTarget = 9 MockExtension = 10 + class MockReceivePolicy(Enum): """MockReceivePolicy: determines what should be returned when polling for messages or calls""" + NoIncoming = 0 IncomingText = 1 IncomingCall = 2 - IncomingTextAndCall = 3 - + IncomingTextAndCall = 3 + + class MockBehavior(Enum): """MockBehavior: determines how the mock should behave. If set to FailRandomly, will fail 1/3 of the time.""" + Succeeds = 0 AlwaysFails = 1 FailsRandomly = 2 + def _construct_mock(*args): assert len(args) > 0 return "_$_".join(str(arg) for arg in args) -T = TypeVar('T') + +T = TypeVar("T") + @dataclass(frozen=True) class MockObject(Generic[T]): """Base class for mock objects.""" + id: str _get_function: Callable[[], T] - + @property def name(self) -> str: return self.id def get(self) -> T: return self._get_function() - + def __eq__(self, value): if not isinstance(value, MockObject): return NotImplemented return self.id == value.id - + def __hash__(self): return hash(self.id) @@ -80,14 +87,25 @@ def __hash__(self): class Mocking: """Constructs mock identifiers for use with the TextVerified API.""" - def __init__(self, verifications_api: VerificationsAPI, reservations_api: ReservationsAPI, billing_cycle_api: BillingCycleAPI, sales_api: SalesAPI, wake_api: WakeAPI): + def __init__( + self, + verifications_api: VerificationsAPI, + reservations_api: ReservationsAPI, + billing_cycle_api: BillingCycleAPI, + sales_api: SalesAPI, + wake_api: WakeAPI, + ): self.verifications_api = verifications_api self.reservations_api = reservations_api self.billing_cycle_api = billing_cycle_api self.sales_api = sales_api self.wake_api = wake_api - def target(self, behavior: MockBehavior = MockBehavior.Succeeds, receive_policy: MockReceivePolicy = MockReceivePolicy.IncomingTextAndCall) -> str: + def target( + self, + behavior: MockBehavior = MockBehavior.Succeeds, + receive_policy: MockReceivePolicy = MockReceivePolicy.IncomingTextAndCall, + ) -> str: """Constructs a mock target identifier. Args: @@ -98,8 +116,12 @@ def target(self, behavior: MockBehavior = MockBehavior.Succeeds, receive_policy: str: The constructed mock target identifier. """ return _construct_mock(MockIdentifierType.MockTarget.name, behavior.name, receive_policy.name) - - def service(self, behavior: MockBehavior = MockBehavior.Succeeds, receive_policy: MockReceivePolicy = MockReceivePolicy.IncomingTextAndCall) -> str: + + def service( + self, + behavior: MockBehavior = MockBehavior.Succeeds, + receive_policy: MockReceivePolicy = MockReceivePolicy.IncomingTextAndCall, + ) -> str: """Constructs a mock service name identifier. Identical to Mocking.target. Args: @@ -111,12 +133,34 @@ def service(self, behavior: MockBehavior = MockBehavior.Succeeds, receive_policy """ return self.target(behavior, receive_policy) - def reservation(self, behavior: MockBehavior = MockBehavior.Succeeds, receive_policy: MockReceivePolicy = MockReceivePolicy.IncomingTextAndCall, reservation_type: ReservationType = ReservationType.NONRENEWABLE, ) -> MockObject[Union[RenewableRentalExpanded, NonrenewableRentalExpanded]]: - line_type = LineReservationType.VERIFICATION if reservation_type == ReservationType.VERIFICATION else LineReservationType.RENTAL - mock_id = _construct_mock(MockIdentifierType.MockReservation.name, behavior.name, line_type.name, reservation_type.name, receive_policy.name) - return MockObject[Union[RenewableRentalExpanded, NonrenewableRentalExpanded]](id=mock_id, _get_function=lambda: self.reservations_api.details(mock_id)) - - def rental(self, behavior: MockBehavior = MockBehavior.Succeeds, receive_policy: MockReceivePolicy = MockReceivePolicy.IncomingTextAndCall, reservation_type: ReservationType = ReservationType.NONRENEWABLE,) -> MockObject[Union[RenewableRentalExpanded, NonrenewableRentalExpanded]]: + def reservation( + self, + behavior: MockBehavior = MockBehavior.Succeeds, + receive_policy: MockReceivePolicy = MockReceivePolicy.IncomingTextAndCall, + reservation_type: ReservationType = ReservationType.NONRENEWABLE, + ) -> MockObject[Union[RenewableRentalExpanded, NonrenewableRentalExpanded]]: + line_type = ( + LineReservationType.VERIFICATION + if reservation_type == ReservationType.VERIFICATION + else LineReservationType.RENTAL + ) + mock_id = _construct_mock( + MockIdentifierType.MockReservation.name, + behavior.name, + line_type.name, + reservation_type.name, + receive_policy.name, + ) + return MockObject[Union[RenewableRentalExpanded, NonrenewableRentalExpanded]]( + id=mock_id, _get_function=lambda: self.reservations_api.details(mock_id) + ) + + def rental( + self, + behavior: MockBehavior = MockBehavior.Succeeds, + receive_policy: MockReceivePolicy = MockReceivePolicy.IncomingTextAndCall, + reservation_type: ReservationType = ReservationType.NONRENEWABLE, + ) -> MockObject[Union[RenewableRentalExpanded, NonrenewableRentalExpanded]]: """Constructs a mock rental identifier that returns rental details when get() is called. Args: @@ -127,11 +171,27 @@ def rental(self, behavior: MockBehavior = MockBehavior.Succeeds, receive_policy: Returns: MockObject[Union[RenewableRentalExpanded, NonrenewableRentalExpanded]]: A mock object that can retrieve rental details. """ - line_type = LineReservationType.VERIFICATION if reservation_type == ReservationType.VERIFICATION else LineReservationType.RENTAL - mock_id = _construct_mock(MockIdentifierType.MockRental.name, behavior.name, line_type.name, reservation_type.name, receive_policy.name) - return MockObject[Union[RenewableRentalExpanded, NonrenewableRentalExpanded]](id=mock_id, _get_function=lambda: self.reservations_api.details(mock_id)) - - def verification(self, behavior: MockBehavior = MockBehavior.Succeeds, receive_policy: MockReceivePolicy = MockReceivePolicy.IncomingTextAndCall) -> MockObject[VerificationExpanded]: + line_type = ( + LineReservationType.VERIFICATION + if reservation_type == ReservationType.VERIFICATION + else LineReservationType.RENTAL + ) + mock_id = _construct_mock( + MockIdentifierType.MockRental.name, + behavior.name, + line_type.name, + reservation_type.name, + receive_policy.name, + ) + return MockObject[Union[RenewableRentalExpanded, NonrenewableRentalExpanded]]( + id=mock_id, _get_function=lambda: self.reservations_api.details(mock_id) + ) + + def verification( + self, + behavior: MockBehavior = MockBehavior.Succeeds, + receive_policy: MockReceivePolicy = MockReceivePolicy.IncomingTextAndCall, + ) -> MockObject[VerificationExpanded]: """Constructs a mock verification identifier that returns verification details when get() is called. Args: @@ -141,10 +201,23 @@ def verification(self, behavior: MockBehavior = MockBehavior.Succeeds, receive_p Returns: MockObject[VerificationExpanded]: A mock object that can retrieve verification details. """ - mock_id = _construct_mock(MockIdentifierType.MockVerification.name, behavior.name, LineReservationType.VERIFICATION.name, ReservationType.VERIFICATION.name, receive_policy.name) - return MockObject[VerificationExpanded](id=mock_id, _get_function=lambda: self.verifications_api.details(mock_id)) - - def sale(self, behavior: MockBehavior = MockBehavior.Succeeds, receive_policy: MockReceivePolicy = MockReceivePolicy.IncomingTextAndCall, reservation_type: ReservationType = ReservationType.NONRENEWABLE,) -> MockObject[ReservationSaleExpanded]: + mock_id = _construct_mock( + MockIdentifierType.MockVerification.name, + behavior.name, + LineReservationType.VERIFICATION.name, + ReservationType.VERIFICATION.name, + receive_policy.name, + ) + return MockObject[VerificationExpanded]( + id=mock_id, _get_function=lambda: self.verifications_api.details(mock_id) + ) + + def sale( + self, + behavior: MockBehavior = MockBehavior.Succeeds, + receive_policy: MockReceivePolicy = MockReceivePolicy.IncomingTextAndCall, + reservation_type: ReservationType = ReservationType.NONRENEWABLE, + ) -> MockObject[ReservationSaleExpanded]: """Constructs a mock sale identifier that returns sale details when get() is called. Args: @@ -155,11 +228,22 @@ def sale(self, behavior: MockBehavior = MockBehavior.Succeeds, receive_policy: M Returns: MockObject[ReservationSaleExpanded]: A mock object that can retrieve sale details. """ - line_type = LineReservationType.VERIFICATION if reservation_type == ReservationType.VERIFICATION else LineReservationType.RENTAL - mock_id = _construct_mock(MockIdentifierType.MockSale.name, behavior.name, line_type.name, reservation_type.name, receive_policy.name) + line_type = ( + LineReservationType.VERIFICATION + if reservation_type == ReservationType.VERIFICATION + else LineReservationType.RENTAL + ) + mock_id = _construct_mock( + MockIdentifierType.MockSale.name, behavior.name, line_type.name, reservation_type.name, receive_policy.name + ) return MockObject[ReservationSaleExpanded](id=mock_id, _get_function=lambda: self.sales_api.get(mock_id)) - - def backorder_sale(self, behavior: MockBehavior = MockBehavior.Succeeds, receive_policy: MockReceivePolicy = MockReceivePolicy.IncomingTextAndCall, reservation_type: ReservationType = ReservationType.NONRENEWABLE,) -> MockObject[BackOrderReservationExpanded]: + + def backorder_sale( + self, + behavior: MockBehavior = MockBehavior.Succeeds, + receive_policy: MockReceivePolicy = MockReceivePolicy.IncomingTextAndCall, + reservation_type: ReservationType = ReservationType.NONRENEWABLE, + ) -> MockObject[BackOrderReservationExpanded]: """Constructs a mock backorder sale identifier that returns backorder details when get() is called. Args: @@ -170,10 +254,22 @@ def backorder_sale(self, behavior: MockBehavior = MockBehavior.Succeeds, receive Returns: MockObject[BackOrderReservationExpanded]: A mock object that can retrieve backorder details. """ - line_type = LineReservationType.VERIFICATION if reservation_type == ReservationType.VERIFICATION else LineReservationType.RENTAL - mock_id = _construct_mock(MockIdentifierType.MockBackorderReservation.name, behavior.name, line_type.name, reservation_type.name, receive_policy.name) - return MockObject[BackOrderReservationExpanded](id=mock_id, _get_function=lambda: self.reservations_api.backorder(mock_id)) - + line_type = ( + LineReservationType.VERIFICATION + if reservation_type == ReservationType.VERIFICATION + else LineReservationType.RENTAL + ) + mock_id = _construct_mock( + MockIdentifierType.MockBackorderReservation.name, + behavior.name, + line_type.name, + reservation_type.name, + receive_policy.name, + ) + return MockObject[BackOrderReservationExpanded]( + id=mock_id, _get_function=lambda: self.reservations_api.backorder(mock_id) + ) + def billing_cycle(self, behavior: MockBehavior = MockBehavior.Succeeds) -> MockObject[BillingCycleExpanded]: """Constructs a mock billing cycle identifier that returns billing cycle details when get() is called. @@ -185,7 +281,7 @@ def billing_cycle(self, behavior: MockBehavior = MockBehavior.Succeeds) -> MockO """ mock_id = _construct_mock(MockIdentifierType.MockBillingCycle.name, behavior.name) return MockObject[BillingCycleExpanded](id=mock_id, _get_function=lambda: self.billing_cycle_api.get(mock_id)) - + def extension(self, behavior: MockBehavior = MockBehavior.Succeeds) -> MockObject[RentalExtensionRequest]: """Constructs a mock extension identifier. @@ -196,12 +292,17 @@ def extension(self, behavior: MockBehavior = MockBehavior.Succeeds) -> MockObjec MockObject[RentalExtensionRequest]: A mock object that can retrieve a rental extension request. """ mock_id = _construct_mock(MockIdentifierType.MockExtension.name, behavior.name) - return MockObject[RentalExtensionRequest](id=mock_id, _get_function=lambda: RentalExtensionRequest( - mock_id, - extension_duration=RentalDuration.THIRTY_DAY - )) + return MockObject[RentalExtensionRequest]( + id=mock_id, + _get_function=lambda: RentalExtensionRequest(mock_id, extension_duration=RentalDuration.THIRTY_DAY), + ) - def wake_request(self, behavior: MockBehavior = MockBehavior.Succeeds, receive_policy: MockReceivePolicy = MockReceivePolicy.IncomingTextAndCall, reservation_type: ReservationType = ReservationType.NONRENEWABLE,) -> MockObject[WakeResponse]: + def wake_request( + self, + behavior: MockBehavior = MockBehavior.Succeeds, + receive_policy: MockReceivePolicy = MockReceivePolicy.IncomingTextAndCall, + reservation_type: ReservationType = ReservationType.NONRENEWABLE, + ) -> MockObject[WakeResponse]: """Constructs a mock wake request identifier that returns wake request details when get() is called. Args: @@ -212,7 +313,16 @@ def wake_request(self, behavior: MockBehavior = MockBehavior.Succeeds, receive_p Returns: MockObject[WakeResponse]: A mock object that can retrieve wake request details. """ - line_type = LineReservationType.VERIFICATION if reservation_type == ReservationType.VERIFICATION else LineReservationType.RENTAL - mock_id = _construct_mock(MockIdentifierType.MockWakeRequest.name, behavior.name, line_type.name, reservation_type.name, receive_policy.name) + line_type = ( + LineReservationType.VERIFICATION + if reservation_type == ReservationType.VERIFICATION + else LineReservationType.RENTAL + ) + mock_id = _construct_mock( + MockIdentifierType.MockWakeRequest.name, + behavior.name, + line_type.name, + reservation_type.name, + receive_policy.name, + ) return MockObject[WakeResponse](id=mock_id, _get_function=lambda: self.wake_api.get(mock_id)) - diff --git a/textverified/reservations_api.py b/textverified/reservations_api.py index 1f58565..6d032e9 100644 --- a/textverified/reservations_api.py +++ b/textverified/reservations_api.py @@ -304,7 +304,7 @@ def details( elif "reservations/rental/renewable/" in action.href: return RenewableRentalExpanded.from_api(response.data) - + raise ValueError("Unexpected reservation type in response.") def list_renewable(self) -> PaginatedList[RenewableRentalCompact]: From 9b47ee21391e47d157f457aca4b2a073141a2e97 Mon Sep 17 00:00:00 2001 From: Leon Leibmann Date: Fri, 26 Sep 2025 14:32:34 -0700 Subject: [PATCH 11/11] format --- textverified/mocking.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/textverified/mocking.py b/textverified/mocking.py index cd99acf..774fcc7 100644 --- a/textverified/mocking.py +++ b/textverified/mocking.py @@ -1,4 +1,4 @@ -from typing import Union, Generic, TypeVar, Callable +from typing import Union, Generic, TypeVar, Callable from enum import Enum from dataclasses import dataclass from .data import (