From d0421e8164fdf590b680e50416927bcb6e9ce8ec Mon Sep 17 00:00:00 2001
From: bobbyonmagic <237182227+bobbyonmagic@users.noreply.github.com>
Date: Sun, 11 Oct 2026 04:21:30 +0000
Subject: [PATCH] Sync with the API spec
---
spec/openapi.json | 1429 ++++++++++++++++++++++++++++++++----
src/smtpfast/_resources.py | 36 +-
src/smtpfast/types.py | 452 +++++++++++-
3 files changed, 1730 insertions(+), 187 deletions(-)
diff --git a/spec/openapi.json b/spec/openapi.json
index 9f3f855..585b1c3 100644
--- a/spec/openapi.json
+++ b/spec/openapi.json
@@ -368,11 +368,11 @@
},
"responses": {
"200": {
- "description": "Email queued successfully",
+ "description": "Email accepted. A repeat with the same Idempotency-Key and body returns the same id.",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/Email"
+ "$ref": "#/components/schemas/SendEmailResponse"
}
}
}
@@ -1086,6 +1086,16 @@
"application/json": {
"schema": {
"type": "object",
+ "required": [
+ "object",
+ "start_date",
+ "end_date",
+ "timezone",
+ "metrics",
+ "dimensions",
+ "granularity",
+ "totals"
+ ],
"properties": {
"object": {
"type": "string",
@@ -1100,25 +1110,44 @@
"format": "date-time"
},
"timezone": {
- "type": "string"
+ "type": "string",
+ "example": "UTC"
},
"metrics": {
"type": "array",
"items": {
"type": "string"
- }
+ },
+ "example": [
+ "sent",
+ "delivered",
+ "open_rate"
+ ]
},
"dimensions": {
"type": "array",
"items": {
- "type": "string"
+ "type": "string",
+ "enum": [
+ "period",
+ "domain",
+ "email",
+ "broadcast"
+ ]
}
},
"granularity": {
- "type": "string"
+ "type": "string",
+ "enum": [
+ "hourly",
+ "daily",
+ "weekly",
+ "monthly"
+ ]
},
"totals": {
"type": "object",
+ "description": "One value per requested metric: counts are integers, rates percentages with one decimal.",
"additionalProperties": {
"type": "number"
},
@@ -1130,10 +1159,56 @@
},
"data": {
"type": "array",
- "description": "One row per combination of the requested dimensions. Absent when no dimensions are requested.",
+ "description": "One row per combination of the requested dimensions, with the requested metrics. Absent when no dimensions are requested.",
"items": {
"type": "object",
- "additionalProperties": true,
+ "properties": {
+ "period": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "With the period dimension"
+ },
+ "domain_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "With the domain dimension"
+ },
+ "domain_name": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "With the domain dimension"
+ },
+ "email_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "With the email dimension"
+ },
+ "broadcast_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "With the broadcast dimension"
+ },
+ "broadcast_name": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "With the broadcast dimension"
+ }
+ },
+ "additionalProperties": {
+ "type": "number"
+ },
"example": {
"period": "2026-07-01",
"domain_id": "dom_1",
@@ -5961,7 +6036,7 @@
"post": {
"operationId": "createContact",
"summary": "Create a contact",
- "description": "Create a global contact for the current account. Supports firstName/lastName and first_name/last_name aliases for Resend compatibility.",
+ "description": "Create one contact, or send { contacts: [...] } to create or update up to 500 at once. The single form returns 409 when the email already exists; the bulk form matches existing contacts by email and updates them. Supports firstName/lastName and first_name/last_name aliases for Resend compatibility.",
"tags": [
"Contacts"
],
@@ -5970,18 +6045,32 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/CreateContactRequest"
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/CreateContactRequest"
+ },
+ {
+ "$ref": "#/components/schemas/CreateContactBatchRequest"
+ }
+ ]
}
}
}
},
"responses": {
"201": {
- "description": "Contact created",
+ "description": "Contact created. The bulk form returns the created and updated contacts as a list.",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ContactCreated"
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/ContactCreated"
+ },
+ {
+ "$ref": "#/components/schemas/ContactBatchCreated"
+ }
+ ]
}
}
}
@@ -6119,6 +6208,16 @@
"maximum": 100,
"default": 50
}
+ },
+ {
+ "name": "page",
+ "in": "query",
+ "description": "Page number, starting at 1",
+ "schema": {
+ "type": "integer",
+ "minimum": 1,
+ "default": 1
+ }
}
],
"responses": {
@@ -6235,7 +6334,7 @@
},
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ContactList"
+ "$ref": "#/components/schemas/ContactExport"
}
}
}
@@ -6790,7 +6889,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ContactList"
+ "$ref": "#/components/schemas/SegmentContactList"
}
}
}
@@ -8295,7 +8394,7 @@
"post": {
"operationId": "addSuppressionsBatch",
"summary": "Suppress addresses in bulk",
- "description": "Resend-compatible: add 1 to 100 addresses as manual suppressions. An address already suppressed keeps its record. Requires the contact:write scope.",
+ "description": "Resend-compatible: add 1 to 100 addresses as manual suppressions of every send (scope all). An address already suppressed keeps its record, except that a marketing-only one becomes a manual suppression of every send. Requires the contact:write scope.",
"tags": [
"Suppressions"
],
@@ -8502,7 +8601,7 @@
"get": {
"operationId": "listSuppressions",
"summary": "List suppressions",
- "description": "Return suppressed addresses for the current account. Suppressed recipients are blocked before sending.",
+ "description": "Return suppressed addresses for the current account. Suppressed recipients are blocked before sending. A suppression with scope marketing blocks marketing email only; scope all blocks every send.",
"tags": [
"Suppressions"
],
@@ -8603,7 +8702,7 @@
"post": {
"operationId": "createSuppression",
"summary": "Create a manual suppression",
- "description": "Manually suppress an address so future sends to it are blocked.",
+ "description": "Manually suppress an address so future sends to it are blocked: every send by default, or marketing email only with scope marketing.",
"tags": [
"Suppressions"
],
@@ -9060,7 +9159,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/Broadcast"
+ "$ref": "#/components/schemas/BroadcastDetail"
}
}
}
@@ -9703,7 +9802,7 @@
"post": {
"operationId": "sendBroadcast",
"summary": "Send or schedule a broadcast",
- "description": "Snapshot eligible subscribed contacts, create one queued email per recipient, and send now or schedule within 30 days.",
+ "description": "Snapshot eligible subscribed contacts, create one queued email per recipient, and send now or schedule within 30 days. With send_over_minutes the recipients go out evenly spread over that window instead of all at once; a canceled broadcast stops the sends that have not gone out yet.",
"tags": [
"Broadcasts"
],
@@ -9728,6 +9827,13 @@
"scheduled_at": {
"type": "string",
"format": "date-time"
+ },
+ "send_over_minutes": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 4320,
+ "description": "Not in Resend. Spread the send evenly over this many minutes from the send time (or scheduled_at): recipient i of N goes out at start + i * window / N. 0 sends to everyone at once. 15 to 4320 (72 hours). Useful on a new sending domain or during the first week's warm-up, where each hour of the window must fit the hourly cap; the daily cap still counts the whole send. Left out, the broadcast's saved send_over_minutes applies; given, it is saved on the broadcast. The window must end within 30 days.",
+ "example": 240
}
}
}
@@ -9740,7 +9846,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/Broadcast"
+ "$ref": "#/components/schemas/BroadcastSendResult"
}
}
}
@@ -9954,7 +10060,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/Email"
+ "type": "object",
+ "required": [
+ "object",
+ "id",
+ "status"
+ ],
+ "properties": {
+ "object": {
+ "type": "string",
+ "example": "email"
+ },
+ "id": {
+ "type": "string",
+ "example": "email_abc123",
+ "description": "The test email, in GET /v1/emails/{id}"
+ },
+ "status": {
+ "type": "string",
+ "example": "queued"
+ }
+ }
}
}
}
@@ -12019,10 +12145,19 @@
"application/json": {
"schema": {
"type": "object",
+ "required": [
+ "success",
+ "message"
+ ],
"properties": {
"success": {
"type": "boolean",
"example": true
+ },
+ "message": {
+ "type": "string",
+ "example": "API key revoked",
+ "description": "\"Connected app disconnected\" for a connected app's key"
}
}
}
@@ -15116,6 +15251,28 @@
"items": {
"type": "string"
}
+ },
+ "read_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "When the message was marked read. Not in Resend."
+ },
+ "inbox_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The inbox this copy was filed into, when its address has one. Not in Resend."
+ },
+ "thread_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The inbox thread this copy was filed into. Not in Resend."
}
}
},
@@ -16066,9 +16223,19 @@
"description": "Send later, up to 30 days ahead (ISO 8601)",
"example": "2026-04-10T14:00:00Z"
},
+ "category": {
+ "type": "string",
+ "enum": [
+ "transactional",
+ "marketing"
+ ],
+ "default": "marketing",
+ "description": "Defaults to marketing. Label receipts, password resets and other transactional mail transactional so a marketing opt-out does not block them; unlabelled mail is treated as marketing. Over SMTP, set the X-SMTPfast-Category header instead; it is removed before sending. An email whose html or text (for a template, the rendered body) contains an unsubscribe placeholder is always marketing. A marketing email carries the one-click List-Unsubscribe header and is not sent to anyone who unsubscribed. A transactional email has no List-Unsubscribe header and still goes to addresses whose suppression has scope marketing (a recipient's own unsubscribe, or a manual suppression created with scope marketing), but not to addresses with scope all: hard bounces, complaints, and manual suppressions, which block everything by default. Transactional means mail the recipient needs because of an action or account; labelling promotions transactional is not allowed. Any other value is refused with 400.",
+ "example": "transactional"
+ },
"html": {
"type": "string",
- "description": "HTML body of the email. Include `{{unsubscribe_url}}` (or the Resend-compatible `{{{RESEND_UNSUBSCRIBE_URL}}}`) anywhere in the body and SMTPfast substitutes the per-recipient unsubscribe link at send time. The same URL is set on the RFC 8058 `List-Unsubscribe` header automatically.",
+ "description": "HTML body of the email. Include `{{unsubscribe_url}}` (or the Resend-compatible `{{{RESEND_UNSUBSCRIBE_URL}}}`) anywhere in the body and SMTPfast substitutes the per-recipient unsubscribe link at send time. An email with the placeholder is always marketing, and a marketing email gets the same URL on the RFC 8058 `List-Unsubscribe` header.",
"example": "
Hello!
Unsubscribe
"
},
"text": {
@@ -16078,16 +16245,48 @@
}
}
},
+ "SendEmailResponse": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "description": "The id of the accepted email, as Resend returns it. Fetch the email with GET /v1/emails/{id}.",
+ "properties": {
+ "id": {
+ "type": "string",
+ "example": "email_abc123"
+ }
+ }
+ },
"Email": {
"type": "object",
"required": [
"id",
"from",
"to",
+ "cc",
+ "bcc",
"subject",
- "status",
+ "html",
+ "text",
+ "created_at",
+ "last_event",
+ "body_cleared_at",
"source",
- "created_at"
+ "category",
+ "status",
+ "reply_to",
+ "tags",
+ "message_id",
+ "ses_message_id",
+ "sent_at",
+ "delivered_at",
+ "opened_at",
+ "clicked_at",
+ "bounced_at",
+ "unsubscribed_at",
+ "error_message",
+ "events"
],
"properties": {
"id": {
@@ -16102,13 +16301,26 @@
"to": {
"type": "array",
"items": {
- "type": "string",
- "format": "email"
+ "type": "string"
},
"example": [
"user@example.com"
]
},
+ "cc": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "example": []
+ },
+ "bcc": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "example": []
+ },
"subject": {
"type": "string",
"example": "Welcome!"
@@ -16117,13 +16329,47 @@
"type": [
"string",
"null"
- ]
+ ],
+ "description": "The stored body. For an API send the {{unsubscribe_url}} placeholder is filled in when the email goes out, so it appears here unreplaced; broadcast and form emails are stored with the real link. Null when there was no HTML body or the retention window removed it."
},
"text": {
"type": [
"string",
"null"
- ]
+ ],
+ "description": "The stored plain-text body, null when there was none or the retention window removed it."
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time",
+ "example": "2026-04-05T12:00:00Z"
+ },
+ "last_event": {
+ "type": "string",
+ "example": "delivered",
+ "description": "The type of the newest event, or the status when the email has no events yet. Resend-compatible."
+ },
+ "body_cleared_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "When the retention window removed html and text. Null while the body is kept, so null html and text with a timestamp here means the content is no longer stored."
+ },
+ "source": {
+ "type": "string",
+ "example": "api",
+ "description": "api, smtp, dashboard, broadcast or broadcast_test"
+ },
+ "category": {
+ "type": "string",
+ "enum": [
+ "transactional",
+ "marketing"
+ ],
+ "example": "transactional",
+ "description": "Decides which suppressions apply and whether the List-Unsubscribe header went out. marketing unless the email was labelled transactional; broadcasts, signup form emails and emails sent before categories existed are marketing."
},
"status": {
"type": "string",
@@ -16142,58 +16388,175 @@
],
"example": "queued"
},
- "source": {
- "type": "string",
- "example": "api",
- "description": "api, smtp, dashboard, broadcast or broadcast_test"
- },
- "created_at": {
- "type": "string",
- "format": "date-time",
- "example": "2026-04-05T12:00:00Z"
- }
- }
- },
- "EmailSummary": {
- "type": "object",
- "required": [
- "id",
- "from",
- "to",
- "subject",
- "status",
- "source",
- "created_at",
- "sent_at",
- "delivered_at",
- "opened_at",
- "clicked_at",
- "bounced_at",
- "error_message"
- ],
- "properties": {
- "id": {
- "type": "string",
- "example": "email_abc123"
- },
- "from": {
- "type": "string",
- "example": "hello@yourapp.com"
+ "reply_to": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The Reply-To address the email was sent with, or null."
},
- "to": {
+ "tags": {
"type": "array",
+ "description": "The tags given at send time, as name/value pairs. Empty when none were given.",
"items": {
- "type": "string"
+ "type": "object",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ }
},
"example": [
- "user@example.com"
+ {
+ "name": "campaign",
+ "value": "welcome"
+ }
]
},
- "subject": {
- "type": "string",
- "example": "Welcome!"
+ "message_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The RFC 5322 Message-ID on your sending domain, the one a recipient's server and bounce reports quote. Null until the email is sent."
},
- "status": {
+ "ses_message_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The Amazon SES message id. Kept for older integrations; correlate with message_id."
+ },
+ "sent_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "delivered_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "opened_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "clicked_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "bounced_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "unsubscribed_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "When the recipient unsubscribed through this email. status keeps its delivery value."
+ },
+ "error_message": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "events": {
+ "type": "array",
+ "description": "Every event of the email, oldest first.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "metadata",
+ "timestamp"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "example": "delivered",
+ "description": "queued, scheduled, sending, retrying, sent, delivered, delivery_delayed, bounced, complained, opened, clicked, failed, suppressed or unsubscribed"
+ },
+ "metadata": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "additionalProperties": true,
+ "description": "Event details such as the bounce type or the clicked URL"
+ },
+ "timestamp": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ }
+ }
+ }
+ },
+ "EmailSummary": {
+ "type": "object",
+ "required": [
+ "id",
+ "from",
+ "to",
+ "subject",
+ "status",
+ "source",
+ "category",
+ "created_at",
+ "sent_at",
+ "delivered_at",
+ "opened_at",
+ "clicked_at",
+ "bounced_at",
+ "error_message"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "example": "email_abc123"
+ },
+ "from": {
+ "type": "string",
+ "example": "hello@yourapp.com"
+ },
+ "to": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "example": [
+ "user@example.com"
+ ]
+ },
+ "subject": {
+ "type": "string",
+ "example": "Welcome!"
+ },
+ "status": {
"type": "string",
"example": "delivered",
"description": "queued, scheduled, sending, sent, delivered, opened, clicked, bounced, complained, failed or canceled"
@@ -16203,6 +16566,14 @@
"example": "api",
"description": "api, smtp, dashboard, broadcast or broadcast_test"
},
+ "category": {
+ "type": "string",
+ "enum": [
+ "transactional",
+ "marketing"
+ ],
+ "example": "marketing"
+ },
"created_at": {
"type": "string",
"format": "date-time"
@@ -16573,13 +16944,240 @@
"type": "string",
"example": "clx_contact123"
},
- "deleted": {
- "type": "boolean",
- "example": true
+ "deleted": {
+ "type": "boolean",
+ "example": true
+ }
+ }
+ },
+ "ContactList": {
+ "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/ContactListItem"
+ }
+ },
+ "page": {
+ "type": "integer",
+ "example": 1
+ },
+ "page_size": {
+ "type": "integer",
+ "example": 50
+ },
+ "filtered_total": {
+ "type": "integer",
+ "example": 1240,
+ "description": "Contacts matching the filters"
+ },
+ "total": {
+ "type": "integer",
+ "example": 1240,
+ "description": "Every contact of the team"
+ },
+ "subscribed": {
+ "type": "integer",
+ "example": 1198
+ },
+ "unsubscribed": {
+ "type": "integer",
+ "example": 42
+ },
+ "disposable": {
+ "type": "integer",
+ "example": 3,
+ "description": "Subscribed contacts on throwaway-mailbox domains"
+ },
+ "new_7d": {
+ "type": "integer",
+ "example": 18,
+ "description": "Subscribed contacts added in the last 7 days"
+ },
+ "new_30d": {
+ "type": "integer",
+ "example": 77,
+ "description": "Subscribed contacts added in the last 30 days"
+ },
+ "contact_limit": {
+ "type": "integer",
+ "example": 25000,
+ "description": "The plan's contact limit, with any override for the account"
+ },
+ "contact_limit_base": {
+ "type": "integer",
+ "example": 25000,
+ "description": "The plan's contact limit without overrides"
+ },
+ "tier": {
+ "type": "string",
+ "example": "pro"
+ }
+ }
+ },
+ "ContactListItem": {
+ "description": "A contact as lists return it: the summary fields plus properties, segments and the suppression.",
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/ContactSummary"
+ },
+ {
+ "type": "object",
+ "required": [
+ "properties",
+ "segments",
+ "suppression"
+ ],
+ "properties": {
+ "properties": {
+ "type": "object",
+ "additionalProperties": true,
+ "example": {
+ "company": "Acme"
+ }
+ },
+ "suppression": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ContactSuppression"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ }
+ }
+ ]
+ },
+ "ContactExport": {
+ "type": "object",
+ "required": [
+ "object",
+ "has_more",
+ "data",
+ "exported_at"
+ ],
+ "properties": {
+ "object": {
+ "type": "string",
+ "example": "list"
+ },
+ "has_more": {
+ "type": "boolean",
+ "example": false,
+ "description": "True when more than 10,000 contacts matched and the export stopped there"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/ContactListItem"
+ }
+ },
+ "exported_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
+ "SegmentContactList": {
+ "type": "object",
+ "required": [
+ "object",
+ "has_more",
+ "data",
+ "segment",
+ "page",
+ "page_size",
+ "total"
+ ],
+ "properties": {
+ "object": {
+ "type": "string",
+ "example": "list"
+ },
+ "has_more": {
+ "type": "boolean",
+ "example": false
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/ContactListItem"
+ }
+ },
+ "segment": {
+ "type": "object",
+ "required": [
+ "id",
+ "name"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "example": "seg_product"
+ },
+ "name": {
+ "type": "string",
+ "example": "Product updates"
+ }
+ }
+ },
+ "page": {
+ "type": "integer",
+ "example": 1
+ },
+ "page_size": {
+ "type": "integer",
+ "example": 50
+ },
+ "total": {
+ "type": "integer",
+ "example": 128,
+ "description": "Contacts in the segment"
+ }
+ }
+ },
+ "CreateContactBatchRequest": {
+ "type": "object",
+ "required": [
+ "contacts"
+ ],
+ "description": "Create or update up to 500 contacts, matched by email. Fields sent replace the stored ones; fields left out stay. segment_ids only adds memberships.",
+ "properties": {
+ "contacts": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 500,
+ "items": {
+ "$ref": "#/components/schemas/CreateContactRequest"
+ }
+ },
+ "segment_ids": {
+ "type": "array",
+ "maxItems": 50,
+ "description": "Manual segments to add every contact to, for contacts that do not name their own",
+ "items": {
+ "type": "string"
+ }
}
}
},
- "ContactList": {
+ "ContactBatchCreated": {
"type": "object",
"required": [
"object",
@@ -16600,11 +17198,6 @@
"items": {
"$ref": "#/components/schemas/ContactSummary"
}
- },
- "disposable": {
- "type": "integer",
- "example": 3,
- "description": "Subscribed contacts on throwaway-mailbox domains"
}
}
},
@@ -16657,12 +17250,36 @@
},
"suppressed": {
"type": "boolean",
- "example": false
+ "example": false,
+ "description": "The address is on the suppression list"
},
"segments": {
"type": "array",
+ "description": "The manual segments the contact is in. Lists and GET /v1/contacts/{id} return it.",
"items": {
- "$ref": "#/components/schemas/SegmentSummary"
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "color"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "example": "seg_product"
+ },
+ "name": {
+ "type": "string",
+ "example": "Product updates"
+ },
+ "color": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "example": "#10b981"
+ }
+ }
}
}
}
@@ -16852,6 +17469,7 @@
"id",
"reason",
"source",
+ "scope",
"created_at"
],
"properties": {
@@ -16867,6 +17485,15 @@
"type": "string",
"example": "ses"
},
+ "scope": {
+ "type": "string",
+ "enum": [
+ "all",
+ "marketing"
+ ],
+ "example": "all",
+ "description": "all: every send to the address is blocked. marketing: only marketing email is blocked, and email labelled transactional still goes out. A recipient's own unsubscribe is marketing. Hard bounces, complaints and unsubscribes recorded before marketing-only unsubscribes existed are all. A manual suppression is all unless it was created with scope marketing."
+ },
"created_at": {
"type": "string",
"format": "date-time"
@@ -16891,6 +17518,15 @@
],
"default": "manual"
},
+ "scope": {
+ "type": "string",
+ "enum": [
+ "all",
+ "marketing"
+ ],
+ "default": "all",
+ "description": "all blocks every send to the address. marketing blocks marketing email only, like a recipient's own unsubscribe. An address already on the list keeps its suppression, except that scope all turns a marketing-only one into a manual suppression of every send."
+ },
"diagnostic_code": {
"type": "string",
"description": "Optional internal note for manual suppressions",
@@ -16919,6 +17555,36 @@
"items": {
"$ref": "#/components/schemas/Suppression"
}
+ },
+ "page": {
+ "type": "integer",
+ "example": 1
+ },
+ "page_size": {
+ "type": "integer",
+ "example": 50
+ },
+ "filtered_total": {
+ "type": "integer",
+ "example": 412,
+ "description": "Suppressions matching q and reason"
+ },
+ "total": {
+ "type": "integer",
+ "example": 412,
+ "description": "Every suppression of the team"
+ },
+ "hard_bounces": {
+ "type": "integer",
+ "example": 388
+ },
+ "complaints": {
+ "type": "integer",
+ "example": 19
+ },
+ "manual": {
+ "type": "integer",
+ "example": 5
}
}
},
@@ -16929,6 +17595,7 @@
"email",
"reason",
"source",
+ "scope",
"created_at"
],
"properties": {
@@ -16959,6 +17626,15 @@
"type": "string",
"example": "ses"
},
+ "scope": {
+ "type": "string",
+ "enum": [
+ "all",
+ "marketing"
+ ],
+ "example": "all",
+ "description": "all: every send to the address is blocked. marketing: only marketing email is blocked, and email labelled transactional still goes out. A recipient's own unsubscribe is marketing. Hard bounces, complaints and unsubscribes recorded before marketing-only unsubscribes existed are all. A manual suppression is all unless it was created with scope marketing."
+ },
"bounce_type": {
"type": [
"string",
@@ -17132,6 +17808,17 @@
"track_clicks": {
"type": "boolean",
"default": true
+ },
+ "send_over_minutes": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0,
+ "maximum": 4320,
+ "default": 0,
+ "description": "Not in Resend. Spread the send evenly over this many minutes from the send time (or scheduled_at): recipient i of N goes out at start + i * window / N. 0 sends to everyone at once. 15 to 4320 (72 hours). Useful on a new sending domain or during the first week's warm-up, where each hour of the window must fit the hourly cap; the daily cap still counts the whole send. null clears it.",
+ "example": 240
}
}
},
@@ -17164,6 +17851,24 @@
"type": "boolean",
"example": false
},
+ "page": {
+ "type": "integer",
+ "example": 1
+ },
+ "page_size": {
+ "type": "integer",
+ "example": 50
+ },
+ "filtered_total": {
+ "type": "integer",
+ "example": 1,
+ "description": "Broadcasts matching the status filter"
+ },
+ "total": {
+ "type": "integer",
+ "example": 1,
+ "description": "Every broadcast of the team"
+ },
"status_counts": {
"type": "object",
"additionalProperties": {
@@ -17258,89 +17963,314 @@
],
"example": "draft"
},
- "scheduled_at": {
+ "scheduled_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "queued_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "sent_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "canceled_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "paused_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "Set when the campaign was stopped mid-send by the deliverability guard"
+ },
+ "pause_reason": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Why the campaign was paused, for example the complaint rate that tripped the guard"
+ },
+ "fanout_completed_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "When every recipient of the send had been created"
+ },
+ "expected_recipients": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "Eligible recipients counted when the send was accepted"
+ },
+ "send_over_minutes": {
+ "type": "integer",
+ "example": 0,
+ "description": "Minutes the send is spread over from its start (scheduled_at, else queued_at); 0 means all at once"
+ },
+ "track_opens": {
+ "type": "boolean",
+ "example": true
+ },
+ "track_clicks": {
+ "type": "boolean",
+ "example": true
+ },
+ "recipient_count": {
+ "type": "integer",
+ "example": 1250
+ },
+ "metrics": {
+ "$ref": "#/components/schemas/BroadcastMetrics"
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
+ "BroadcastDetail": {
+ "description": "A broadcast with its latest 250 recipients and the links clicked in it.",
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/Broadcast"
+ },
+ {
+ "type": "object",
+ "required": [
+ "recipients",
+ "links"
+ ],
+ "properties": {
+ "recipients": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/BroadcastRecipientDetail"
+ }
+ },
+ "links": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "url",
+ "clicks"
+ ],
+ "properties": {
+ "url": {
+ "type": "string",
+ "example": "https://example.com/pricing"
+ },
+ "clicks": {
+ "type": "integer",
+ "example": 42
+ }
+ }
+ }
+ }
+ }
+ }
+ ]
+ },
+ "BroadcastRecipientDetail": {
+ "type": "object",
+ "required": [
+ "id",
+ "email",
+ "status",
+ "events"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "example": "clx_recipient123"
+ },
+ "email": {
+ "type": "string",
+ "example": "ada@example.com"
+ },
+ "first_name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "last_name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "example": "delivered"
+ },
+ "skip_reason": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Why the contact was left out: unsubscribed, suppressed or invalid"
+ },
+ "email_id": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "queued_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "sent_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "delivered_at": {
"type": [
"string",
"null"
],
"format": "date-time"
},
- "queued_at": {
+ "opened_at": {
"type": [
"string",
"null"
],
"format": "date-time"
},
- "sent_at": {
+ "clicked_at": {
"type": [
"string",
"null"
],
"format": "date-time"
},
- "canceled_at": {
+ "bounced_at": {
"type": [
"string",
"null"
],
"format": "date-time"
},
- "paused_at": {
+ "complained_at": {
"type": [
"string",
"null"
],
- "format": "date-time",
- "description": "Set when the campaign was stopped mid-send by the deliverability guard"
+ "format": "date-time"
},
- "pause_reason": {
+ "unsubscribed_at": {
"type": [
"string",
"null"
],
- "description": "Why the campaign was paused, for example the complaint rate that tripped the guard"
+ "format": "date-time"
},
- "fanout_completed_at": {
+ "failed_at": {
"type": [
"string",
"null"
],
- "format": "date-time",
- "description": "When every recipient of the send had been created"
+ "format": "date-time"
},
- "expected_recipients": {
+ "error_message": {
"type": [
- "integer",
+ "string",
"null"
- ],
- "description": "Eligible recipients counted when the send was accepted"
- },
- "track_opens": {
- "type": "boolean",
- "example": true
- },
- "track_clicks": {
- "type": "boolean",
- "example": true
- },
- "recipient_count": {
- "type": "integer",
- "example": 1250
- },
- "metrics": {
- "$ref": "#/components/schemas/BroadcastMetrics"
- },
- "created_at": {
- "type": "string",
- "format": "date-time"
+ ]
},
- "updated_at": {
- "type": "string",
- "format": "date-time"
+ "events": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "id",
+ "type",
+ "created_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "example": "delivered"
+ },
+ "metadata": {
+ "type": "object",
+ "additionalProperties": true
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ }
}
}
},
+ "BroadcastSendResult": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/Broadcast"
+ },
+ {
+ "type": "object",
+ "required": [
+ "object",
+ "recipients",
+ "skipped",
+ "batch_id"
+ ],
+ "properties": {
+ "recipients": {
+ "type": "integer",
+ "example": 1240,
+ "description": "Contacts the send will reach"
+ },
+ "skipped": {
+ "type": "integer",
+ "example": 18,
+ "description": "Contacts left out: unsubscribed, suppressed or invalid"
+ },
+ "batch_id": {
+ "type": "string",
+ "example": "batch_abc123"
+ }
+ }
+ }
+ ]
+ },
"BroadcastMetrics": {
"type": "object",
"properties": {
@@ -17360,6 +18290,11 @@
"type": "integer",
"example": 0
},
+ "handed_off": {
+ "type": "integer",
+ "example": 1238,
+ "description": "Recipients whose email the provider accepted, whatever happened next. Shows how far a send spread over send_over_minutes has got."
+ },
"sent": {
"type": "integer",
"example": 1238
@@ -18687,8 +19622,10 @@
"id",
"domain",
"status",
- "dns_records"
+ "dns_records",
+ "created_at"
],
+ "description": "POST /v1/domains returns id, domain, status, dns_records and created_at. GET /v1/domains/{id} adds the verification detail, a live DNS check of each record and the receiving settings.",
"properties": {
"id": {
"type": "string",
@@ -18702,35 +19639,193 @@
"type": "string",
"enum": [
"pending",
+ "verifying",
"verified",
"failed"
],
- "example": "pending"
+ "example": "pending",
+ "description": "GET /v1/domains/{id} answers pending for a verified domain whose required records are no longer live."
},
- "receivingEnabled": {
- "type": "boolean",
- "description": "Whether inbound email is turned on for the domain (list responses)."
+ "receiving": {
+ "type": "object",
+ "description": "Inbound email for the domain",
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "status": {
+ "type": "string",
+ "enum": [
+ "disabled",
+ "pending",
+ "active",
+ "failed"
+ ]
+ },
+ "available": {
+ "type": "boolean",
+ "description": "The team's plan includes inbound email"
+ },
+ "eligible": {
+ "type": "boolean",
+ "description": "The domain can turn receiving on"
+ },
+ "mx_record": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "example": "MX"
+ },
+ "priority": {
+ "type": "integer",
+ "example": 10
+ },
+ "value": {
+ "type": "string"
+ }
+ }
+ },
+ "checked_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ }
+ }
},
- "receivingStatus": {
+ "verification_phase": {
"type": "string",
"enum": [
- "disabled",
- "pending",
- "active",
+ "verified",
+ "dns_missing",
+ "waiting_on_ses",
+ "mail_from_pending",
"failed"
+ ]
+ },
+ "verification_message": {
+ "type": "string"
+ },
+ "next_auto_check_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "Null once the domain is verified"
+ },
+ "dkim_status": {
+ "type": "string",
+ "example": "verified"
+ },
+ "spf_status": {
+ "type": "string",
+ "example": "verified"
+ },
+ "dmarc_status": {
+ "type": "string",
+ "example": "pending"
+ },
+ "mail_from_status": {
+ "type": "string",
+ "example": "verified"
+ },
+ "mail_from_domain": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "example": "bounce.yourapp.com"
+ },
+ "verified_at": {
+ "type": [
+ "string",
+ "null"
],
- "description": "Inbound status (list responses)."
+ "format": "date-time"
+ },
+ "region": {
+ "type": "string",
+ "example": "us-east-1"
+ },
+ "provider": {
+ "type": "object",
+ "description": "The DNS provider found from the NS records",
+ "properties": {
+ "id": {
+ "type": "string",
+ "example": "cloudflare"
+ },
+ "name": {
+ "type": "string",
+ "example": "Cloudflare"
+ },
+ "badge": {
+ "type": "string"
+ },
+ "zone": {
+ "type": "string"
+ }
+ }
+ },
+ "events": {
+ "type": "array",
+ "description": "The verification steps and when each was done",
+ "items": {
+ "type": "object",
+ "properties": {
+ "key": {
+ "type": "string",
+ "example": "dns_live"
+ },
+ "label": {
+ "type": "string",
+ "example": "DNS records live"
+ },
+ "at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "done": {
+ "type": "boolean"
+ }
+ }
+ }
},
"dns_records": {
"type": "array",
"items": {
"$ref": "#/components/schemas/DnsRecord"
}
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "last_checked_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
}
}
},
"DnsRecord": {
"type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
"properties": {
"type": {
"type": "string",
@@ -18743,11 +19838,79 @@
},
"name": {
"type": "string",
- "example": "smtp1._domainkey.yourapp.com"
+ "example": "abc123._domainkey.yourapp.com"
},
"value": {
"type": "string",
- "example": "smtp1.dkim.smtpfa.st"
+ "example": "abc123.dkim.amazonses.com"
+ },
+ "ttl": {
+ "type": "string",
+ "example": "Auto"
+ },
+ "priority": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "Set for MX records"
+ },
+ "status": {
+ "type": "string",
+ "example": "pending",
+ "description": "The verification status that covers this record"
+ },
+ "purpose": {
+ "type": "string",
+ "example": "DKIM authentication",
+ "description": "DKIM authentication, SPF authorization, DMARC policy, MAIL FROM bounce delivery, MAIL FROM SPF or Inbound mail"
+ },
+ "category": {
+ "type": "string",
+ "enum": [
+ "verification",
+ "sending",
+ "receiving"
+ ]
+ },
+ "actual_value": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "GET /v1/domains/{id} only: what DNS answers now, several values joined with \" | \""
+ },
+ "live_check": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "enum": [
+ "match",
+ "mismatch",
+ "missing",
+ "needs_include",
+ "multiple_spf_records",
+ "invalid_spf",
+ null
+ ],
+ "description": "GET /v1/domains/{id} only: how the live value compares with the expected one. Null for the inbound MX record."
+ },
+ "actualValue": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "deprecated": true,
+ "description": "Same as actual_value, for older clients"
+ },
+ "liveCheck": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "deprecated": true,
+ "description": "Same as live_check, for older clients"
}
}
},
diff --git a/src/smtpfast/_resources.py b/src/smtpfast/_resources.py
index c7b1826..34dbfae 100644
--- a/src/smtpfast/_resources.py
+++ b/src/smtpfast/_resources.py
@@ -83,7 +83,7 @@ def list_all(self, *, limit: int | None = None, status: str | None = None, to: s
"""Every item of list, fetching the following pages as you iterate."""
return self._core.paginate("GET", "/v1/emails", path_params=None, query={"limit": limit, "status": status, "to": to, "after": after}, body=None, options=options)
- def send(self, params: SendEmailRequest, *, idempotency_key: str | None = None, options: RequestOptions | None = None) -> Email:
+ def send(self, params: SendEmailRequest, *, idempotency_key: str | None = None, options: RequestOptions | None = None) -> SendEmailResponse:
"""Send an email. `POST /v1/emails`"""
return self._core.request("POST", "/v1/emails", path_params=None, query=None, body=params, idempotent=True, idempotency_key=idempotency_key, options=options) # type: ignore[no-any-return]
@@ -305,11 +305,11 @@ def __init__(self, core: Any) -> None:
super().__init__(core)
self.segments = ContactsSegmentsResource(core)
- def list(self, *, q: str | None = None, status: Literal["all", "subscribed", "unsubscribed"] | None = None, disposable: bool | None = None, segment_id: str | None = None, limit: int | None = None, options: RequestOptions | None = None) -> ContactList:
+ def list(self, *, q: str | None = None, status: Literal["all", "subscribed", "unsubscribed"] | None = None, disposable: bool | None = None, segment_id: str | None = None, limit: int | None = None, page: int | None = None, options: RequestOptions | None = None) -> ContactList:
"""List contacts. `GET /v1/contacts`"""
- return self._core.request("GET", "/v1/contacts", path_params=None, query={"q": q, "status": status, "disposable": disposable, "segment_id": segment_id, "limit": limit}, body=None, options=options) # type: ignore[no-any-return]
+ return self._core.request("GET", "/v1/contacts", path_params=None, query={"q": q, "status": status, "disposable": disposable, "segment_id": segment_id, "limit": limit, "page": page}, body=None, options=options) # type: ignore[no-any-return]
- def create(self, params: CreateContactRequest, *, options: RequestOptions | None = None) -> ContactCreated:
+ def create(self, params: CreateContactParams, *, options: RequestOptions | None = None) -> CreateContactResponse:
"""Create a contact. `POST /v1/contacts`"""
return self._core.request("POST", "/v1/contacts", path_params=None, query=None, body=params, options=options) # type: ignore[no-any-return]
@@ -317,7 +317,7 @@ def delete_many(self, params: DeleteContactsParams, *, options: RequestOptions |
"""Delete contacts in bulk. `DELETE /v1/contacts`"""
return self._core.request("DELETE", "/v1/contacts", path_params=None, query=None, body=params, options=options) # type: ignore[no-any-return]
- def export(self, *, format: Literal["csv", "json"] | None = None, status: Literal["all", "subscribed", "unsubscribed"] | None = None, disposable: bool | None = None, options: RequestOptions | None = None) -> ContactList:
+ def export(self, *, format: Literal["csv", "json"] | None = None, status: Literal["all", "subscribed", "unsubscribed"] | None = None, disposable: bool | None = None, options: RequestOptions | None = None) -> ContactExport:
"""Export contacts. `GET /v1/contacts/export`"""
return self._core.request("GET", "/v1/contacts/export", path_params=None, query={"format": format if format is not None else "json", "status": status, "disposable": disposable}, body=None, options=options) # type: ignore[no-any-return]
@@ -337,7 +337,7 @@ def delete(self, id: str, *, options: RequestOptions | None = None) -> ContactDe
class SegmentsContactsResource(_SyncResource):
"""`client.segments.contacts`"""
- def list(self, id: str, *, limit: int | None = None, page: int | None = None, options: RequestOptions | None = None) -> ContactList:
+ def list(self, id: str, *, limit: int | None = None, page: int | None = None, options: RequestOptions | None = None) -> SegmentContactList:
"""List contacts in a segment. `GET /v1/segments/{id}/contacts`"""
return self._core.request("GET", "/v1/segments/{id}/contacts", path_params={"id": id}, query={"limit": limit, "page": page}, body=None, options=options) # type: ignore[no-any-return]
@@ -478,7 +478,7 @@ def create(self, params: CreateBroadcastRequest, *, options: RequestOptions | No
"""Create a broadcast draft. `POST /v1/broadcasts`"""
return self._core.request("POST", "/v1/broadcasts", path_params=None, query=None, body=params, options=options) # type: ignore[no-any-return]
- def get(self, id: str, *, options: RequestOptions | None = None) -> Broadcast:
+ def get(self, id: str, *, options: RequestOptions | None = None) -> BroadcastDetail:
"""Get broadcast details. `GET /v1/broadcasts/{id}`"""
return self._core.request("GET", "/v1/broadcasts/{id}", path_params={"id": id}, query=None, body=None, options=options) # type: ignore[no-any-return]
@@ -502,7 +502,7 @@ def duplicate(self, id: str, *, options: RequestOptions | None = None) -> Duplic
"""Duplicate broadcast. `POST /v1/broadcasts/{id}/duplicate`"""
return self._core.request("POST", "/v1/broadcasts/{id}/duplicate", path_params={"id": id}, query=None, body=None, options=options) # type: ignore[no-any-return]
- def send(self, id: str, params: SendBroadcastParams | None = None, *, options: RequestOptions | None = None) -> Broadcast:
+ def send(self, id: str, params: SendBroadcastParams | None = None, *, options: RequestOptions | None = None) -> BroadcastSendResult:
"""Send or schedule a broadcast. `POST /v1/broadcasts/{id}/send`"""
return self._core.request("POST", "/v1/broadcasts/{id}/send", path_params={"id": id}, query=None, body=params, options=options) # type: ignore[no-any-return]
@@ -510,7 +510,7 @@ def cancel(self, id: str, *, options: RequestOptions | None = None) -> Broadcast
"""Cancel a scheduled broadcast. `POST /v1/broadcasts/{id}/cancel`"""
return self._core.request("POST", "/v1/broadcasts/{id}/cancel", path_params={"id": id}, query=None, body=None, options=options) # type: ignore[no-any-return]
- def send_test(self, id: str, params: SendBroadcastTestParams, *, options: RequestOptions | None = None) -> Email:
+ def send_test(self, id: str, params: SendBroadcastTestParams, *, options: RequestOptions | None = None) -> SendBroadcastTestResponse:
"""Send a test broadcast email. `POST /v1/broadcasts/{id}/test`"""
return self._core.request("POST", "/v1/broadcasts/{id}/test", path_params={"id": id}, query=None, body=params, options=options) # type: ignore[no-any-return]
@@ -865,7 +865,7 @@ def list_all(self, *, limit: int | None = None, status: str | None = None, to: s
"""Every item of list, fetching the following pages as you iterate."""
return self._core.paginate("GET", "/v1/emails", path_params=None, query={"limit": limit, "status": status, "to": to, "after": after}, body=None, options=options)
- async def send(self, params: SendEmailRequest, *, idempotency_key: str | None = None, options: RequestOptions | None = None) -> Email:
+ async def send(self, params: SendEmailRequest, *, idempotency_key: str | None = None, options: RequestOptions | None = None) -> SendEmailResponse:
"""Send an email. `POST /v1/emails`"""
return await self._core.request("POST", "/v1/emails", path_params=None, query=None, body=params, idempotent=True, idempotency_key=idempotency_key, options=options) # type: ignore[no-any-return]
@@ -1087,11 +1087,11 @@ def __init__(self, core: Any) -> None:
super().__init__(core)
self.segments = AsyncContactsSegmentsResource(core)
- async def list(self, *, q: str | None = None, status: Literal["all", "subscribed", "unsubscribed"] | None = None, disposable: bool | None = None, segment_id: str | None = None, limit: int | None = None, options: RequestOptions | None = None) -> ContactList:
+ async def list(self, *, q: str | None = None, status: Literal["all", "subscribed", "unsubscribed"] | None = None, disposable: bool | None = None, segment_id: str | None = None, limit: int | None = None, page: int | None = None, options: RequestOptions | None = None) -> ContactList:
"""List contacts. `GET /v1/contacts`"""
- return await self._core.request("GET", "/v1/contacts", path_params=None, query={"q": q, "status": status, "disposable": disposable, "segment_id": segment_id, "limit": limit}, body=None, options=options) # type: ignore[no-any-return]
+ return await self._core.request("GET", "/v1/contacts", path_params=None, query={"q": q, "status": status, "disposable": disposable, "segment_id": segment_id, "limit": limit, "page": page}, body=None, options=options) # type: ignore[no-any-return]
- async def create(self, params: CreateContactRequest, *, options: RequestOptions | None = None) -> ContactCreated:
+ async def create(self, params: CreateContactParams, *, options: RequestOptions | None = None) -> CreateContactResponse:
"""Create a contact. `POST /v1/contacts`"""
return await self._core.request("POST", "/v1/contacts", path_params=None, query=None, body=params, options=options) # type: ignore[no-any-return]
@@ -1099,7 +1099,7 @@ async def delete_many(self, params: DeleteContactsParams, *, options: RequestOpt
"""Delete contacts in bulk. `DELETE /v1/contacts`"""
return await self._core.request("DELETE", "/v1/contacts", path_params=None, query=None, body=params, options=options) # type: ignore[no-any-return]
- async def export(self, *, format: Literal["csv", "json"] | None = None, status: Literal["all", "subscribed", "unsubscribed"] | None = None, disposable: bool | None = None, options: RequestOptions | None = None) -> ContactList:
+ async def export(self, *, format: Literal["csv", "json"] | None = None, status: Literal["all", "subscribed", "unsubscribed"] | None = None, disposable: bool | None = None, options: RequestOptions | None = None) -> ContactExport:
"""Export contacts. `GET /v1/contacts/export`"""
return await self._core.request("GET", "/v1/contacts/export", path_params=None, query={"format": format if format is not None else "json", "status": status, "disposable": disposable}, body=None, options=options) # type: ignore[no-any-return]
@@ -1119,7 +1119,7 @@ async def delete(self, id: str, *, options: RequestOptions | None = None) -> Con
class AsyncSegmentsContactsResource(_AsyncResource):
"""`client.segments.contacts`"""
- async def list(self, id: str, *, limit: int | None = None, page: int | None = None, options: RequestOptions | None = None) -> ContactList:
+ async def list(self, id: str, *, limit: int | None = None, page: int | None = None, options: RequestOptions | None = None) -> SegmentContactList:
"""List contacts in a segment. `GET /v1/segments/{id}/contacts`"""
return await self._core.request("GET", "/v1/segments/{id}/contacts", path_params={"id": id}, query={"limit": limit, "page": page}, body=None, options=options) # type: ignore[no-any-return]
@@ -1260,7 +1260,7 @@ async def create(self, params: CreateBroadcastRequest, *, options: RequestOption
"""Create a broadcast draft. `POST /v1/broadcasts`"""
return await self._core.request("POST", "/v1/broadcasts", path_params=None, query=None, body=params, options=options) # type: ignore[no-any-return]
- async def get(self, id: str, *, options: RequestOptions | None = None) -> Broadcast:
+ async def get(self, id: str, *, options: RequestOptions | None = None) -> BroadcastDetail:
"""Get broadcast details. `GET /v1/broadcasts/{id}`"""
return await self._core.request("GET", "/v1/broadcasts/{id}", path_params={"id": id}, query=None, body=None, options=options) # type: ignore[no-any-return]
@@ -1284,7 +1284,7 @@ async def duplicate(self, id: str, *, options: RequestOptions | None = None) ->
"""Duplicate broadcast. `POST /v1/broadcasts/{id}/duplicate`"""
return await self._core.request("POST", "/v1/broadcasts/{id}/duplicate", path_params={"id": id}, query=None, body=None, options=options) # type: ignore[no-any-return]
- async def send(self, id: str, params: SendBroadcastParams | None = None, *, options: RequestOptions | None = None) -> Broadcast:
+ async def send(self, id: str, params: SendBroadcastParams | None = None, *, options: RequestOptions | None = None) -> BroadcastSendResult:
"""Send or schedule a broadcast. `POST /v1/broadcasts/{id}/send`"""
return await self._core.request("POST", "/v1/broadcasts/{id}/send", path_params={"id": id}, query=None, body=params, options=options) # type: ignore[no-any-return]
@@ -1292,7 +1292,7 @@ async def cancel(self, id: str, *, options: RequestOptions | None = None) -> Bro
"""Cancel a scheduled broadcast. `POST /v1/broadcasts/{id}/cancel`"""
return await self._core.request("POST", "/v1/broadcasts/{id}/cancel", path_params={"id": id}, query=None, body=None, options=options) # type: ignore[no-any-return]
- async def send_test(self, id: str, params: SendBroadcastTestParams, *, options: RequestOptions | None = None) -> Email:
+ async def send_test(self, id: str, params: SendBroadcastTestParams, *, options: RequestOptions | None = None) -> SendBroadcastTestResponse:
"""Send a test broadcast email. `POST /v1/broadcasts/{id}/test`"""
return await self._core.request("POST", "/v1/broadcasts/{id}/test", path_params={"id": id}, query=None, body=params, options=options) # type: ignore[no-any-return]
diff --git a/src/smtpfast/types.py b/src/smtpfast/types.py
index c175060..daa444d 100644
--- a/src/smtpfast/types.py
+++ b/src/smtpfast/types.py
@@ -84,6 +84,9 @@
"verdicts": NotRequired[ReceivedEmailSummaryVerdicts],
"in_reply_to": NotRequired[Union[str, None]],
"references": NotRequired[list[str]],
+ "read_at": NotRequired[Union[str, None]],
+ "inbox_id": NotRequired[Union[str, None]],
+ "thread_id": NotRequired[Union[str, None]],
},
)
@@ -372,6 +375,9 @@
"verdicts": NotRequired[ReceivedEmailVerdicts],
"in_reply_to": NotRequired[Union[str, None]],
"references": NotRequired[list[str]],
+ "read_at": NotRequired[Union[str, None]],
+ "inbox_id": NotRequired[Union[str, None]],
+ "thread_id": NotRequired[Union[str, None]],
"html": NotRequired[Union[str, None]],
"text": NotRequired[Union[str, None]],
"headers": NotRequired[dict[str, str]],
@@ -457,24 +463,70 @@
"tags": NotRequired[list[SendEmailRequestTagsItem]],
"headers": NotRequired[dict[str, str]],
"scheduled_at": NotRequired[str],
+ "category": NotRequired[Literal["transactional", "marketing"]],
"html": NotRequired[str],
"text": NotRequired[str],
},
)
+# The id of the accepted email, as Resend returns it. Fetch the email with GET /v1/emails/{id}.
+SendEmailResponse = TypedDict(
+ "SendEmailResponse",
+ {
+ "id": str,
+ },
+)
+
+
+EmailEventsItem = TypedDict(
+ "EmailEventsItem",
+ {
+ "type": str,
+ "metadata": Union[dict[str, Any], None],
+ "timestamp": str,
+ },
+)
+
+
+EmailTagsItem = TypedDict(
+ "EmailTagsItem",
+ {
+ "name": str,
+ "value": str,
+ },
+)
+
+
Email = TypedDict(
"Email",
{
"id": str,
"from": str,
"to": list[str],
+ "cc": list[str],
+ "bcc": list[str],
"subject": str,
- "html": NotRequired[Union[str, None]],
- "text": NotRequired[Union[str, None]],
- "status": Literal["queued", "scheduled", "sending", "sent", "delivered", "opened", "clicked", "bounced", "complained", "failed", "canceled"],
- "source": str,
+ "html": Union[str, None],
+ "text": Union[str, None],
"created_at": str,
+ "last_event": str,
+ "body_cleared_at": Union[str, None],
+ "source": str,
+ "category": Literal["transactional", "marketing"],
+ "status": Literal["queued", "scheduled", "sending", "sent", "delivered", "opened", "clicked", "bounced", "complained", "failed", "canceled"],
+ "reply_to": Union[str, None],
+ "tags": list[EmailTagsItem],
+ "message_id": Union[str, None],
+ "ses_message_id": Union[str, None],
+ "sent_at": Union[str, None],
+ "delivered_at": Union[str, None],
+ "opened_at": Union[str, None],
+ "clicked_at": Union[str, None],
+ "bounced_at": Union[str, None],
+ "unsubscribed_at": Union[str, None],
+ "error_message": Union[str, None],
+ "events": list[EmailEventsItem],
},
)
@@ -488,6 +540,7 @@
"subject": str,
"status": str,
"source": str,
+ "category": Literal["transactional", "marketing"],
"created_at": str,
"sent_at": Union[str, None],
"delivered_at": Union[str, None],
@@ -611,22 +664,30 @@
)
-SegmentSummary = TypedDict(
- "SegmentSummary",
+ContactListItemSegmentsItem = TypedDict(
+ "ContactListItemSegmentsItem",
{
"id": str,
"name": str,
- "description": NotRequired[Union[str, None]],
- "color": NotRequired[Union[str, None]],
- "contact_count": NotRequired[int],
- "created_at": NotRequired[str],
- "updated_at": NotRequired[str],
+ "color": Union[str, None],
},
)
-ContactSummary = TypedDict(
- "ContactSummary",
+ContactSuppression = TypedDict(
+ "ContactSuppression",
+ {
+ "id": str,
+ "reason": str,
+ "source": str,
+ "scope": Literal["all", "marketing"],
+ "created_at": str,
+ },
+)
+
+
+ContactListItem = TypedDict(
+ "ContactListItem",
{
"id": str,
"email": str,
@@ -636,7 +697,9 @@
"unsubscribed": bool,
"disposable": bool,
"suppressed": bool,
- "segments": NotRequired[list[SegmentSummary]],
+ "segments": list[ContactListItemSegmentsItem],
+ "properties": dict[str, Any],
+ "suppression": Union[ContactSuppression, None],
},
)
@@ -646,8 +709,113 @@
{
"object": str,
"has_more": bool,
- "data": list[ContactSummary],
+ "data": list[ContactListItem],
+ "page": NotRequired[int],
+ "page_size": NotRequired[int],
+ "filtered_total": NotRequired[int],
+ "total": NotRequired[int],
+ "subscribed": NotRequired[int],
+ "unsubscribed": NotRequired[int],
"disposable": NotRequired[int],
+ "new_7d": NotRequired[int],
+ "new_30d": NotRequired[int],
+ "contact_limit": NotRequired[int],
+ "contact_limit_base": NotRequired[int],
+ "tier": NotRequired[str],
+ },
+)
+
+
+ContactExport = TypedDict(
+ "ContactExport",
+ {
+ "object": str,
+ "has_more": bool,
+ "data": list[ContactListItem],
+ "exported_at": str,
+ },
+)
+
+
+SegmentContactListSegment = TypedDict(
+ "SegmentContactListSegment",
+ {
+ "id": str,
+ "name": str,
+ },
+)
+
+
+SegmentContactList = TypedDict(
+ "SegmentContactList",
+ {
+ "object": str,
+ "has_more": bool,
+ "data": list[ContactListItem],
+ "segment": SegmentContactListSegment,
+ "page": int,
+ "page_size": int,
+ "total": int,
+ },
+)
+
+
+# Create or update up to 500 contacts, matched by email. Fields sent replace the stored ones; fields left out stay. segment_ids only adds memberships.
+CreateContactBatchRequest = TypedDict(
+ "CreateContactBatchRequest",
+ {
+ "contacts": list[CreateContactRequest],
+ "segment_ids": NotRequired[list[str]],
+ },
+)
+
+
+ContactSummarySegmentsItem = TypedDict(
+ "ContactSummarySegmentsItem",
+ {
+ "id": str,
+ "name": str,
+ "color": Union[str, None],
+ },
+)
+
+
+ContactSummary = TypedDict(
+ "ContactSummary",
+ {
+ "id": str,
+ "email": str,
+ "first_name": NotRequired[Union[str, None]],
+ "last_name": NotRequired[Union[str, None]],
+ "created_at": str,
+ "unsubscribed": bool,
+ "disposable": bool,
+ "suppressed": bool,
+ "segments": NotRequired[list[ContactSummarySegmentsItem]],
+ },
+)
+
+
+ContactBatchCreated = TypedDict(
+ "ContactBatchCreated",
+ {
+ "object": str,
+ "has_more": bool,
+ "data": list[ContactSummary],
+ },
+)
+
+
+SegmentSummary = TypedDict(
+ "SegmentSummary",
+ {
+ "id": str,
+ "name": str,
+ "description": NotRequired[Union[str, None]],
+ "color": NotRequired[Union[str, None]],
+ "contact_count": NotRequired[int],
+ "created_at": NotRequired[str],
+ "updated_at": NotRequired[str],
},
)
@@ -700,13 +868,12 @@
)
-ContactSuppression = TypedDict(
- "ContactSuppression",
+ContactSegmentsItem = TypedDict(
+ "ContactSegmentsItem",
{
"id": str,
- "reason": str,
- "source": str,
- "created_at": str,
+ "name": str,
+ "color": Union[str, None],
},
)
@@ -722,7 +889,7 @@
"unsubscribed": bool,
"disposable": bool,
"suppressed": bool,
- "segments": NotRequired[list[SegmentSummary]],
+ "segments": NotRequired[list[ContactSegmentsItem]],
"object": NotRequired[str],
"properties": NotRequired[dict[str, Any]],
"suppression": NotRequired[Union[ContactSuppression, None]],
@@ -735,6 +902,7 @@
{
"email": str,
"reason": NotRequired[Literal["manual"]],
+ "scope": NotRequired[Literal["all", "marketing"]],
"diagnostic_code": NotRequired[str],
},
)
@@ -748,6 +916,7 @@
"email": str,
"reason": Literal["hard_bounce", "complaint", "manual", "unsubscribe"],
"source": str,
+ "scope": Literal["all", "marketing"],
"bounce_type": NotRequired[Union[str, None]],
"diagnostic_code": NotRequired[Union[str, None]],
"feedback_id": NotRequired[Union[str, None]],
@@ -768,6 +937,13 @@
"object": str,
"has_more": bool,
"data": list[Suppression],
+ "page": NotRequired[int],
+ "page_size": NotRequired[int],
+ "filtered_total": NotRequired[int],
+ "total": NotRequired[int],
+ "hard_bounces": NotRequired[int],
+ "complaints": NotRequired[int],
+ "manual": NotRequired[int],
},
)
@@ -805,6 +981,7 @@
"text": NotRequired[Union[str, None]],
"track_opens": NotRequired[bool],
"track_clicks": NotRequired[bool],
+ "send_over_minutes": NotRequired[Union[int, None]],
},
)
@@ -819,6 +996,7 @@
"skipped": NotRequired[int],
"queued": NotRequired[int],
"sending": NotRequired[int],
+ "handed_off": NotRequired[int],
"sent": NotRequired[int],
"delivered": NotRequired[int],
"opened": NotRequired[int],
@@ -861,6 +1039,7 @@
"pause_reason": NotRequired[Union[str, None]],
"fanout_completed_at": NotRequired[Union[str, None]],
"expected_recipients": NotRequired[Union[int, None]],
+ "send_over_minutes": NotRequired[int],
"track_opens": NotRequired[bool],
"track_clicks": NotRequired[bool],
"recipient_count": NotRequired[int],
@@ -877,11 +1056,135 @@
"object": str,
"data": list[Broadcast],
"has_more": bool,
+ "page": NotRequired[int],
+ "page_size": NotRequired[int],
+ "filtered_total": NotRequired[int],
+ "total": NotRequired[int],
"status_counts": NotRequired[dict[str, int]],
},
)
+BroadcastDetailLinksItem = TypedDict(
+ "BroadcastDetailLinksItem",
+ {
+ "url": str,
+ "clicks": int,
+ },
+)
+
+
+BroadcastRecipientDetailEventsItem = TypedDict(
+ "BroadcastRecipientDetailEventsItem",
+ {
+ "id": str,
+ "type": str,
+ "metadata": NotRequired[dict[str, Any]],
+ "created_at": str,
+ },
+)
+
+
+BroadcastRecipientDetail = TypedDict(
+ "BroadcastRecipientDetail",
+ {
+ "id": str,
+ "email": str,
+ "first_name": NotRequired[Union[str, None]],
+ "last_name": NotRequired[Union[str, None]],
+ "status": str,
+ "skip_reason": NotRequired[Union[str, None]],
+ "email_id": NotRequired[Union[str, None]],
+ "queued_at": NotRequired[Union[str, None]],
+ "sent_at": NotRequired[Union[str, None]],
+ "delivered_at": NotRequired[Union[str, None]],
+ "opened_at": NotRequired[Union[str, None]],
+ "clicked_at": NotRequired[Union[str, None]],
+ "bounced_at": NotRequired[Union[str, None]],
+ "complained_at": NotRequired[Union[str, None]],
+ "unsubscribed_at": NotRequired[Union[str, None]],
+ "failed_at": NotRequired[Union[str, None]],
+ "error_message": NotRequired[Union[str, None]],
+ "events": list[BroadcastRecipientDetailEventsItem],
+ },
+)
+
+
+BroadcastDetail = TypedDict(
+ "BroadcastDetail",
+ {
+ "object": NotRequired[str],
+ "id": str,
+ "name": str,
+ "audience": NotRequired[str],
+ "audience_meta": NotRequired[AudienceMeta],
+ "from": NotRequired[Union[str, None]],
+ "reply_to": NotRequired[Union[str, None]],
+ "subject": NotRequired[Union[str, None]],
+ "preview_text": NotRequired[Union[str, None]],
+ "html": NotRequired[Union[str, None]],
+ "text": NotRequired[Union[str, None]],
+ "markdown": NotRequired[Union[str, None]],
+ "status": Literal["draft", "scheduled", "queued", "sending", "sent", "paused", "canceled", "failed"],
+ "scheduled_at": NotRequired[Union[str, None]],
+ "queued_at": NotRequired[Union[str, None]],
+ "sent_at": NotRequired[Union[str, None]],
+ "canceled_at": NotRequired[Union[str, None]],
+ "paused_at": NotRequired[Union[str, None]],
+ "pause_reason": NotRequired[Union[str, None]],
+ "fanout_completed_at": NotRequired[Union[str, None]],
+ "expected_recipients": NotRequired[Union[int, None]],
+ "send_over_minutes": NotRequired[int],
+ "track_opens": NotRequired[bool],
+ "track_clicks": NotRequired[bool],
+ "recipient_count": NotRequired[int],
+ "metrics": NotRequired[BroadcastMetrics],
+ "created_at": str,
+ "updated_at": str,
+ "recipients": list[BroadcastRecipientDetail],
+ "links": list[BroadcastDetailLinksItem],
+ },
+)
+
+
+BroadcastSendResult = TypedDict(
+ "BroadcastSendResult",
+ {
+ "object": str,
+ "id": str,
+ "name": str,
+ "audience": NotRequired[str],
+ "audience_meta": NotRequired[AudienceMeta],
+ "from": NotRequired[Union[str, None]],
+ "reply_to": NotRequired[Union[str, None]],
+ "subject": NotRequired[Union[str, None]],
+ "preview_text": NotRequired[Union[str, None]],
+ "html": NotRequired[Union[str, None]],
+ "text": NotRequired[Union[str, None]],
+ "markdown": NotRequired[Union[str, None]],
+ "status": Literal["draft", "scheduled", "queued", "sending", "sent", "paused", "canceled", "failed"],
+ "scheduled_at": NotRequired[Union[str, None]],
+ "queued_at": NotRequired[Union[str, None]],
+ "sent_at": NotRequired[Union[str, None]],
+ "canceled_at": NotRequired[Union[str, None]],
+ "paused_at": NotRequired[Union[str, None]],
+ "pause_reason": NotRequired[Union[str, None]],
+ "fanout_completed_at": NotRequired[Union[str, None]],
+ "expected_recipients": NotRequired[Union[int, None]],
+ "send_over_minutes": NotRequired[int],
+ "track_opens": NotRequired[bool],
+ "track_clicks": NotRequired[bool],
+ "recipient_count": NotRequired[int],
+ "metrics": NotRequired[BroadcastMetrics],
+ "created_at": str,
+ "updated_at": str,
+ "recipients": int,
+ "skipped": int,
+ "batch_id": str,
+ },
+)
+
+
TemplateVariable = TypedDict(
"TemplateVariable",
{
@@ -1234,22 +1537,68 @@
DnsRecord = TypedDict(
"DnsRecord",
{
- "type": NotRequired[Literal["CNAME", "TXT", "MX"]],
+ "type": Literal["CNAME", "TXT", "MX"],
+ "name": str,
+ "value": str,
+ "ttl": NotRequired[str],
+ "priority": NotRequired[Union[int, None]],
+ "status": NotRequired[str],
+ "purpose": NotRequired[str],
+ "category": NotRequired[Literal["verification", "sending", "receiving"]],
+ "actual_value": NotRequired[Union[str, None]],
+ "live_check": NotRequired[Union[Literal["match", "mismatch", "missing", "needs_include", "multiple_spf_records", "invalid_spf"], None]],
+ "actualValue": NotRequired[Union[str, None]],
+ "liveCheck": NotRequired[Union[str, None]],
+ },
+)
+
+
+DomainEventsItem = TypedDict(
+ "DomainEventsItem",
+ {
+ "key": NotRequired[str],
+ "label": NotRequired[str],
+ "at": NotRequired[Union[str, None]],
+ "done": NotRequired[bool],
+ },
+)
+
+
+# The DNS provider found from the NS records
+DomainProvider = TypedDict(
+ "DomainProvider",
+ {
+ "id": NotRequired[str],
"name": NotRequired[str],
- "value": NotRequired[str],
+ "badge": NotRequired[str],
+ "zone": NotRequired[str],
},
)
+# POST /v1/domains returns id, domain, status, dns_records and created_at. GET /v1/domains/{id} adds the verification detail, a live DNS check of each r
Domain = TypedDict(
"Domain",
{
"id": str,
"domain": str,
- "status": Literal["pending", "verified", "failed"],
- "receivingEnabled": NotRequired[bool],
- "receivingStatus": NotRequired[Literal["disabled", "pending", "active", "failed"]],
+ "status": Literal["pending", "verifying", "verified", "failed"],
+ "receiving": NotRequired[DomainReceiving],
+ "verification_phase": NotRequired[Literal["verified", "dns_missing", "waiting_on_ses", "mail_from_pending", "failed"]],
+ "verification_message": NotRequired[str],
+ "next_auto_check_at": NotRequired[Union[str, None]],
+ "dkim_status": NotRequired[str],
+ "spf_status": NotRequired[str],
+ "dmarc_status": NotRequired[str],
+ "mail_from_status": NotRequired[str],
+ "mail_from_domain": NotRequired[Union[str, None]],
+ "verified_at": NotRequired[Union[str, None]],
+ "region": NotRequired[str],
+ "provider": NotRequired[DomainProvider],
+ "events": NotRequired[list[DomainEventsItem]],
"dns_records": list[DnsRecord],
+ "created_at": str,
+ "last_checked_at": NotRequired[Union[str, None]],
},
)
@@ -1711,18 +2060,31 @@
)
+GetEmailMetricsResponseDataItem = TypedDict(
+ "GetEmailMetricsResponseDataItem",
+ {
+ "period": NotRequired[Union[str, None]],
+ "domain_id": NotRequired[Union[str, None]],
+ "domain_name": NotRequired[Union[str, None]],
+ "email_id": NotRequired[Union[str, None]],
+ "broadcast_id": NotRequired[Union[str, None]],
+ "broadcast_name": NotRequired[Union[str, None]],
+ },
+)
+
+
GetEmailMetricsResponse = TypedDict(
"GetEmailMetricsResponse",
{
- "object": NotRequired[str],
- "start_date": NotRequired[str],
- "end_date": NotRequired[str],
- "timezone": NotRequired[str],
- "metrics": NotRequired[list[str]],
- "dimensions": NotRequired[list[str]],
- "granularity": NotRequired[str],
- "totals": NotRequired[dict[str, float]],
- "data": NotRequired[list[dict[str, Any]]],
+ "object": str,
+ "start_date": str,
+ "end_date": str,
+ "timezone": str,
+ "metrics": list[str],
+ "dimensions": list[Literal["period", "domain", "email", "broadcast"]],
+ "granularity": Literal["hourly", "daily", "weekly", "monthly"],
+ "totals": dict[str, float],
+ "data": NotRequired[list[GetEmailMetricsResponseDataItem]],
},
)
@@ -2102,6 +2464,12 @@
)
+CreateContactParams: TypeAlias = Union[CreateContactRequest, CreateContactBatchRequest]
+
+
+CreateContactResponse: TypeAlias = Union[ContactCreated, ContactBatchCreated]
+
+
DeleteContactsParams = TypedDict(
"DeleteContactsParams",
{
@@ -2276,6 +2644,7 @@
"SendBroadcastParams",
{
"scheduled_at": NotRequired[str],
+ "send_over_minutes": NotRequired[int],
},
)
@@ -2288,6 +2657,16 @@
)
+SendBroadcastTestResponse = TypedDict(
+ "SendBroadcastTestResponse",
+ {
+ "object": str,
+ "id": str,
+ "status": str,
+ },
+)
+
+
PublishTemplateParams = TypedDict(
"PublishTemplateParams",
{
@@ -2425,7 +2804,8 @@
RevokeApiKeyResponse = TypedDict(
"RevokeApiKeyResponse",
{
- "success": NotRequired[bool],
+ "success": bool,
+ "message": str,
},
)