From 0a1021c092cc3efe6f33724bdd4febc24ae0a290 Mon Sep 17 00:00:00 2001 From: IzikAJ Date: Mon, 14 Sep 2026 17:38:56 +0300 Subject: [PATCH 01/16] Document the experimental paginated templates endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The number of email templates an account can hold is now set by its subscription plan and counted across every account in the organization, rather than a single hardcoded ceiling. Lists can therefore grow past what one unpaginated payload should carry. The API's answer is a new endpoint rather than a reshaped one. GET /api/templates returns the {data, pagination} envelope, while GET /api/email_templates keeps its bare-array contract untouched, because that array is already published and parsed by SDKs and integrations. Only the list is duplicated: show, create, update and destroy stay on email_templates. The new operation is marked experimental — a GitBook hint plus an "(Experimental)" summary suffix — because the backend change has not shipped and no SDK exposes the method yet. That marking comes off, and /api/email_templates is marked deprecated, in the same later change. Pagination, RateLimitedResponse and the RateLimited response are copied from specs/email-campaigns.openapi.yml rather than invented: both render from the same server-side partial under the same throttle, so key names, order and nullability must not drift. The response component is named RateLimited to match this file's PascalCase responses. Code samples are cURL only. No SDK has the method, so per CLAUDE.md the per-language tabs are omitted rather than invented; they come back as each SDK ships it. No Terraform sample either — the provider has no templates data source. Also documents the plan-based limit on the templates tag, and adds a template_limit_reached example to the create 422 alongside the existing validation error. Co-Authored-By: Claude Opus 5 (1M context) --- specs/templates.openapi.yml | 179 +++++++++++++++++++++++++++++++++++- 1 file changed, 176 insertions(+), 3 deletions(-) diff --git a/specs/templates.openapi.yml b/specs/templates.openapi.yml index 24a9a8b..4a8ef0d 100644 --- a/specs/templates.openapi.yml +++ b/specs/templates.openapi.yml @@ -25,12 +25,21 @@ tags: - name: templates x-page-title: Templates x-page-description: Manage email templates - description: Create and manage reusable email templates with dynamic content. + description: |- + Create and manage reusable email templates with dynamic content. + + The number of templates you can hold is set by your subscription plan and counted across + every account in your organization. paths: /api/email_templates: get: - description: Get all email templates existing in your account + description: |- + Get all email templates existing in your account, as a single unpaginated array. + + If your account holds enough templates for the response size to matter, see the + experimental [`GET /api/templates`](#operation/getTemplates), which returns the same + templates one page at a time. operationId: emailTemplates summary: Get all Email Templates tags: @@ -348,7 +357,78 @@ paths: '403': $ref: '#/components/responses/Forbidden' '422': - $ref: '#/components/responses/UnprocessableEntity' + description: Validation errors, or the account has reached its plan's template limit. + content: + application/json: + schema: + $ref: '#/components/schemas/UnprocessableEntity' + examples: + validation_error: + summary: Required attributes are missing + value: + errors: + category: + - can't be blank + subject: + - can't be blank + template_limit_reached: + summary: The plan's template limit is reached + value: + errors: + base: + - You've reached the maximum number of templates allowed. Please upgrade your plan to add more. + + /api/templates: + get: + description: |- + Returns a paginated list of the email templates in your account. + + {% hint style="warning" %} + This endpoint is experimental. Its parameters and response shape may change before it is + generally available, and no Mailtrap SDK covers it yet. Until then, + `GET /api/email_templates` remains the stable way to list templates. + {% endhint %} + + This is the paginated counterpart to `GET /api/email_templates`, which returns every + template as a single unpaginated array. Pagination is page-token based: pass the `token` + query parameter and follow the `next_token` and `prev_token` values returned in the + `pagination` object to walk between pages. + + A `per_page` above `100` is clamped to `100` rather than rejected, and a `token` past the + last page returns an empty `data` array rather than an error. + operationId: getTemplates + summary: Get a paginated list of Email Templates (Experimental) + tags: + - templates + parameters: + - $ref: '#/components/parameters/token' + - $ref: '#/components/parameters/per_page' + x-codeSamples: + - lang: shell + label: 'cURL' + source: | + curl -X GET "https://mailtrap.io/api/templates?per_page=50&token=1" \ + -H "Api-Token: YOUR_API_KEY" + responses: + '200': + description: OK. + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/EmailTemplateResponse' + pagination: + $ref: '#/components/schemas/Pagination' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '429': + $ref: '#/components/responses/RateLimited' /api/email_templates/{email_template_id}: get: @@ -809,6 +889,29 @@ components: format: int64 example: 60927 + token: + name: token + description: Page number to retrieve (page-token pagination). Defaults to `1`. + in: query + required: false + schema: + type: integer + minimum: 1 + default: 1 + example: 1 + + per_page: + name: per_page + description: Number of templates per page. Defaults to `50`, maximum `100`. Larger values are clamped to `100`. + in: query + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 50 + example: 50 + schemas: UnprocessableEntity: type: object @@ -935,6 +1038,50 @@ components: created_at: '2021-01-01T00:00:00Z' updated_at: '2021-01-01T00:00:00Z' + Pagination: + title: Pagination + type: object + description: Page-token pagination metadata. + properties: + token: + type: integer + description: Current page number. + example: 1 + prev_token: + type: [integer, "null"] + description: Previous page number, or `null` on the first page. + example: null + next_token: + type: [integer, "null"] + description: Next page number, or `null` on the last page. + example: 2 + first_url: + type: string + format: uri + example: 'https://mailtrap.io/api/templates?per_page=50&token=1' + prev_url: + type: [string, "null"] + format: uri + example: null + current_url: + type: string + format: uri + example: 'https://mailtrap.io/api/templates?per_page=50&token=1' + next_url: + type: [string, "null"] + format: uri + example: 'https://mailtrap.io/api/templates?per_page=50&token=2' + + RateLimitedResponse: + title: RateLimitedResponse + type: object + # NOTE: the shared global throttle (Rack::Attack) returns the legacy singular `error` key. + # This diverges from the plural-`errors` convention; kept here to match the real response. + properties: + error: + type: string + example: Throttled + responses: Unauthorized: description: Returns unauthorized error message. Check your credentials. @@ -975,3 +1122,29 @@ components: - can't be blank subject: - can't be blank + + RateLimited: + description: |- + Too many requests. The public API is rate limited to 150 requests per 10 seconds per + API token. Retry after the window resets (see the `x-ratelimit-reset` header). + headers: + x-ratelimit-limit: + description: Maximum number of requests allowed within the window. + schema: + type: integer + example: 150 + x-ratelimit-remaining: + description: Requests remaining in the current window. + schema: + type: integer + example: 0 + x-ratelimit-reset: + description: ISO 8601 timestamp when the current rate-limit window resets. + schema: + type: string + format: date-time + example: '2026-06-15T10:00:10.000000Z' + content: + application/json: + schema: + $ref: '#/components/schemas/RateLimitedResponse' From 6d33c0d40ec4f8cb7aafa199393b68c33813fbc7 Mon Sep 17 00:00:00 2001 From: IzikAJ Date: Mon, 14 Sep 2026 18:27:36 +0300 Subject: [PATCH 02/16] Describe the out-of-range token response for paginated templates An out-of-range token echoes the requested token and points prev_token at the last page that has data, so clients can tell it apart from an empty account. --- specs/templates.openapi.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/specs/templates.openapi.yml b/specs/templates.openapi.yml index 4a8ef0d..af00b1c 100644 --- a/specs/templates.openapi.yml +++ b/specs/templates.openapi.yml @@ -394,8 +394,9 @@ paths: query parameter and follow the `next_token` and `prev_token` values returned in the `pagination` object to walk between pages. - A `per_page` above `100` is clamped to `100` rather than rejected, and a `token` past the - last page returns an empty `data` array rather than an error. + A `per_page` above `100` is clamped to `100` rather than rejected. A `token` past the last + page returns an empty `data` array rather than an error: the `pagination` object echoes the + `token` you asked for, and `prev_token` points at the last page that has data. operationId: getTemplates summary: Get a paginated list of Email Templates (Experimental) tags: From 59d965e0f304cd3cdc91e085df40819b62aa193a Mon Sep 17 00:00:00 2001 From: IzikAJ Date: Mon, 14 Sep 2026 18:34:04 +0300 Subject: [PATCH 03/16] Tell clients to follow returned pagination tokens rather than build them --- specs/templates.openapi.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/specs/templates.openapi.yml b/specs/templates.openapi.yml index af00b1c..dc2ea4f 100644 --- a/specs/templates.openapi.yml +++ b/specs/templates.openapi.yml @@ -396,7 +396,8 @@ paths: A `per_page` above `100` is clamped to `100` rather than rejected. A `token` past the last page returns an empty `data` array rather than an error: the `pagination` object echoes the - `token` you asked for, and `prev_token` points at the last page that has data. + `token` you asked for, and `prev_token` points at the last page that has data. Follow the + tokens returned by a previous response rather than constructing them by hand. operationId: getTemplates summary: Get a paginated list of Email Templates (Experimental) tags: From dd5ef735840b4f7137d745bffb5ae919278fa73e Mon Sep 17 00:00:00 2001 From: IzikAJ Date: Mon, 14 Sep 2026 18:52:14 +0300 Subject: [PATCH 04/16] Document the 422 for a token too large to paginate with --- specs/templates.openapi.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/specs/templates.openapi.yml b/specs/templates.openapi.yml index dc2ea4f..f78dfab 100644 --- a/specs/templates.openapi.yml +++ b/specs/templates.openapi.yml @@ -397,7 +397,8 @@ paths: A `per_page` above `100` is clamped to `100` rather than rejected. A `token` past the last page returns an empty `data` array rather than an error: the `pagination` object echoes the `token` you asked for, and `prev_token` points at the last page that has data. Follow the - tokens returned by a previous response rather than constructing them by hand. + tokens returned by a previous response rather than constructing them by hand — a `token` + too large to paginate with is rejected with `422`. operationId: getTemplates summary: Get a paginated list of Email Templates (Experimental) tags: @@ -429,6 +430,17 @@ paths: $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' + '422': + description: The `token` is too large to paginate with. + content: + application/json: + schema: + type: object + properties: + errors: + type: string + example: + errors: token is out of range '429': $ref: '#/components/responses/RateLimited' From d170ca2d137c6f23ed3a8ce6ab9a7b879ca8b0cb Mon Sep 17 00:00:00 2001 From: IzikAJ Date: Tue, 15 Sep 2026 10:07:05 +0300 Subject: [PATCH 05/16] Reference the paginated endpoint without a relative anchor link GitBook resolves anchors only within the page being rendered and gives each operation its own page, so #operation/getTemplates rendered as a dead link. Co-Authored-By: Claude Opus 5 (1M context) --- specs/templates.openapi.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/specs/templates.openapi.yml b/specs/templates.openapi.yml index f78dfab..d2745e8 100644 --- a/specs/templates.openapi.yml +++ b/specs/templates.openapi.yml @@ -38,8 +38,7 @@ paths: Get all email templates existing in your account, as a single unpaginated array. If your account holds enough templates for the response size to matter, see the - experimental [`GET /api/templates`](#operation/getTemplates), which returns the same - templates one page at a time. + experimental `GET /api/templates`, which returns the same templates one page at a time. operationId: emailTemplates summary: Get all Email Templates tags: From dd04312749d2a0221a0886f29e284036972faba5 Mon Sep 17 00:00:00 2001 From: IzikAJ Date: Tue, 15 Sep 2026 10:07:05 +0300 Subject: [PATCH 06/16] Name the offset limit behind the token 422 A token past the last page returns an empty array; only one whose offset, (token - 1) * per_page, overflows the bigint the offset is handed to is rejected. Without the threshold the two paragraphs read as a contradiction. Co-Authored-By: Claude Opus 5 (1M context) --- specs/templates.openapi.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specs/templates.openapi.yml b/specs/templates.openapi.yml index d2745e8..d036a50 100644 --- a/specs/templates.openapi.yml +++ b/specs/templates.openapi.yml @@ -397,7 +397,7 @@ paths: page returns an empty `data` array rather than an error: the `pagination` object echoes the `token` you asked for, and `prev_token` points at the last page that has data. Follow the tokens returned by a previous response rather than constructing them by hand — a `token` - too large to paginate with is rejected with `422`. + whose offset (`(token - 1) * per_page`) overflows a 64-bit integer is rejected with `422`. operationId: getTemplates summary: Get a paginated list of Email Templates (Experimental) tags: @@ -430,7 +430,7 @@ paths: '403': $ref: '#/components/responses/Forbidden' '422': - description: The `token` is too large to paginate with. + description: The `token` is too large to paginate with — its offset overflows a 64-bit integer. content: application/json: schema: From 4a6e1f6f13a565463f122570e1186af88e0fbc0a Mon Sep 17 00:00:00 2001 From: IzikAJ Date: Tue, 15 Sep 2026 10:07:05 +0300 Subject: [PATCH 07/16] Document the 429 on every templates operation The throttle is keyed on the API token rather than on the path, so all six operations can return it, not just the new paginated list. Co-Authored-By: Claude Opus 5 (1M context) --- specs/templates.openapi.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/specs/templates.openapi.yml b/specs/templates.openapi.yml index d036a50..c473f74 100644 --- a/specs/templates.openapi.yml +++ b/specs/templates.openapi.yml @@ -157,6 +157,8 @@ paths: $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' + '429': + $ref: '#/components/responses/RateLimited' post: description: Create a new email template operationId: createEmailTemplate @@ -376,6 +378,8 @@ paths: errors: base: - You've reached the maximum number of templates allowed. Please upgrade your plan to add more. + '429': + $ref: '#/components/responses/RateLimited' /api/templates: get: @@ -575,6 +579,8 @@ paths: $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/RateLimited' patch: description: Update an email template operationId: updateEmailTemplate @@ -753,6 +759,8 @@ paths: $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/UnprocessableEntity' + '429': + $ref: '#/components/responses/RateLimited' delete: description: Delete an email template operationId: deleteEmailTemplate @@ -875,6 +883,8 @@ paths: $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/RateLimited' parameters: - $ref: '#/components/parameters/email_template_id' From 2ff864b4b4142b9d8fbeeebc94261277ed77b3ca Mon Sep 17 00:00:00 2001 From: IzikAJ Date: Tue, 15 Sep 2026 10:07:05 +0300 Subject: [PATCH 08/16] Keep the experimental templates path below the stable CRUD group Operations render in spec order, so the experimental list was splitting the email_templates collection from its member operations. Co-Authored-By: Claude Opus 5 (1M context) --- specs/templates.openapi.yml | 132 ++++++++++++++++++------------------ 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/specs/templates.openapi.yml b/specs/templates.openapi.yml index c473f74..f98d598 100644 --- a/specs/templates.openapi.yml +++ b/specs/templates.openapi.yml @@ -381,72 +381,6 @@ paths: '429': $ref: '#/components/responses/RateLimited' - /api/templates: - get: - description: |- - Returns a paginated list of the email templates in your account. - - {% hint style="warning" %} - This endpoint is experimental. Its parameters and response shape may change before it is - generally available, and no Mailtrap SDK covers it yet. Until then, - `GET /api/email_templates` remains the stable way to list templates. - {% endhint %} - - This is the paginated counterpart to `GET /api/email_templates`, which returns every - template as a single unpaginated array. Pagination is page-token based: pass the `token` - query parameter and follow the `next_token` and `prev_token` values returned in the - `pagination` object to walk between pages. - - A `per_page` above `100` is clamped to `100` rather than rejected. A `token` past the last - page returns an empty `data` array rather than an error: the `pagination` object echoes the - `token` you asked for, and `prev_token` points at the last page that has data. Follow the - tokens returned by a previous response rather than constructing them by hand — a `token` - whose offset (`(token - 1) * per_page`) overflows a 64-bit integer is rejected with `422`. - operationId: getTemplates - summary: Get a paginated list of Email Templates (Experimental) - tags: - - templates - parameters: - - $ref: '#/components/parameters/token' - - $ref: '#/components/parameters/per_page' - x-codeSamples: - - lang: shell - label: 'cURL' - source: | - curl -X GET "https://mailtrap.io/api/templates?per_page=50&token=1" \ - -H "Api-Token: YOUR_API_KEY" - responses: - '200': - description: OK. - content: - application/json: - schema: - type: object - properties: - data: - type: array - items: - $ref: '#/components/schemas/EmailTemplateResponse' - pagination: - $ref: '#/components/schemas/Pagination' - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '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/RateLimited' - /api/email_templates/{email_template_id}: get: description: Get an email template by ID @@ -888,6 +822,72 @@ paths: parameters: - $ref: '#/components/parameters/email_template_id' + /api/templates: + get: + description: |- + Returns a paginated list of the email templates in your account. + + {% hint style="warning" %} + This endpoint is experimental. Its parameters and response shape may change before it is + generally available, and no Mailtrap SDK covers it yet. Until then, + `GET /api/email_templates` remains the stable way to list templates. + {% endhint %} + + This is the paginated counterpart to `GET /api/email_templates`, which returns every + template as a single unpaginated array. Pagination is page-token based: pass the `token` + query parameter and follow the `next_token` and `prev_token` values returned in the + `pagination` object to walk between pages. + + A `per_page` above `100` is clamped to `100` rather than rejected. A `token` past the last + page returns an empty `data` array rather than an error: the `pagination` object echoes the + `token` you asked for, and `prev_token` points at the last page that has data. Follow the + tokens returned by a previous response rather than constructing them by hand — a `token` + whose offset (`(token - 1) * per_page`) overflows a 64-bit integer is rejected with `422`. + operationId: getTemplates + summary: Get a paginated list of Email Templates (Experimental) + tags: + - templates + parameters: + - $ref: '#/components/parameters/token' + - $ref: '#/components/parameters/per_page' + x-codeSamples: + - lang: shell + label: 'cURL' + source: | + curl -X GET "https://mailtrap.io/api/templates?per_page=50&token=1" \ + -H "Api-Token: YOUR_API_KEY" + responses: + '200': + description: OK. + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/EmailTemplateResponse' + pagination: + $ref: '#/components/schemas/Pagination' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '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/RateLimited' + components: securitySchemes: HeaderAuth: From 4cf330fbbddcded4bb9457f0cda92c522e13f1b9 Mon Sep 17 00:00:00 2001 From: IzikAJ Date: Tue, 15 Sep 2026 15:49:23 +0300 Subject: [PATCH 09/16] Require data and pagination in the paginated templates response A 200 always carries both, so declaring them optional only pushes nullable fields into generated clients. Co-Authored-By: Claude Opus 5 (1M context) --- specs/templates.openapi.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/specs/templates.openapi.yml b/specs/templates.openapi.yml index f98d598..b2220ed 100644 --- a/specs/templates.openapi.yml +++ b/specs/templates.openapi.yml @@ -863,6 +863,9 @@ paths: application/json: schema: type: object + required: + - data + - pagination properties: data: type: array From 25776deccc73b7e4e06aac70b9f91de22edeee70 Mon Sep 17 00:00:00 2001 From: IzikAJ Date: Fri, 18 Sep 2026 13:49:38 +0300 Subject: [PATCH 10/16] Correct the templates list docs now that the path serves full CRUD /api/templates moved onto its own controller: it is the conventions-compliant replacement for the whole email_templates surface rather than a paginated counterpart to its index, and its list and create actions carry a 150/min per-account limit whose body is {"errors": "Rate limit exceeded"}, not the global throttle's {"error": "Throttled"}. Co-Authored-By: Claude Opus 5 (1M context) --- specs/templates.openapi.yml | 44 ++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/specs/templates.openapi.yml b/specs/templates.openapi.yml index b2220ed..4f4f874 100644 --- a/specs/templates.openapi.yml +++ b/specs/templates.openapi.yml @@ -833,10 +833,14 @@ paths: `GET /api/email_templates` remains the stable way to list templates. {% endhint %} - This is the paginated counterpart to `GET /api/email_templates`, which returns every - template as a single unpaginated array. Pagination is page-token based: pass the `token` - query parameter and follow the `next_token` and `prev_token` values returned in the - `pagination` object to walk between pages. + `/api/templates` is the conventions-compliant replacement for `/api/email_templates`: it + wraps every response in a `data` envelope, takes a flat request body on writes, and + paginates this list, where `GET /api/email_templates` returns every template as a single + unpaginated array. `/api/email_templates` keeps its published shapes and is unaffected. + + Pagination is page-token based: pass the `token` query parameter and follow the + `next_token` and `prev_token` values returned in the `pagination` object to walk between + pages. A `per_page` above `100` is clamped to `100` rather than rejected. A `token` past the last page returns an empty `data` array rather than an error: the `pagination` object echoes the @@ -889,7 +893,7 @@ paths: example: errors: token is out of range '429': - $ref: '#/components/responses/RateLimited' + $ref: '#/components/responses/TemplatesRateLimited' components: securitySchemes: @@ -1098,6 +1102,14 @@ components: format: uri example: 'https://mailtrap.io/api/templates?per_page=50&token=2' + EndpointRateLimitedResponse: + title: EndpointRateLimitedResponse + type: object + properties: + errors: + type: string + example: Rate limit exceeded + RateLimitedResponse: title: RateLimitedResponse type: object @@ -1174,3 +1186,25 @@ components: application/json: schema: $ref: '#/components/schemas/RateLimitedResponse' + + TemplatesRateLimited: + description: |- + Too many requests. Two limits apply: 150 requests per minute per account on this endpoint, + counted in separate allowances for listing and for creating, and the global 150 requests + per 10 seconds per API token. The response body tells them apart, and only the global + throttle sets the `x-ratelimit-*` headers. + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/EndpointRateLimitedResponse' + - $ref: '#/components/schemas/RateLimitedResponse' + examples: + endpoint_rate_limit: + summary: The per-account limit on this endpoint is reached + value: + errors: Rate limit exceeded + global_rate_limit: + summary: The global per-token throttle is reached + value: + error: Throttled From b92039b21b38aec27be36e6abf9646a171b3557f Mon Sep 17 00:00:00 2001 From: IzikAJ Date: Fri, 18 Sep 2026 13:49:46 +0300 Subject: [PATCH 11/16] Document show, create, update and delete on /api/templates The path stopped being list-only. The four new operations answer with the data envelope and take a flat request body, though the email_template wrapper is still read through so a client can switch the path before flattening the body. An update carrying none of the permitted attributes is a 200 no-op. Co-Authored-By: Claude Opus 5 (1M context) --- specs/templates.openapi.yml | 290 ++++++++++++++++++++++++++++++++++++ 1 file changed, 290 insertions(+) diff --git a/specs/templates.openapi.yml b/specs/templates.openapi.yml index 4f4f874..1a30526 100644 --- a/specs/templates.openapi.yml +++ b/specs/templates.openapi.yml @@ -894,6 +894,224 @@ paths: errors: token is out of range '429': $ref: '#/components/responses/TemplatesRateLimited' + post: + description: |- + Creates an email template and returns it in a `data` envelope. + + {% hint style="warning" %} + This endpoint is experimental. Its request and response shapes may change before it is + generally available, and no Mailtrap SDK covers it yet. Until then, + `POST /api/email_templates` remains the stable way to create a template. + {% endhint %} + + The request body is flat, where `POST /api/email_templates` expects the attributes wrapped + in `email_template`. That wrapper is still accepted here, so you can switch the path first + and flatten the body afterwards. + operationId: createTemplate + summary: Create Email Template (Experimental) + tags: + - templates + x-codeSamples: + - lang: shell + label: 'cURL' + source: | + curl -X POST "https://mailtrap.io/api/templates" \ + -H "Api-Token: YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Welcome Email", + "subject": "Welcome to {{company_name}}!", + "category": "onboarding", + "body_html": "

Welcome {{user_name}}!

", + "body_text": "Welcome {{user_name}}!" + }' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TemplateRequest' + responses: + '201': + description: Email template created successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/TemplateResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '422': + description: Validation errors, or the account has reached its plan's template limit. + content: + application/json: + schema: + $ref: '#/components/schemas/UnprocessableEntity' + examples: + validation_error: + summary: Required attributes are missing + value: + errors: + name: + - can't be blank + category: + - can't be blank + subject: + - can't be blank + template_limit_reached: + summary: The plan's template limit is reached + value: + errors: + base: + - You've reached the maximum number of templates allowed. Please upgrade your plan to add more. + '429': + $ref: '#/components/responses/TemplatesRateLimited' + + /api/templates/{template_id}: + get: + description: |- + Returns a single email template in a `data` envelope. + + {% hint style="warning" %} + This endpoint is experimental. Its parameters and response shape may change before it is + generally available, and no Mailtrap SDK covers it yet. Until then, + `GET /api/email_templates/{email_template_id}` remains the stable way to read a template. + {% endhint %} + operationId: getTemplate + summary: Get Email Template (Experimental) + tags: + - templates + x-codeSamples: + - lang: shell + label: 'cURL' + source: | + curl -X GET "https://mailtrap.io/api/templates/{template_id}" \ + -H "Api-Token: YOUR_API_KEY" + responses: + '200': + description: OK. + content: + application/json: + schema: + $ref: '#/components/schemas/TemplateResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/RateLimited' + patch: + description: |- + Updates an email template and returns it in a `data` envelope. `PUT` is accepted on the + same path and behaves identically. + + {% hint style="warning" %} + This endpoint is experimental. Its request and response shapes may change before it is + generally available, and no Mailtrap SDK covers it yet. Until then, + `PATCH /api/email_templates/{email_template_id}` remains the stable way to update a + template. + {% endhint %} + + The request body is flat, where `PATCH /api/email_templates/{email_template_id}` expects + the attributes wrapped in `email_template`. That wrapper is still accepted here, so you can + switch the path first and flatten the body afterwards. + + Every attribute is optional. A request carrying none of them returns `200` with the + template unchanged. + operationId: updateTemplate + summary: Update Email Template (Experimental) + tags: + - templates + x-codeSamples: + - lang: shell + label: 'cURL' + source: | + curl -X PATCH "https://mailtrap.io/api/templates/{template_id}" \ + -H "Api-Token: YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Welcome Email v2", + "subject": "Welcome aboard, {{user_name}}!" + }' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TemplateUpdateRequest' + responses: + '200': + description: OK. + content: + application/json: + schema: + $ref: '#/components/schemas/TemplateResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '422': + description: Validation errors. + content: + application/json: + schema: + $ref: '#/components/schemas/UnprocessableEntity' + examples: + validation_error: + summary: An attribute was blanked out + value: + errors: + name: + - can't be blank + category: + - can't be blank + subject: + - can't be blank + drag_and_drop_template: + summary: The template was built in the drag-and-drop editor + value: + errors: + body_html: + - cannot update body_html of template created within UI editor + body_text: + - cannot update body_text of template created within UI editor + '429': + $ref: '#/components/responses/RateLimited' + delete: + description: |- + Deletes an email template. + + {% hint style="warning" %} + This endpoint is experimental and no Mailtrap SDK covers it yet. Until then, + `DELETE /api/email_templates/{email_template_id}` remains the stable way to delete a + template. + {% endhint %} + operationId: deleteTemplate + summary: Delete Email Template (Experimental) + tags: + - templates + x-codeSamples: + - lang: shell + label: 'cURL' + source: | + curl -X DELETE "https://mailtrap.io/api/templates/{template_id}" \ + -H "Api-Token: YOUR_API_KEY" + responses: + '204': + description: No Content + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/RateLimited' + parameters: + - $ref: '#/components/parameters/template_id' components: securitySchemes: @@ -919,6 +1137,16 @@ components: format: int64 example: 60927 + template_id: + name: template_id + in: path + required: true + description: Email template ID + schema: + type: integer + format: int64 + example: 60927 + token: name: token description: Page number to retrieve (page-token pagination). Defaults to `1`. @@ -1102,6 +1330,68 @@ components: format: uri example: 'https://mailtrap.io/api/templates?per_page=50&token=2' + TemplateRequest: + title: TemplateRequest + type: object + required: + - name + - subject + - category + properties: + name: + type: string + maxLength: 255 + category: + type: string + maxLength: 255 + subject: + type: string + maxLength: 255 + body_text: + type: string + maxLength: 10000000 + body_html: + type: string + maxLength: 10000000 + example: + name: My Email Template + subject: Promotional Email + category: Promotional + body_html:
Template Body
+ body_text: Template Body + + TemplateUpdateRequest: + title: TemplateUpdateRequest + type: object + properties: + name: + type: string + maxLength: 255 + category: + type: string + maxLength: 255 + subject: + type: string + maxLength: 255 + body_text: + type: string + maxLength: 10000000 + body_html: + type: string + maxLength: 10000000 + example: + name: My Email Template + subject: Promotional Email + + TemplateResponse: + title: TemplateResponse + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/EmailTemplateResponse' + EndpointRateLimitedResponse: title: EndpointRateLimitedResponse type: object From 149104f863603c314c72b0a91e9f21d820b93cb8 Mon Sep 17 00:00:00 2001 From: IzikAJ Date: Fri, 18 Sep 2026 14:05:50 +0300 Subject: [PATCH 12/16] Require the discriminating key on each rate-limit body The 429 on /api/templates is a oneOf over the endpoint limit's body and the global throttle's, but neither branch required its own key, so each of the response's two examples matched both branches and oneOf rejected them. A generated client that builds a oneOf wrapper threw on every 429. Co-Authored-By: Claude Opus 5 (1M context) --- specs/templates.openapi.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/specs/templates.openapi.yml b/specs/templates.openapi.yml index 1a30526..615c55d 100644 --- a/specs/templates.openapi.yml +++ b/specs/templates.openapi.yml @@ -1395,6 +1395,8 @@ components: EndpointRateLimitedResponse: title: EndpointRateLimitedResponse type: object + required: + - errors properties: errors: type: string @@ -1405,6 +1407,8 @@ components: type: object # NOTE: the shared global throttle (Rack::Attack) returns the legacy singular `error` key. # This diverges from the plural-`errors` convention; kept here to match the real response. + required: + - error properties: error: type: string From 40df52158a858d18fe66d714559f0a27f2677227 Mon Sep 17 00:00:00 2001 From: IzikAJ Date: Fri, 18 Sep 2026 14:05:50 +0300 Subject: [PATCH 13/16] Declare the throttle headers on the templates 429 The response said in prose that the global throttle sets x-ratelimit-*, then declared no headers, so nothing generated from the two operations most likely to be throttled could read the reset timestamp it points clients at. Co-Authored-By: Claude Opus 5 (1M context) --- specs/templates.openapi.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/specs/templates.openapi.yml b/specs/templates.openapi.yml index 615c55d..4534535 100644 --- a/specs/templates.openapi.yml +++ b/specs/templates.openapi.yml @@ -1487,6 +1487,23 @@ components: counted in separate allowances for listing and for creating, and the global 150 requests per 10 seconds per API token. The response body tells them apart, and only the global throttle sets the `x-ratelimit-*` headers. + headers: + x-ratelimit-limit: + description: Maximum number of requests allowed within the window. Sent by the global throttle only. + schema: + type: integer + example: 150 + x-ratelimit-remaining: + description: Requests remaining in the current window. Sent by the global throttle only. + schema: + type: integer + example: 0 + x-ratelimit-reset: + description: ISO 8601 timestamp when the current rate-limit window resets. Sent by the global throttle only. + schema: + type: string + format: date-time + example: '2026-06-15T10:00:10.000000Z' content: application/json: schema: From e4b037667254938a8ac870f4de9c8dd03d2374a9 Mon Sep 17 00:00:00 2001 From: IzikAJ Date: Fri, 18 Sep 2026 14:05:50 +0300 Subject: [PATCH 14/16] Stop rejecting a per_page the server would clamp Generated clients and the try-it panel enforce maximum client-side, so per_page=200 never left the caller, though the documented behaviour is to accept it and clamp to 100. The cap stays in the description. Co-Authored-By: Claude Opus 5 (1M context) --- specs/templates.openapi.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/specs/templates.openapi.yml b/specs/templates.openapi.yml index 4534535..803ac91 100644 --- a/specs/templates.openapi.yml +++ b/specs/templates.openapi.yml @@ -1166,7 +1166,6 @@ components: schema: type: integer minimum: 1 - maximum: 100 default: 50 example: 50 From 099b845f8c4ac92a898c11a289ca961ec51f7685 Mon Sep 17 00:00:00 2001 From: IzikAJ Date: Fri, 18 Sep 2026 14:05:50 +0300 Subject: [PATCH 15/16] Widen the pagination token to the integer its offset is checked against The 422 is defined in terms of (token - 1) * per_page overflowing a 64-bit integer, but an integer with no format defaults to 32-bit in the Java, C# and Go generators, so those clients could not express a token that reaches it. Co-Authored-By: Claude Opus 5 (1M context) --- specs/templates.openapi.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/specs/templates.openapi.yml b/specs/templates.openapi.yml index 803ac91..51bee1f 100644 --- a/specs/templates.openapi.yml +++ b/specs/templates.openapi.yml @@ -1154,6 +1154,7 @@ components: required: false schema: type: integer + format: int64 minimum: 1 default: 1 example: 1 From a0fc339ad03bcbd4973ee040cc055e4c0753173f Mon Sep 17 00:00:00 2001 From: IzikAJ Date: Fri, 18 Sep 2026 14:05:50 +0300 Subject: [PATCH 16/16] Name every missing attribute in the email_templates validation example name is required alongside subject and category, and the /api/templates example lists all three. Leaving it out of this one reads as name being optional on the legacy path. Co-Authored-By: Claude Opus 5 (1M context) --- specs/templates.openapi.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/specs/templates.openapi.yml b/specs/templates.openapi.yml index 51bee1f..522f8bb 100644 --- a/specs/templates.openapi.yml +++ b/specs/templates.openapi.yml @@ -368,6 +368,8 @@ paths: summary: Required attributes are missing value: errors: + name: + - can't be blank category: - can't be blank subject: