From 4e6814db2b5df06d9513a8af26a80cad4cf2736a Mon Sep 17 00:00:00 2001 From: Maciej Walusiak Date: Thu, 12 Nov 2026 12:53:03 +0100 Subject: [PATCH 1/2] Document the real 422 bodies and the nullable sender name Decisions: - Describe each 422 by the body the API renders: a string, an array, or per-attribute strings, where the documents claimed per-attribute arrays - Add the 422s that deleting an account access, listing campaigns and deleting a domain return but did not list - Split start/schedule from cancel/terminate/reset: only the first two return sending-validation errors, and no action returns a plain array - Keep the per-attribute response component for API token reset, which still renders that shape - from_display_name is null for a campaign without a sender name Co-Authored-By: Claude Opus 5.5 --- specs/account-management.openapi.yml | 54 +++++++++++++++++++++++++--- specs/email-campaigns.openapi.yml | 54 +++++++++++++++++++++------- specs/email-sending.openapi.yml | 34 +++++++++++++++++- 3 files changed, 125 insertions(+), 17 deletions(-) diff --git a/specs/account-management.openapi.yml b/specs/account-management.openapi.yml index e0083b7..738632f 100644 --- a/specs/account-management.openapi.yml +++ b/specs/account-management.openapi.yml @@ -500,6 +500,14 @@ paths: $ref: '#/components/responses/PERMISSION_DENIED' '404': $ref: '#/components/responses/NOT_FOUND' + '422': + description: The access belongs to the account owner, who can't be removed. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + errors: You can not remove the owner of the account parameters: - schema: type: integer @@ -746,7 +754,15 @@ paths: '404': $ref: '#/components/responses/NOT_FOUND' '422': - $ref: '#/components/responses/UNPROCESSABLE_ENTITY' + description: |- + The permissions can't be updated: the access belongs to the account owner or to an expired + API token, or a permission names a resource that doesn't exist or the owner access level. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + errors: You can't update the permissions of an account owner parameters: - schema: type: integer @@ -1246,7 +1262,27 @@ paths: '403': $ref: '#/components/responses/PERMISSION_DENIED' '422': - $ref: '#/components/responses/UNPROCESSABLE_ENTITY' + description: |- + The token wasn't created. Validation errors of the token itself (no permissions, the + account's active token limit, the name or expiration) come per attribute; an invalid + permission (an unknown resource, an access level of owner) comes as a single message. + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/UnprocessableEntity' + - $ref: '#/components/schemas/ErrorResponse' + examples: + validation_errors: + summary: The token is invalid + value: + errors: + base: + - No permissions selected. Please choose at least one. + invalid_permission: + summary: A permission is invalid + value: + errors: "Resource of type 'project' and ID #123 not found" '/api/api_tokens/{id}': get: operationId: getApiToken @@ -2181,11 +2217,21 @@ paths: example: errors: Access forbidden '422': - description: Unprocessable entity - validation errors. + description: |- + The sub account wasn't created: the organization reached its sub-account limit, or the + account is invalid (e.g. a name longer than 80 characters). content: application/json: schema: - $ref: '#/components/schemas/UnprocessableEntity' + type: object + properties: + errors: + type: array + items: + type: string + example: + errors: + - Accounts limit reached parameters: - $ref: '#/components/parameters/organization_id' '/api/organizations/{organization_id}/sub_accounts/{sub_account_id}': diff --git a/specs/email-campaigns.openapi.yml b/specs/email-campaigns.openapi.yml index 33b4955..caf4ef7 100644 --- a/specs/email-campaigns.openapi.yml +++ b/specs/email-campaigns.openapi.yml @@ -55,6 +55,17 @@ paths: $ref: "#/components/responses/UNAUTHENTICATED" "403": $ref: "#/components/responses/PERMISSION_DENIED" + "422": + description: The `token` is too large to paginate with — its offset overflows a 64-bit integer. + content: + application/json: + schema: + type: object + properties: + errors: + type: string + example: + errors: token is out of range "429": $ref: "#/components/responses/RATE_LIMITED" x-codeSamples: @@ -992,7 +1003,7 @@ paths: "404": $ref: "#/components/responses/NOT_FOUND" "422": - $ref: "#/components/responses/ACTION_VALIDATION_ERROR" + $ref: "#/components/responses/SENDING_ACTION_VALIDATION_ERROR" "429": $ref: "#/components/responses/RATE_LIMITED" x-codeSamples: @@ -1137,7 +1148,7 @@ paths: "404": $ref: "#/components/responses/NOT_FOUND" "422": - $ref: "#/components/responses/ACTION_VALIDATION_ERROR" + $ref: "#/components/responses/SENDING_ACTION_VALIDATION_ERROR" "429": $ref: "#/components/responses/RATE_LIMITED" x-codeSamples: @@ -2390,7 +2401,8 @@ components: type: string example: news from_display_name: - type: string + type: [string, "null"] + description: Display name shown in the From header. `null` when the campaign has none. example: Acme Marketing reply_to: $ref: "#/components/schemas/ReplyTo" @@ -2753,19 +2765,31 @@ components: ActionValidationErrorResponse: type: object description: |- - Validation error returned by the lifecycle action endpoints. `errors` is either a single - message (an invalid state transition or an invalid `datetime`) or a list of messages - (sending-validation failures). + Validation error returned by the cancel, terminate and reset actions: a single message, + such as an invalid state transition. + properties: + errors: + type: string + example: "Cannot transition from 'under_review' to 'draft'" + SendingActionValidationErrorResponse: + type: object + description: |- + Validation error returned by the start and schedule actions. `errors` is either a single + message (an invalid state transition or an invalid `datetime`) or the sending-validation + failures, as messages per attribute, with campaign-wide ones under `base`. properties: errors: oneOf: - type: string example: "Cannot transition from 'started' to 'scheduled'" - - type: array - items: - type: string + - type: object + additionalProperties: + type: array + items: + type: string example: - - Campaign design can't be blank + base: + - Your account has no company information. Add it in the account settings ("Company Information" tab) before sending. responses: EmailCampaignsResponse: description: A paginated list of email campaigns. @@ -2859,13 +2883,19 @@ components: schema: $ref: "#/components/schemas/ValidationErrorResponse" ACTION_VALIDATION_ERROR: + description: The lifecycle action was rejected — the campaign was in a state that does not allow the action. + content: + application/json: + schema: + $ref: "#/components/schemas/ActionValidationErrorResponse" + SENDING_ACTION_VALIDATION_ERROR: description: |- - The lifecycle action was rejected — the campaign was in a state that does not allow the + The campaign wasn't started or scheduled — it was in a state that does not allow the action, the `datetime` was invalid, or sending validation failed. content: application/json: schema: - $ref: "#/components/schemas/ActionValidationErrorResponse" + $ref: "#/components/schemas/SendingActionValidationErrorResponse" RATE_LIMITED: description: |- Too many requests. The public API is rate limited to 150 requests per 10 seconds per diff --git a/specs/email-sending.openapi.yml b/specs/email-sending.openapi.yml index 50cc099..edc882b 100644 --- a/specs/email-sending.openapi.yml +++ b/specs/email-sending.openapi.yml @@ -719,6 +719,16 @@ paths: $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' + '422': + description: The domain can't be deleted while inbound inboxes use it. + content: + application/json: + schema: + $ref: '#/components/schemas/UnprocessableEntity' + example: + errors: + base: + - Can't delete the domain because there are inbound inboxes depending on it. patch: summary: Update domain @@ -1005,7 +1015,29 @@ paths: '404': $ref: '#/components/responses/NotFound' '422': - $ref: '#/components/responses/UnprocessableEntity' + description: |- + The instructions weren't sent: the email address is invalid, or the domain is blocked + although its DNS records are verified. Each key holds a single message. + content: + application/json: + schema: + type: object + properties: + errors: + type: object + additionalProperties: + type: string + examples: + invalid_email: + summary: The email address is invalid + value: + errors: + email: Invalid email address + domain_not_eligible: + summary: The domain is blocked + value: + errors: + base: Domain is not eligible to send emails /api/domains/{domain_id}/company_info: get: From 923beb9cf3b2964a4e7acd4f58ddd383cba41731 Mon Sep 17 00:00:00 2001 From: Maciej Walusiak Date: Thu, 12 Nov 2026 14:09:25 +0100 Subject: [PATCH 2/2] Reuse the campaign error schemas and list more 422 causes Decisions: - Start and schedule 422 is a oneOf of the existing single-message and per-attribute schemas, as the API token create 422 is, instead of a third schema that copied both. - The bulk permission, API token and sub-account 422 descriptions now name every cause the API returns them for: an invalid access level, an update that leaves no permissions, an organization scheduled for deletion. Co-Authored-By: Claude Opus 5.5 --- specs/account-management.openapi.yml | 10 ++++--- specs/email-campaigns.openapi.yml | 41 ++++++++++++---------------- 2 files changed, 23 insertions(+), 28 deletions(-) diff --git a/specs/account-management.openapi.yml b/specs/account-management.openapi.yml index 738632f..e14684c 100644 --- a/specs/account-management.openapi.yml +++ b/specs/account-management.openapi.yml @@ -756,7 +756,8 @@ paths: '422': description: |- The permissions can't be updated: the access belongs to the account owner or to an expired - API token, or a permission names a resource that doesn't exist or the owner access level. + API token, a permission names a resource that doesn't exist or an invalid or owner access + level, or the update would leave the access with no permissions. content: application/json: schema: @@ -1265,7 +1266,8 @@ paths: description: |- The token wasn't created. Validation errors of the token itself (no permissions, the account's active token limit, the name or expiration) come per attribute; an invalid - permission (an unknown resource, an access level of owner) comes as a single message. + permission (an unknown resource, an invalid or owner access level) comes as a single + message. content: application/json: schema: @@ -2218,8 +2220,8 @@ paths: errors: Access forbidden '422': description: |- - The sub account wasn't created: the organization reached its sub-account limit, or the - account is invalid (e.g. a name longer than 80 characters). + The sub account wasn't created: the organization reached its sub-account limit or is + scheduled for deletion, or the account is invalid (e.g. a name longer than 80 characters). content: application/json: schema: diff --git a/specs/email-campaigns.openapi.yml b/specs/email-campaigns.openapi.yml index caf4ef7..e08c30c 100644 --- a/specs/email-campaigns.openapi.yml +++ b/specs/email-campaigns.openapi.yml @@ -2764,32 +2764,11 @@ components: - can't be blank ActionValidationErrorResponse: type: object - description: |- - Validation error returned by the cancel, terminate and reset actions: a single message, - such as an invalid state transition. + description: A lifecycle action's validation error as a single message, such as an invalid state transition. properties: errors: type: string example: "Cannot transition from 'under_review' to 'draft'" - SendingActionValidationErrorResponse: - type: object - description: |- - Validation error returned by the start and schedule actions. `errors` is either a single - message (an invalid state transition or an invalid `datetime`) or the sending-validation - failures, as messages per attribute, with campaign-wide ones under `base`. - properties: - errors: - oneOf: - - type: string - example: "Cannot transition from 'started' to 'scheduled'" - - type: object - additionalProperties: - type: array - items: - type: string - example: - base: - - Your account has no company information. Add it in the account settings ("Company Information" tab) before sending. responses: EmailCampaignsResponse: description: A paginated list of email campaigns. @@ -2891,11 +2870,25 @@ components: SENDING_ACTION_VALIDATION_ERROR: description: |- The campaign wasn't started or scheduled — it was in a state that does not allow the - action, the `datetime` was invalid, or sending validation failed. + action or the `datetime` was invalid (a single message), or sending validation failed + (messages per attribute, campaign-wide ones under `base`). content: application/json: schema: - $ref: "#/components/schemas/SendingActionValidationErrorResponse" + oneOf: + - $ref: "#/components/schemas/ActionValidationErrorResponse" + - $ref: "#/components/schemas/ValidationErrorResponse" + examples: + invalid_transition: + summary: The campaign can't be started or scheduled from its state + value: + errors: "Cannot transition from 'started' to 'scheduled'" + sending_validation: + summary: Sending validation failed + value: + errors: + base: + - Your account has no company information. Add it in the account settings ("Company Information" tab) before sending. RATE_LIMITED: description: |- Too many requests. The public API is rate limited to 150 requests per 10 seconds per