From 516b87ef75b73a01ec570f25295b24a9e232c387 Mon Sep 17 00:00:00 2001 From: Maciej Walusiak Date: Tue, 25 Aug 2026 21:33:03 +0200 Subject: [PATCH] MT-21117: document GET /api/contacts Decisions: - Endpoint returns keyset (cursor) pagination, so a ContactsPagination schema was added rather than reusing the page-number pagination that the other specs document. - Extracted the contact body into a shared ContactObject schema so the list response and the existing create/show responses cannot drift. - Documented the filter param as `status` (not the internal `subscription_status`) because that is what the params concern accepts and what the response field is called. - Documented 422 as a oneOf: filter errors are field-keyed objects while a bad cursor returns a bare string, and both shapes are real. - Only a cURL code sample: no SDK exposes this endpoint yet. - Did not document /api/contacts/lists/{list_id}/contacts - the falcon PR deliberately did not implement it; list membership is the `list_id` filter. Co-Authored-By: Claude Opus 5 (1M context) --- specs/contacts.openapi.yml | 270 ++++++++++++++++++++++++++++++++----- 1 file changed, 240 insertions(+), 30 deletions(-) diff --git a/specs/contacts.openapi.yml b/specs/contacts.openapi.yml index 20e8d8a..2c75ba5 100644 --- a/specs/contacts.openapi.yml +++ b/specs/contacts.openapi.yml @@ -58,6 +58,165 @@ tags: description: Manage contact events paths: '/api/contacts': + get: + operationId: getContacts + summary: Get a list of contacts + description: |- + Returns a paginated list of the account's contacts. + + Pagination is cursor based. Every response carries a `pagination` object; pass its + `next_token` value back as the `token` query parameter to fetch the next page, and stop + when `next_token` is `null`. Cursors cannot be walked backwards, so `prev_token` and + `prev_url` are always `null`. No total count is returned. + + {% hint style="info" %} + Filter values are validated rather than ignored. An unknown `status`, a non-integer + `list_id`, a repeated filter parameter, or a malformed `token` is rejected with `422`, + so a mistyped filter can never widen the result set. + {% endhint %} + tags: + - Contacts + parameters: + - name: list_id + in: query + required: false + description: Return only contacts that belong to this contact list. + schema: + type: integer + format: int64 + minimum: 1 + example: 3229 + - name: status + in: query + required: false + description: |- + Return only contacts with this subscription status. Matches the `status` field of the + returned contact. + schema: + type: string + enum: + - subscribed + - unsubscribed + example: subscribed + - name: email + in: query + required: false + description: |- + Return only contacts whose email contains this value (case-insensitive substring match). + `*` and `?` are matched literally, not as wildcards. + schema: + type: string + example: john@ + - name: per_page + in: query + required: false + description: |- + Number of contacts per page. Defaults to `25`, maximum `50`. Larger values are clamped + to the maximum. + schema: + type: integer + minimum: 1 + maximum: 50 + default: 25 + example: 25 + - name: token + in: query + required: false + description: |- + Opaque cursor taken from the `next_token` of a previous response. Omit it to fetch the + first page. Tokens are not page numbers and must not be constructed by hand. + schema: + type: string + example: WzE3NDI4MjA2MDAyMzAsIjAxOGRkNWUzLWY2ZDItN2MwMC04ZjliLWU1YzNmMmQ4YTEzMiJd + x-codeSamples: + - lang: shell + label: 'cURL' + source: | + curl -X GET https://mailtrap.io/api/contacts \ + -H 'Authorization: Bearer YOUR_API_KEY' + + # Filter by list, status and email substring + curl -X GET 'https://mailtrap.io/api/contacts?list_id=3229&status=subscribed&email=john%40' \ + -H 'Authorization: Bearer YOUR_API_KEY' + + # Fetch the next page using the cursor from the previous response + curl -X GET 'https://mailtrap.io/api/contacts?per_page=25&token=WzE3NDI4MjA2MDAyMzAsIjAxOGRkNWUzLWY2ZDItN2MwMC04ZjliLWU1YzNmMmQ4YTEzMiJd' \ + -H 'Authorization: Bearer YOUR_API_KEY' + responses: + '200': + description: A paginated list of contacts. + content: + application/json: + schema: + $ref: '#/components/schemas/ContactsCollection' + example: + data: + - id: 018dd5e3-f6d2-7c00-8f9b-e5c3f2d8a132 + email: john.smith@example.com + created_at: 1742820600230 + updated_at: 1742820600230 + list_ids: + - 1 + - 2 + status: subscribed + fields: + first_name: John + last_name: Smith + - id: 018dd5e3-f6d2-7c00-8f9b-000000000002 + email: jane.doe@example.com + created_at: 1742906999123 + updated_at: 1742906999123 + list_ids: + - 3 + status: unsubscribed + fields: {} + pagination: + token: null + prev_token: null + next_token: WzE3NDI5MDY5OTkxMjMsIjAxOGRkNWUzLWY2ZDItN2MwMC04ZjliLTAwMDAwMDAwMDAwMiJd + first_url: 'https://mailtrap.io/api/contacts?per_page=25' + prev_url: null + current_url: 'https://mailtrap.io/api/contacts?per_page=25' + next_url: 'https://mailtrap.io/api/contacts?per_page=25&token=WzE3NDI5MDY5OTkxMjMsIjAxOGRkNWUzLWY2ZDItN2MwMC04ZjliLTAwMDAwMDAwMDAwMiJd' + '401': + $ref: '#/components/responses/UNAUTHENTICATED' + '403': + $ref: '#/components/responses/PERMISSION_DENIED' + '422': + description: A filter value or the pagination cursor was invalid. The request is rejected instead of the filter being ignored. + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/UnprocessableEntity' + - $ref: '#/components/schemas/ErrorResponse' + examples: + invalid_list_id: + summary: list_id is not an integer + value: + errors: + list_id: + - must be an integer + invalid_status: + summary: status is not a known value + value: + errors: + status: + - 'must be one of: subscribed, unsubscribed' + repeated_parameter: + summary: A filter was supplied more than once or as an array + value: + errors: + list_id: + - must be a single value + invalid_token: + summary: The pagination cursor is malformed + value: + errors: Invalid token + '429': + $ref: '#/components/responses/LIMIT_EXCEEDED' + '500': + $ref: '#/components/responses/INTERNAL_ERROR' post: operationId: createContact summary: Create a new Contact @@ -2297,36 +2456,7 @@ components: type: object properties: data: - type: object - properties: - id: - type: string - email: - type: string - fields: - type: object - description: |- - Object of fields in the following format: - field_1_merge_tag: string | integer | float | boolean | date string in ISO-8601 format (yyyy-mm-dd); - list_ids: - type: array - items: - type: integer - status: - type: string - enum: - - subscribed - - unsubscribed - description: |- - The status of the contact: - - "subscribed" if its email was NOT in the suppression list - - "unsubscribed" otherwise - created_at: - type: integer - format: timestampMillis - updated_at: - type: integer - format: timestampMillis + $ref: '#/components/schemas/ContactObject' ContactCreateRequest_2: title: ContactCreateRequest type: object @@ -2657,6 +2787,86 @@ components: example: id: 26730 name: My Contact List + ContactObject: + title: Contact + type: object + properties: + id: + type: string + email: + type: string + fields: + type: object + description: |- + Object of fields in the following format: + field_1_merge_tag: string | integer | float | boolean | date string in ISO-8601 format (yyyy-mm-dd); + list_ids: + type: array + items: + type: integer + status: + type: string + enum: + - subscribed + - unsubscribed + description: |- + The status of the contact: + - "subscribed" if its email was NOT in the suppression list + - "unsubscribed" otherwise + created_at: + type: integer + format: timestampMillis + description: Creation time as a Unix timestamp in milliseconds. + updated_at: + type: integer + format: timestampMillis + description: Last update time as a Unix timestamp in milliseconds. + ContactsCollection: + title: ContactsCollection + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/ContactObject' + pagination: + $ref: '#/components/schemas/ContactsPagination' + ContactsPagination: + title: ContactsPagination + type: object + description: |- + Keyset (cursor) pagination metadata. Tokens are opaque cursors rather than page numbers, + and the response carries no total count. + properties: + token: + type: [string, 'null'] + description: The cursor used for the current request, or `null` on the first page. + example: null + prev_token: + type: [string, 'null'] + description: Always `null`. Keyset pagination cannot walk backwards. + example: null + next_token: + type: [string, 'null'] + description: Cursor for the next page, or `null` once the last page has been reached. + example: WzE3NDI5MDY5OTkxMjMsIjAxOGRkNWUzLWY2ZDItN2MwMC04ZjliLTAwMDAwMDAwMDAwMiJd + first_url: + type: string + format: uri + example: 'https://mailtrap.io/api/contacts?per_page=25' + prev_url: + type: [string, 'null'] + format: uri + description: Always `null`. Keyset pagination cannot walk backwards. + example: null + current_url: + type: string + format: uri + example: 'https://mailtrap.io/api/contacts?per_page=25' + next_url: + type: [string, 'null'] + format: uri + example: 'https://mailtrap.io/api/contacts?per_page=25&token=WzE3NDI5MDY5OTkxMjMsIjAxOGRkNWUzLWY2ZDItN2MwMC04ZjliLTAwMDAwMDAwMDAwMiJd' ContactUpdateRequest_2: title: ContactUpdateRequest type: object