From bff80f839dc15910539696655e6effcfdb3d3ed2 Mon Sep 17 00:00:00 2001 From: bobbyonmagic Date: Fri, 9 Oct 2026 11:40:22 +0300 Subject: [PATCH] Regenerate from the spec: list objects for domains, webhooks and API keys GET /v1/domains, /v1/webhooks and /v1/api-keys now answer { object: "list", has_more, data }, and domain list items are snake_case. Claude-Session: https://claude.ai/code/session_01FRYkaYp9vFJjWBjAQdjnQ4 --- spec/openapi.json | 187 +++++++++++++++++++++++++++++++++++------- src/smtpfast/types.py | 50 ++++++++--- tests/test_live.py | 4 +- 3 files changed, 199 insertions(+), 42 deletions(-) diff --git a/spec/openapi.json b/spec/openapi.json index c75698d..01ab294 100644 --- a/spec/openapi.json +++ b/spec/openapi.json @@ -438,7 +438,7 @@ "post": { "operationId": "sendEmailBatch", "summary": "Send a batch of emails", - "description": "Queue up to 100 emails in a single request. The body is a JSON array of email objects, each using the same shape as the single-send endpoint. Validation runs on every item before any are queued \u2014 a malformed item rejects the whole batch with 400. Suppressed recipients (to, cc and bcc) are dropped per row; a row left with no deliverable To recipient is recorded with status \"failed\". The batch is accepted whole or not at all: every email is recorded and queued, or none is, so there is no partial success to reconcile. A 4xx means nothing was queued. After a 5xx or a lost response, retry with the same Idempotency-Key: you get the first answer if the batch was accepted, or a fresh attempt if it was not.", + "description": "Queue up to 100 emails in a single request. The body is a JSON array of email objects, each using the same shape as the single-send endpoint. Validation runs on every item before any are queued — a malformed item rejects the whole batch with 400. Suppressed recipients (to, cc and bcc) are dropped per row; a row left with no deliverable To recipient is recorded with status \"failed\". The batch is accepted whole or not at all: every email is recorded and queued, or none is, so there is no partial success to reconcile. A 4xx means nothing was queued. After a 5xx or a lost response, retry with the same Idempotency-Key: you get the first answer if the batch was accepted, or a fresh attempt if it was not.", "tags": [ "Emails" ], @@ -2022,7 +2022,7 @@ } }, "402": { - "description": "Error", + "description": "Plan limit reached - upgrade the plan to add more", "content": { "application/json": { "schema": { @@ -4095,6 +4095,10 @@ "type": "string", "example": "list" }, + "has_more": { + "type": "boolean", + "example": false + }, "data": { "type": "array", "items": { @@ -5570,9 +5574,19 @@ "schema": { "type": "object", "required": [ + "object", + "has_more", "data" ], "properties": { + "object": { + "type": "string", + "example": "list" + }, + "has_more": { + "type": "boolean", + "example": false + }, "data": { "type": "array", "items": { @@ -5992,6 +6006,16 @@ } } }, + "402": { + "description": "Plan limit reached - upgrade the plan to add more", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "403": { "description": "Forbidden - domain not verified or insufficient permissions", "content": { @@ -6376,6 +6400,16 @@ } } }, + "402": { + "description": "Plan limit reached - upgrade the plan to add more", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "403": { "description": "Forbidden - domain not verified or insufficient permissions", "content": { @@ -10813,7 +10847,7 @@ } }, "402": { - "description": "Error", + "description": "Plan limit reached - upgrade the plan to add more", "content": { "application/json": { "schema": { @@ -10881,9 +10915,27 @@ "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/DomainListItem" + "type": "object", + "required": [ + "object", + "has_more", + "data" + ], + "properties": { + "object": { + "type": "string", + "example": "list" + }, + "has_more": { + "type": "boolean", + "example": false + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DomainListItem" + } + } } } } @@ -11175,7 +11227,7 @@ } }, "402": { - "description": "Error", + "description": "Plan limit reached - upgrade the plan to add more", "content": { "application/json": { "schema": { @@ -11446,7 +11498,7 @@ } }, "402": { - "description": "Error", + "description": "Plan limit reached - upgrade the plan to add more", "content": { "application/json": { "schema": { @@ -11743,9 +11795,27 @@ "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ApiKey" + "type": "object", + "required": [ + "object", + "has_more", + "data" + ], + "properties": { + "object": { + "type": "string", + "example": "list" + }, + "has_more": { + "type": "boolean", + "example": false + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApiKey" + } + } } } } @@ -12055,6 +12125,26 @@ } } }, + "402": { + "description": "Plan limit reached - upgrade the plan to add more", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden - domain not verified or insufficient permissions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "429": { "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { @@ -12094,9 +12184,27 @@ "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Webhook" + "type": "object", + "required": [ + "object", + "has_more", + "data" + ], + "properties": { + "object": { + "type": "string", + "example": "list" + }, + "has_more": { + "type": "boolean", + "example": false + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Webhook" + } + } } } } @@ -16145,12 +16253,17 @@ "EmailList": { "type": "object", "required": [ + "object", "data", "limit", "total", "has_more" ], "properties": { + "object": { + "type": "string", + "example": "list" + }, "data": { "type": "array", "items": { @@ -16665,6 +16778,7 @@ "type": "object", "required": [ "object", + "has_more", "data", "total", "segment_limit" @@ -16674,6 +16788,11 @@ "type": "string", "example": "list" }, + "has_more": { + "type": "boolean", + "example": false, + "description": "Always false: every segment is in one page" + }, "data": { "type": "array", "items": { @@ -18101,6 +18220,8 @@ "PendingSignupList": { "type": "object", "required": [ + "object", + "has_more", "data", "total", "undeliverable_total", @@ -18110,6 +18231,14 @@ "per_page" ], "properties": { + "object": { + "type": "string", + "example": "list" + }, + "has_more": { + "type": "boolean", + "example": false + }, "data": { "type": "array", "items": { @@ -18492,18 +18621,18 @@ }, "DomainListItem": { "type": "object", - "description": "A domain as the list returns it. The field names are camelCase here, unlike the rest of the API.", + "description": "A domain as the list returns it. GET /v1/domains/{id} has the DNS records and verification detail.", "required": [ "id", "domain", "status", - "dkimStatus", - "spfStatus", - "dmarcStatus", - "verifiedAt", - "createdAt", - "receivingEnabled", - "receivingStatus" + "dkim_status", + "spf_status", + "dmarc_status", + "verified_at", + "created_at", + "receiving_enabled", + "receiving_status" ], "properties": { "id": { @@ -18519,33 +18648,33 @@ "example": "verified", "description": "pending, verifying, verified or failed" }, - "dkimStatus": { + "dkim_status": { "type": "string", "example": "verified" }, - "spfStatus": { + "spf_status": { "type": "string", "example": "verified" }, - "dmarcStatus": { + "dmarc_status": { "type": "string", "example": "pending" }, - "verifiedAt": { + "verified_at": { "type": [ "string", "null" ], "format": "date-time" }, - "createdAt": { + "created_at": { "type": "string", "format": "date-time" }, - "receivingEnabled": { + "receiving_enabled": { "type": "boolean" }, - "receivingStatus": { + "receiving_status": { "type": "string", "example": "disabled", "description": "disabled, pending, active or failed" diff --git a/src/smtpfast/types.py b/src/smtpfast/types.py index 82dc738..c175060 100644 --- a/src/smtpfast/types.py +++ b/src/smtpfast/types.py @@ -502,6 +502,7 @@ EmailList = TypedDict( "EmailList", { + "object": str, "data": list[EmailSummary], "page": NotRequired[int], "limit": int, @@ -690,6 +691,7 @@ "SegmentList", { "object": str, + "has_more": bool, "data": list[SegmentSummary], "total": int, "segment_limit": int, @@ -1128,6 +1130,8 @@ PendingSignupList = TypedDict( "PendingSignupList", { + "object": str, + "has_more": bool, "data": list[PendingSignup], "total": int, "undeliverable_total": int, @@ -1209,20 +1213,20 @@ ) -# A domain as the list returns it. The field names are camelCase here, unlike the rest of the API. +# A domain as the list returns it. GET /v1/domains/{id} has the DNS records and verification detail. DomainListItem = TypedDict( "DomainListItem", { "id": str, "domain": str, "status": str, - "dkimStatus": str, - "spfStatus": str, - "dmarcStatus": str, - "verifiedAt": Union[str, None], - "createdAt": str, - "receivingEnabled": bool, - "receivingStatus": str, + "dkim_status": str, + "spf_status": str, + "dmarc_status": str, + "verified_at": Union[str, None], + "created_at": str, + "receiving_enabled": bool, + "receiving_status": str, }, ) @@ -1933,6 +1937,7 @@ "ListInboxLabelsResponse", { "object": NotRequired[str], + "has_more": NotRequired[bool], "data": NotRequired[list[InboxLabel]], }, ) @@ -2048,6 +2053,8 @@ ListContactPropertiesResponse = TypedDict( "ListContactPropertiesResponse", { + "object": str, + "has_more": bool, "data": list[ContactProperty], }, ) @@ -2289,7 +2296,14 @@ ) -ListDomainsResponse: TypeAlias = list[DomainListItem] +ListDomainsResponse = TypedDict( + "ListDomainsResponse", + { + "object": str, + "has_more": bool, + "data": list[DomainListItem], + }, +) UpdateDomainReceivingParams = TypedDict( @@ -2378,7 +2392,14 @@ ) -ListApiKeysResponse: TypeAlias = list[ApiKey] +ListApiKeysResponse = TypedDict( + "ListApiKeysResponse", + { + "object": str, + "has_more": bool, + "data": list[ApiKey], + }, +) UpdateApiKeyParams = TypedDict( @@ -2409,7 +2430,14 @@ ) -ListWebhooksResponse: TypeAlias = list[Webhook] +ListWebhooksResponse = TypedDict( + "ListWebhooksResponse", + { + "object": str, + "has_more": bool, + "data": list[Webhook], + }, +) TestWebhookResponse = TypedDict( diff --git a/tests/test_live.py b/tests/test_live.py index 3194d43..b7d33d5 100644 --- a/tests/test_live.py +++ b/tests/test_live.py @@ -47,8 +47,8 @@ def test_emails(client): def test_domains(client): domains = client.domains.list() check("listDomains", domains) - if domains: - check("getDomain", client.domains.get(domains[0]["id"])) + if domains["data"]: + check("getDomain", client.domains.get(domains["data"][0]["id"])) @pytest.mark.parametrize(