From f766c30de50995c7bb2c8690e4fa9651b320f0cd Mon Sep 17 00:00:00 2001 From: KishenKumarrrrr Date: Thu, 8 Aug 2024 14:05:43 +0800 Subject: [PATCH 1/4] refactor: modify default from address --- backend/src/core/config.ts | 4 +-- backend/src/core/utils/from-address.ts | 8 ++++- .../src/email/middlewares/email.middleware.ts | 15 +++++++--- .../tests/email-campaign.routes.test.ts | 24 +++++++-------- .../tests/email-transactional.routes.test.ts | 30 +++++++++---------- backend/src/test-utils/test-env.ts | 2 +- e2e/config.ts | 8 ++--- .../dashboard/create/email/EmailTemplate.tsx | 2 +- frontend/src/locales/en/messages.po | 2 +- worker/src/core/config.ts | 2 +- 10 files changed, 54 insertions(+), 43 deletions(-) diff --git a/backend/src/core/config.ts b/backend/src/core/config.ts index cc889bf17..ef7ac9a0c 100644 --- a/backend/src/core/config.ts +++ b/backend/src/core/config.ts @@ -810,9 +810,7 @@ const config: Config = convict({ }) // If mailFrom was not set in an env var, set it using the app_name -const defaultMailFrom = `${config.get( - 'APP_NAME' -)} ` +const defaultMailFrom = `${config.get('APP_NAME')} ` config.set('mailFrom', config.get('mailFrom') || defaultMailFrom) // Override some defaults diff --git a/backend/src/core/utils/from-address.ts b/backend/src/core/utils/from-address.ts index 2e17615e3..c2dd40c00 100644 --- a/backend/src/core/utils/from-address.ts +++ b/backend/src/core/utils/from-address.ts @@ -15,7 +15,13 @@ export const isDefaultFromAddress = (from: string): boolean => { const { fromAddress: defaultFromAddress } = parseFromAddress( config.get('mailFrom') ) - return fromAddress === defaultFromAddress + // As part of a PSD directive, we have changed the defaultFromAddress to info@mail.postman.gov.sg. + // To prevent any breaking changes, we must now support both the new and old default address + const allowedDefaultAddresses = [ + defaultFromAddress, + 'donotreply@mail.postman.gov.sg', + ] + return allowedDefaultAddresses.includes(fromAddress) } export const fromAddressValidator = Joi.string() diff --git a/backend/src/email/middlewares/email.middleware.ts b/backend/src/email/middlewares/email.middleware.ts index 9b300bd4a..3cb00fb85 100644 --- a/backend/src/email/middlewares/email.middleware.ts +++ b/backend/src/email/middlewares/email.middleware.ts @@ -30,7 +30,7 @@ export interface EmailMiddleware { } export const INVALID_FROM_ADDRESS_ERROR_MESSAGE = - "Invalid 'from' email address, which must be either the default donotreply@mail.postman.gov.sg or the user's email (which requires setup with Postman team). Contact us to learn more." + "Invalid 'from' email address, which must be either the default info@mail.postman.gov.sg or the user's email (which requires setup with Postman team). Contact us to learn more." export const UNVERIFIED_FROM_ADDRESS_ERROR_MESSAGE = 'From Address has not been verified. Contact us to learn more.' @@ -223,10 +223,17 @@ export const InitEmailMiddleware = ( const { fromName: defaultFromName, fromAddress: defaultFromAddress } = parseFromAddress(config.get('mailFrom')) + // As part of a PSD directive, we have changed the defaultFromAddress to info@mail.postman.gov.sg. + // To prevent any breaking changes, we must now support both the new and old default address + const allowedDefaultAddresses = [ + defaultFromAddress, + 'donotreply@mail.postman.gov.sg', + ] + if ( - // user enters an email that is neither their own nor donotreply@mail.postman.gov.sg + // user enters an email that is neither their own nor info@mail.postman.gov.sg fromAddress !== userEmail && - fromAddress !== defaultFromAddress + allowedDefaultAddresses.includes(fromAddress) ) { logger.error({ message: INVALID_FROM_ADDRESS_ERROR_MESSAGE, @@ -304,7 +311,7 @@ export const InitEmailMiddleware = ( /** * Verifies the user's email address to see if it can be used as custom 'from' address - * - if it is the default donotreply@mail.postman.gov.sg, return immediately + * - if it is the default info@mail.postman.gov.sg, return immediately * - else, make network calls to AWS SES and the user's domain to verify DNS settings are set up properly. */ const verifyFromAddress = async ( diff --git a/backend/src/email/routes/tests/email-campaign.routes.test.ts b/backend/src/email/routes/tests/email-campaign.routes.test.ts index 0fdafb70b..b8f76981b 100644 --- a/backend/src/email/routes/tests/email-campaign.routes.test.ts +++ b/backend/src/email/routes/tests/email-campaign.routes.test.ts @@ -95,7 +95,7 @@ describe('PUT /campaign/{campaignId}/email/template', () => { expect.objectContaining({ message: `Template for campaign ${campaignId} updated`, template: expect.objectContaining({ - from: 'Postman ', + from: 'Postman ', reply_to: 'user@agency.gov.sg', }), }) @@ -106,7 +106,7 @@ describe('PUT /campaign/{campaignId}/email/template', () => { const res = await request(app) .put(`/campaign/${campaignId}/email/template`) .send({ - from: 'Agency.gov.sg ', + from: 'Agency.gov.sg ', subject: 'test', body: 'test', reply_to: 'user@agency.gov.sg', @@ -116,7 +116,7 @@ describe('PUT /campaign/{campaignId}/email/template', () => { expect.objectContaining({ message: `Template for campaign ${campaignId} updated`, template: expect.objectContaining({ - from: 'Agency.gov.sg via Postman ', + from: 'Agency.gov.sg via Postman ', reply_to: 'user@agency.gov.sg', }), }) @@ -127,7 +127,7 @@ describe('PUT /campaign/{campaignId}/email/template', () => { const res = await request(app) .put(`/campaign/${campaignId}/email/template`) .send({ - from: 'Postman ', + from: 'Postman ', subject: 'test', body: 'test', reply_to: 'user@agency.gov.sg', @@ -137,7 +137,7 @@ describe('PUT /campaign/{campaignId}/email/template', () => { expect.objectContaining({ message: `Template for campaign ${campaignId} updated`, template: expect.objectContaining({ - from: 'Postman ', + from: 'Postman ', reply_to: 'user@agency.gov.sg', }), }) @@ -194,7 +194,7 @@ describe('PUT /campaign/{campaignId}/email/template', () => { const res = await request(app) .put(`/campaign/${campaignId}/email/template`) .send({ - from: 'Custom Name ', + from: 'Custom Name ', subject: 'test', body: 'test', reply_to: 'user@agency.gov.sg', @@ -205,7 +205,7 @@ describe('PUT /campaign/{campaignId}/email/template', () => { expect.objectContaining({ message: `Template for campaign ${campaignId} updated`, template: expect.objectContaining({ - from: `Custom Name ${mailVia} `, + from: `Custom Name ${mailVia} `, reply_to: 'user@agency.gov.sg', }), }) @@ -266,7 +266,7 @@ describe('PUT /campaign/{campaignId}/email/template', () => { const res = await request(app) .put(`/campaign/${campaignId}/email/template`) .send({ - from: `Custom Name ${mailVia} `, + from: `Custom Name ${mailVia} `, subject: 'test', body: 'test', reply_to: 'user@agency.gov.sg', @@ -277,7 +277,7 @@ describe('PUT /campaign/{campaignId}/email/template', () => { expect.objectContaining({ message: `Template for campaign ${campaignId} updated`, template: expect.objectContaining({ - from: `Custom Name ${mailVia} `, + from: `Custom Name ${mailVia} `, reply_to: 'user@agency.gov.sg', }), }) @@ -347,7 +347,7 @@ describe('PUT /campaign/{campaignId}/email/template', () => { expect.objectContaining({ message: `Template for campaign ${protectedCampaignId} updated`, template: expect.objectContaining({ - from: 'Postman ', + from: 'Postman ', reply_to: 'user@agency.gov.sg', }), }) @@ -391,7 +391,7 @@ describe('PUT /campaign/{campaignId}/email/template', () => { message: 'Please re-upload your recipient list as template has changed.', template: expect.objectContaining({ - from: 'Postman ', + from: 'Postman ', reply_to: 'user@agency.gov.sg', }), }) @@ -418,7 +418,7 @@ describe('PUT /campaign/{campaignId}/email/template', () => { template: { subject: 'test', body: 'test {{name}}', - from: 'Postman ', + from: 'Postman ', reply_to: 'user@agency.gov.sg', params: ['name'], }, diff --git a/backend/src/email/routes/tests/email-transactional.routes.test.ts b/backend/src/email/routes/tests/email-transactional.routes.test.ts index 107b33dfc..41c448152 100644 --- a/backend/src/email/routes/tests/email-transactional.routes.test.ts +++ b/backend/src/email/routes/tests/email-transactional.routes.test.ts @@ -74,7 +74,7 @@ describe(`${emailTransactionalRoute}/send`, () => { recipient: 'recipient@agency.gov.sg', subject: 'subject', body: '

body

', - from: 'Postman ', + from: 'Postman ', reply_to: 'user@agency.gov.sg', } const generateRandomSmallFile = () => { @@ -150,7 +150,7 @@ describe(`${emailTransactionalRoute}/send`, () => { .spyOn(EmailService, 'sendEmail') .mockResolvedValue(true) - const from = 'Hello ' + const from = 'Hello ' const res = await request(app) .post(endpoint) .set('Authorization', `Bearer ${apiKey}`) @@ -172,14 +172,14 @@ describe(`${emailTransactionalRoute}/send`, () => { expect(transactionalEmail).not.toBeNull() expect(transactionalEmail).toMatchObject({ recipient: validApiCall.recipient, - from: 'Hello ', + from: 'Hello ', status: TransactionalEmailMessageStatus.Accepted, errorCode: null, }) expect(transactionalEmail?.params).toMatchObject({ subject: validApiCall.subject, body: validApiCall.body, - from: 'Hello ', + from: 'Hello ', reply_to: user.email, }) }) @@ -479,7 +479,7 @@ describe(`${emailTransactionalRoute}/send`, () => { .field('recipient', validApiCallAttachment.recipient) .field('subject', validApiCallAttachment.subject) .field('body', validApiCallAttachment.body) - .field('from', 'Postman ') + .field('from', 'Postman ') .field('reply_to', validApiCallAttachment.reply_to) .attach('attachments', validAttachment, validAttachmentName) expect(res.status).toBe(403) @@ -971,9 +971,9 @@ describe(`GET ${emailTransactionalRoute}`, () => { const endpoint = emailTransactionalRoute const acceptedMessage = { recipient: 'recipient@gmail.com', - from: 'Postman ', + from: 'Postman ', params: { - from: 'Postman ', + from: 'Postman ', subject: 'Test', body: 'Test Body', }, @@ -981,9 +981,9 @@ describe(`GET ${emailTransactionalRoute}`, () => { } const sentMessage = { recipient: 'recipient@agency.gov.sg', - from: 'Postman ', + from: 'Postman ', params: { - from: 'Postman ', + from: 'Postman ', subject: 'Test', body: 'Test Body', }, @@ -991,9 +991,9 @@ describe(`GET ${emailTransactionalRoute}`, () => { } const deliveredMessage = { recipient: 'recipient3@agency.gov.sg', - from: 'Postman ', + from: 'Postman ', params: { - from: 'Postman ', + from: 'Postman ', subject: 'Test', body: 'Test Body', }, @@ -1330,9 +1330,9 @@ describe(`GET ${emailTransactionalRoute}/:emailId`, () => { const message = await EmailMessageTransactional.create({ userId: user.id, recipient: 'recipient@agency.gov.sg', - from: 'Postman ', + from: 'Postman ', params: { - from: 'Postman ', + from: 'Postman ', subject: 'Test', body: 'Test Body', }, @@ -1368,9 +1368,9 @@ describe(`GET ${emailTransactionalRoute}/:emailId`, () => { const message = await EmailMessageTransactional.create({ userId: user.id, recipient: 'recipient@agency.gov.sg', - from: 'Postman ', + from: 'Postman ', params: { - from: 'Postman ', + from: 'Postman ', subject: 'Test', body: 'Test Body', }, diff --git a/backend/src/test-utils/test-env.ts b/backend/src/test-utils/test-env.ts index ff606050d..352866565 100644 --- a/backend/src/test-utils/test-env.ts +++ b/backend/src/test-utils/test-env.ts @@ -10,7 +10,7 @@ process.env.SENDGRID_PUBLIC_KEY = process.env.SESSION_SECRET = 'SESSIONSECRET' process.env.DB_URI = 'postgres://postgres:postgres@localhost:5432/postmangovsg_test' -process.env.BACKEND_SES_FROM = 'Postman ' +process.env.BACKEND_SES_FROM = 'Postman ' process.env.API_KEY_SALT_V1 = bcrypt.genSaltSync(1) process.env.TRANSACTIONAL_EMAIL_RATE = '1' process.env.TWILIO_CREDENTIAL_CACHE_MAX_AGE = '0' diff --git a/e2e/config.ts b/e2e/config.ts index cc52f4ed7..e218c12ea 100644 --- a/e2e/config.ts +++ b/e2e/config.ts @@ -1,11 +1,11 @@ export const API_URL = - process.env.API_URL || 'https://api-staging.postman.gov.sg'; + process.env.API_URL || "https://api-staging.postman.gov.sg"; export const API_KEY = process.env.API_KEY as string; -export const MAILBOX = process.env.MAIL_BOX || 'internal-testing@open.gov.sg'; +export const MAILBOX = process.env.MAIL_BOX || "internal-testing@open.gov.sg"; export const DASHBOARD_URL = - process.env.DASHBOARD_URL || 'https://staging.postman.gov.sg'; -export const POSTMAN_FROM = 'donotreply@mail.postman.gov.sg'; + process.env.DASHBOARD_URL || "https://staging.postman.gov.sg"; +export const POSTMAN_FROM = "info@mail.postman.gov.sg"; export const SMS_NUMBER = process.env.SMS_NUMBER as string; export const TWILIO_ACC_SID = process.env.TWILIO_ACC_SID as string; diff --git a/frontend/src/components/dashboard/create/email/EmailTemplate.tsx b/frontend/src/components/dashboard/create/email/EmailTemplate.tsx index 511aecfc0..d04d04c06 100644 --- a/frontend/src/components/dashboard/create/email/EmailTemplate.tsx +++ b/frontend/src/components/dashboard/create/email/EmailTemplate.tsx @@ -197,7 +197,7 @@ const EmailTemplate = ({ parseFromAddress(selectedFrom) // Use custom from name if it has already been set. For e.g., - // for "Custom "", we should + // for "Custom "", we should // use "Custom" instead of the default "Postman.gov.sg". setFromName( selectedFromAddress === initialFromAddress diff --git a/frontend/src/locales/en/messages.po b/frontend/src/locales/en/messages.po index 17e8f089e..61f6e5d01 100644 --- a/frontend/src/locales/en/messages.po +++ b/frontend/src/locales/en/messages.po @@ -120,7 +120,7 @@ msgstr "Want to send your campaigns through WhatsApp?" #: components/dashboard/settings/custom-from-address/CustomFromAddress.tsx:112 #: config.ts:87 msgid "defaultMailFrom" -msgstr "donotreply@mail.postman.gov.sg" +msgstr "info@mail.postman.gov.sg" #: components/dashboard/demo/demo-info-banner/DemoInfoBanner.tsx:8 msgid "demoMessage" diff --git a/worker/src/core/config.ts b/worker/src/core/config.ts index 76e13cf06..916ad3c0e 100644 --- a/worker/src/core/config.ts +++ b/worker/src/core/config.ts @@ -397,7 +397,7 @@ const config: Config = convict({ }) // If mailFrom was not set in an env var, set it using the app_name -const defaultMailFrom = 'Postman.gov.sg ' +const defaultMailFrom = 'Postman.gov.sg ' config.set('mailFrom', config.get('mailFrom') || defaultMailFrom) // Only development is a non-production environment From d7b4229a6543b92957cfb7d1770e707b37a28bb4 Mon Sep 17 00:00:00 2001 From: KishenKumarrrrr Date: Thu, 8 Aug 2024 14:22:40 +0800 Subject: [PATCH 2/4] fix: bug in from address validation --- backend/src/email/middlewares/email.middleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/email/middlewares/email.middleware.ts b/backend/src/email/middlewares/email.middleware.ts index 3cb00fb85..51204eca9 100644 --- a/backend/src/email/middlewares/email.middleware.ts +++ b/backend/src/email/middlewares/email.middleware.ts @@ -233,7 +233,7 @@ export const InitEmailMiddleware = ( if ( // user enters an email that is neither their own nor info@mail.postman.gov.sg fromAddress !== userEmail && - allowedDefaultAddresses.includes(fromAddress) + !allowedDefaultAddresses.includes(fromAddress) ) { logger.error({ message: INVALID_FROM_ADDRESS_ERROR_MESSAGE, From 69cf50c829121fd5cfad4da2e76ef57327e5aca2 Mon Sep 17 00:00:00 2001 From: KishenKumarrrrr Date: Thu, 8 Aug 2024 15:00:25 +0800 Subject: [PATCH 3/4] refactor: reuse isDefaultFromAddress --- backend/src/email/middlewares/email.middleware.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/backend/src/email/middlewares/email.middleware.ts b/backend/src/email/middlewares/email.middleware.ts index 51204eca9..89099f771 100644 --- a/backend/src/email/middlewares/email.middleware.ts +++ b/backend/src/email/middlewares/email.middleware.ts @@ -223,17 +223,10 @@ export const InitEmailMiddleware = ( const { fromName: defaultFromName, fromAddress: defaultFromAddress } = parseFromAddress(config.get('mailFrom')) - // As part of a PSD directive, we have changed the defaultFromAddress to info@mail.postman.gov.sg. - // To prevent any breaking changes, we must now support both the new and old default address - const allowedDefaultAddresses = [ - defaultFromAddress, - 'donotreply@mail.postman.gov.sg', - ] - if ( // user enters an email that is neither their own nor info@mail.postman.gov.sg fromAddress !== userEmail && - !allowedDefaultAddresses.includes(fromAddress) + !isDefaultFromAddress(from) ) { logger.error({ message: INVALID_FROM_ADDRESS_ERROR_MESSAGE, From 2269ef14af29c7745523be9c0d9beaa2ac869aaf Mon Sep 17 00:00:00 2001 From: Jiayee Lim Date: Mon, 2 Sep 2024 10:39:54 +0800 Subject: [PATCH 4/4] chore: remove swagger (#2276) * refactor: modify default from address * fix: bug in from address validation * refactor: reuse isDefaultFromAddress * chore: remove swagger * fix: missing peer dep * fix: move peer dep to fe * chore: move enquirer to dev dep --------- Co-authored-by: KishenKumarrrrr --- README.md | 1 - backend/Dockerfile | 1 - backend/openapi.yaml | 4905 -------------------- backend/package-lock.json | 35 +- backend/package.json | 2 - backend/src/core/loaders/index.ts | 2 - backend/src/core/loaders/swagger.loader.ts | 51 - backend/src/core/routes/index.ts | 10 - frontend/package-lock.json | 3 +- frontend/package.json | 1 + package-lock.json | 26 + 11 files changed, 30 insertions(+), 5007 deletions(-) delete mode 100644 backend/openapi.yaml delete mode 100644 backend/src/core/loaders/swagger.loader.ts diff --git a/README.md b/README.md index 8afe657a4..fe9b717a5 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,6 @@ You should find the - React frontend at [localhost:3000](http://localhost:3000) - Express backend at [localhost:4000](http://localhost:4000) -- Swagger docs at [localhost:4000/docs](http://localhost:4000/docs) Alternatively, if you would like to develop locally against staging database and workers, ensure that you have set up the necessary variables in `./backend/.env` and run either: diff --git a/backend/Dockerfile b/backend/Dockerfile index ffd68de25..87f99645a 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -47,7 +47,6 @@ RUN npm install COPY --from=builder /usr/home/shared/build ../shared/build -COPY openapi.yaml ./ COPY --from=builder /usr/home/postmangovsg/build ./build ENTRYPOINT [ "tini", "--" ] diff --git a/backend/openapi.yaml b/backend/openapi.yaml deleted file mode 100644 index abc44c561..000000000 --- a/backend/openapi.yaml +++ /dev/null @@ -1,4905 +0,0 @@ -openapi: 3.0.0 -info: - title: Postman - version: v1 - description: Postman API - license: - name: MIT - url: https://choosealicense.com/licenses/mit/ -servers: - - url: /v1 - description: Postman API v1 -security: - - bearerAuth: [] -paths: - /campaigns: - get: - security: - - bearerAuth: [] - tags: - - Campaigns - summary: List all campaigns for user - parameters: - - in: query - name: limit - description: max number of campaigns returned - required: false - schema: - type: integer - minimum: 1 - maximum: 100 - default: 10 - - in: query - name: offset - description: offset to begin returning campaigns from - required: false - schema: - type: integer - minimum: 0 - default: 0 - - in: query - name: type - description: mode of campaigns to filter for - required: false - schema: - $ref: '#/components/schemas/ChannelType' - - in: query - name: status - description: status of campaigns to filter for - required: false - schema: - type: string - enum: [Draft, Sending, Sent] - - in: query - name: name - description: name of campaigns to filter for - required: false - schema: - type: string - - in: query - name: sort_by - description: field used to sort campaigns - required: false - schema: - type: string - enum: [created_at, sent_at] - default: created_at - - in: query - name: order_by - description: order to sort campaigns by - required: false - schema: - type: string - enum: [ASC, DESC] - default: DESC - responses: - '200': - description: List of campaigns - content: - application/json: - schema: - type: object - properties: - campaigns: - type: array - items: - $ref: '#/components/schemas/CampaignMeta' - total_count: - type: integer - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - post: - security: - - bearerAuth: [] - summary: Create a new campaign - tags: - - Campaigns - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - name: - type: string - type: - $ref: '#/components/schemas/ChannelType' - required: - - name - - type - - responses: - '201': - description: Campaign created - content: - application/json: - schema: - $ref: '#/components/schemas/CampaignMeta' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - - /campaigns/{campaignId}: - delete: - security: - - bearerAuth: [] - tags: - - Campaigns - summary: Delete a campaign using its ID - parameters: - - in: path - name: campaignId - description: ID of the campaign - required: true - schema: - type: integer - minimum: 1 - responses: - '200': - description: Successfully deleted - content: - application/json: - schema: - $ref: '#/components/schemas/CampaignIdObject' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '404': - description: Not Found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - put: - security: - - bearerAuth: [] - summary: Rename campaign - tags: - - Campaigns - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - name: - type: string - should_save_list: - type: boolean - responses: - '200': - description: Campaign renamed - content: - application/json: - schema: - $ref: '#/components/schemas/CampaignMeta' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '404': - description: Not Found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - - /campaigns/{campaignId}/cancel: - post: - security: - - bearerAuth: [] - tags: - - Campaigns - summary: Cancels the scheduling of the campaign - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - responses: - '200': - description: Successfully cancelled the campaign - content: - application/json: - schema: - $ref: '#/components/schemas/CampaignIdObject' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - - /campaign/{campaignId}/email: - get: - security: - - bearerAuth: [] - tags: - - Email - summary: Get email campaign details - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - responses: - '200': - description: Email campaign details - content: - application/json: - schema: - $ref: '#/components/schemas/EmailCampaign' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - - /campaign/{campaignId}/email/template: - put: - security: - - bearerAuth: [] - tags: - - Email - summary: Stores body template for email campaign - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - subject - - body - - reply_to - - from - properties: - subject: - type: string - body: - type: string - minLength: 1 - maxLength: 200 - reply_to: - type: string - nullable: true - from: - type: string - show_logo: - type: boolean - default: true - responses: - 200: - description: Success - content: - application/json: - schema: - required: - - message - - valid - - num_recipients - properties: - message: - type: string - extra_keys: - type: array - items: - type: string - valid: - type: boolean - num_recipients: - type: integer - template: - type: object - properties: - subject: - type: string - body: - type: string - reply_to: - type: string - nullable: true - params: - type: array - items: - type: string - from: - type: string - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - - /campaign/{campaignId}/email/upload/start: - get: - security: - - bearerAuth: [] - summary: 'Get a presigned URL for upload with Content-MD5 header' - tags: - - Email - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - - name: mime_type - in: query - required: true - schema: - type: string - - name: md5 - in: query - required: true - schema: - type: string - responses: - 200: - description: Success - content: - application/json: - schema: - type: object - required: - - presigned_url - - transaction_id - properties: - presigned_url: - type: string - transaction_id: - type: string - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - - /campaign/{campaignId}/email/upload/complete: - post: - security: - - bearerAuth: [] - summary: 'Complete upload session with ETag verification' - tags: - - Email - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - requestBody: - content: - application/json: - schema: - required: - - transaction_id - - filename - properties: - transaction_id: - type: string - filename: - type: string - etag: - type: string - responses: - '202': - description: Accepted. The uploaded file is being processed. - content: - application/json: - schema: - $ref: '#/components/schemas/CampaignIdObject' - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - - /campaign/{campaignId}/email/upload/status: - get: - security: - - bearerAuth: [] - summary: 'Get csv processing status' - tags: - - Email - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - responses: - 200: - description: Success - content: - application/json: - schema: - properties: - is_csv_processing: - type: boolean - csv_filename: - type: string - temp_csv_filename: - type: string - csv_error: - type: string - num_recipients: - type: number - preview: - type: object - required: - - subject - - body - - from - properties: - subject: - type: string - body: - type: string - replyTo: - type: string - nullable: true - from: - type: string - example: 'Postman ' - showMasthead: - type: boolean - agencyLogoURI: - type: string - example: 'https://file.go.gov.sg/postman-ogp.png' - agencyName: - type: string - example: Open Government Products - themedBody: - type: string - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - delete: - security: - - bearerAuth: [] - description: 'Deletes error status from previous failed upload' - tags: - - Email - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - responses: - '200': - description: Success - content: - application/json: - schema: - $ref: '#/components/schemas/CampaignIdObject' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - - /campaign/{campaignId}/email/credentials: - post: - security: - - bearerAuth: [] - tags: - - Email - summary: Sends a test message and defaults to Postman's credentials for the campaign - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - requestBody: - content: - application/json: - schema: - required: - - recipient - properties: - recipient: - type: string - responses: - '200': - description: Success - content: - application/json: - schema: - type: object - required: - - message - properties: - message: - type: string - example: 'OK' - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - - /campaign/{campaignId}/email/preview: - get: - security: - - bearerAuth: [] - tags: - - Email - summary: Preview templated message - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - - responses: - '200': - description: Success - content: - application/json: - schema: - type: object - properties: - preview: - type: object - required: - - body - - subject - - from - properties: - body: - type: string - subject: - type: string - reply_to: - type: string - nullable: true - from: - type: string - example: 'Postman ' - themed_body: - type: string - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/email/send: - post: - security: - - bearerAuth: [] - tags: - - Email - summary: Start sending campaign - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - responses: - '200': - description: Success - content: - application/json: - schema: - type: object - properties: - campaign_id: - type: integer - job_id: - type: array - items: - type: number - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/email/retry: - post: - security: - - bearerAuth: [] - tags: - - Email - summary: Retry sending campaign - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - responses: - '200': - description: Success - content: - application/json: - schema: - type: object - properties: - campaign_id: - type: integer - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/email/stats: - get: - security: - - bearerAuth: [] - tags: - - Email - summary: Get email campaign stats - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - - responses: - '200': - description: Success - content: - application/json: - schema: - allOf: - - $ref: '#/components/schemas/CampaignStats' - - type: object - required: - - unsubscribed - properties: - unsubscribed: - type: number - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/email/refresh-stats: - post: - security: - - bearerAuth: [] - tags: - - Email - summary: Get email campaign stats - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - - responses: - '200': - description: Success - content: - application/json: - schema: - allOf: - - $ref: '#/components/schemas/CampaignStats' - - type: object - required: - - unsubscribed - properties: - unsubscribed: - type: number - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/email/export: - get: - security: - - bearerAuth: [] - tags: - - Email - summary: Get recipients of campaign - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - - responses: - '200': - description: Success - content: - application/json: - schema: - type: array - items: - allOf: - - $ref: '#/components/schemas/CampaignRecipient' - - type: object - properties: - 'unsubscriber.recipient': - type: string - 'unsubscriber.reason': - type: string - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '410': - description: Data No Longer Available - content: - application/json: - schema: - $ref: '#/components/schemas/Error410Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/email/protect/upload/start: - get: - security: - - bearerAuth: [] - tags: - - Email - summary: Start multipart upload - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - - name: mime_type - in: query - required: true - schema: - type: string - - name: part_count - in: query - required: true - schema: - type: integer - minimum: 1 - maximum: 100 - default: 1 - responses: - '200': - description: Success - content: - application/json: - schema: - type: object - properties: - transaction_id: - type: string - presigned_urls: - type: array - items: - type: string - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/email/protect/upload/complete: - post: - security: - - bearerAuth: [] - summary: Complete multipart upload - tags: - - Email - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - requestBody: - content: - application/json: - schema: - required: - - filename - - transaction_id - - part_count - - etags - properties: - filename: - type: string - transaction_id: - type: string - part_count: - type: integer - etags: - type: array - items: - type: string - responses: - '200': - description: Success - content: - application/json: - schema: - type: object - properties: - transaction_id: - type: string - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/email/duplicate: - post: - security: - - bearerAuth: [] - tags: - - Email - summary: Duplicate the campaign and its template - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - name - properties: - name: - type: string - - responses: - '201': - description: A duplicate of the campaign was created - content: - application/json: - schema: - $ref: '#/components/schemas/CampaignDuplicateMeta' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '404': - description: Not Found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/email/select-list: - post: - security: - - bearerAuth: [] - tags: - - Email - summary: Select the list of recipients from an existing managed list - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - list_id: - type: number - responses: - '201': - description: A duplicate of the campaign was created - content: - application/json: - schema: - $ref: '#/components/schemas/CampaignMeta' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - - /transactional/email/send: - post: - security: - - bearerAuth: [] - summary: 'Send a transactional email' - tags: - - Email - requestBody: - content: - application/json: - schema: - required: - - subject - - body - - recipient - properties: - subject: - type: string - body: - type: string - recipient: - type: string - cc: - type: array - items: - type: string - bcc: - type: array - items: - type: string - from: - type: string - reply_to: - type: string - classification: - type: string - enum: [URGENT, FOR_ACTION, FOR_INFO] - tag: - type: string - multipart/form-data: - schema: - type: object - required: - - subject - - body - - recipient - properties: - subject: - type: string - body: - type: string - recipient: - type: string - cc: - type: array - items: - type: string - bcc: - type: array - items: - type: string - from: - type: string - reply_to: - type: string - attachments: - type: array - items: - type: string - format: binary - classification: - type: string - enum: [URGENT, FOR_ACTION, FOR_INFO] - tag: - type: string - responses: - '201': - description: Created. The message is being sent. - content: - application/json: - schema: - $ref: '#/components/schemas/EmailMessageTransactional' - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '413': - description: Number of attachments or size of attachments exceeded limit. - content: - application/json: - schema: - $ref: '#/components/schemas/Error413Response' - '429': - description: Rate limit exceeded. Too many requests. - content: - application/json: - schema: - $ref: '#/components/schemas/Error429Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - - /transactional/email: - get: - security: - - bearerAuth: [] - tags: - - Email - summary: 'List transactional emails' - parameters: - - in: query - name: limit - description: max number of messages returned - required: false - schema: - type: integer - minimum: 1 - maximum: 100 - default: 10 - - in: query - name: offset - description: offset to begin returning messages from - required: false - schema: - type: integer - minimum: 0 - default: 0 - - in: query - name: status - description: status of messages to filter for - required: false - schema: - type: array - items: - type: string - enum: - [UNSENT, ACCEPTED, SENT, BOUNCED, DELIVERED, OPENED, COMPLAINT] - style: form - explode: true - - in: query - name: created_at - description: > - Filter for created_at timestamp of messages: - - gt: greater than - - gte: greater than or equal - - lt: less than - - lte: less than or equal - required: false - schema: - type: object - minProperties: 1 - properties: - gt: - type: string - format: date-time - gte: - type: string - format: date-time - lt: - type: string - format: date-time - lte: - type: string - format: date-time - - in: query - name: sort_by - description: > - Array of fields to sort by, default order is desc, but can be configured by adding a prefix of: - - plus sign (+) for ascending order - - minus sign (-) for descending order - required: false - schema: - type: array - default: [created_at] - items: - type: string - enum: - [ - created_at, - +created_at, - -created_at, - updated_at, - -updated_at, - +updated_at, - ] - - responses: - 200: - description: Successfully retrieved a list of messages - content: - application/json: - schema: - type: object - required: - - has_more - - data - properties: - has_more: - type: boolean - data: - type: array - items: - $ref: '#/components/schemas/EmailMessageTransactional' - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /transactional/email/{emailId}: - get: - security: - - bearerAuth: [] - tags: - - Email - summary: 'Get transactional email by ID' - parameters: - - in: path - name: emailId - required: true - schema: - type: string - example: 42 - responses: - 200: - description: Successfully retrieved transactional email with corresponding ID - content: - application/json: - schema: - $ref: '#/components/schemas/EmailMessageTransactional' - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '404': - description: Not Found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - - /campaign/{campaignId}/govsg: - get: - security: - - bearerAuth: [] - tags: - - GovSG - summary: Get GovSG campaign details - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - responses: - '200': - description: GovSG campaign details - content: - application/json: - schema: - $ref: '#/components/schemas/GovSGCampaign' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /govsg/templates: - get: - security: - - bearerAuth: [] - tags: - - GovSG - summary: Get available templates for GovSG channel - responses: - 200: - description: Success - content: - application/json: - schema: - required: - - data - properties: - data: - type: array - items: - $ref: '#/components/schemas/GovSGTemplate' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/govsg/templates: - put: - security: - - bearerAuth: [] - tags: - - GovSG - summary: Choose a template for a govsg campaign - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - template_id: - type: number - language_code: - type: string - default: en_GB - responses: - 200: - description: Success - content: - application/json: - schema: - required: - - data - properties: - data: - $ref: '#/components/schemas/GovSGCampaign' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/govsg/upload/start: - get: - security: - - bearerAuth: [] - summary: 'Get a presigned URL for upload with Content-MD5 header' - tags: - - GovSG - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - - name: mime_type - in: query - required: true - schema: - type: string - - name: md5 - in: query - required: true - schema: - type: string - responses: - 200: - description: Success - content: - application/json: - schema: - type: object - required: - - presigned_url - - transaction_id - properties: - presigned_url: - type: string - transaction_id: - type: string - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/govsg/upload/status: - get: - security: - - bearerAuth: [] - summary: 'Get csv processing status' - tags: - - GovSG - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - responses: - 200: - description: Success - content: - application/json: - schema: - properties: - is_csv_processing: - type: boolean - csv_filename: - type: string - temp_csv_filename: - type: string - csv_error: - type: string - num_recipients: - type: number - preview: - type: object - required: - - body - properties: - body: - type: string - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - delete: - security: - - bearerAuth: [] - description: 'Deletes error status from previous failed upload' - tags: - - GovSG - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - responses: - '200': - description: Success - content: - application/json: - schema: - $ref: '#/components/schemas/CampaignIdObject' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/govsg/preview: - get: - security: - - bearerAuth: [] - tags: - - GovSG - summary: Preview templated message - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - - responses: - '200': - description: Success - content: - application/json: - schema: - type: object - properties: - preview: - type: object - properties: - body: - type: string - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/govsg/send: - post: - security: - - bearerAuth: [] - tags: - - GovSG - summary: Start sending campaign - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - responses: - '200': - description: Success - content: - application/json: - schema: - type: object - properties: - campaign_id: - type: integer - job_id: - type: array - items: - type: number - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/govsg/retry: - post: - security: - - bearerAuth: [] - tags: - - GovSG - summary: Retry sending campaign - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - responses: - '200': - description: Success - content: - application/json: - schema: - type: object - properties: - campaign_id: - type: integer - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/govsg/stats: - get: - security: - - bearerAuth: [] - tags: - - GovSG - summary: Get GovSG campaign stats - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - responses: - '200': - description: Success - content: - application/json: - schema: - $ref: '#/components/schemas/CampaignStats' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/govsg/refresh-stats: - post: - security: - - bearerAuth: [] - tags: - - GovSG - summary: Forcibly refresh GovSG campaign stats, then retrieves them - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - responses: - '200': - description: Success - content: - application/json: - schema: - $ref: '#/components/schemas/CampaignStats' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/govsg/export: - get: - security: - - bearerAuth: [] - tags: - - GovSG - summary: Get invalid recipients in campaign - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - - responses: - '200': - description: Success - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/CampaignRecipient' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '410': - description: Data No Longer Available - content: - application/json: - schema: - $ref: '#/components/schemas/Error410Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/govsg/duplicate: - post: - security: - - bearerAuth: [] - tags: - - GovSG - summary: Duplicate the campaign and its choice of template - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - name - properties: - name: - type: string - responses: - '201': - description: A duplicate of the campaign was created - content: - application/json: - schema: - $ref: '#/components/schemas/CampaignDuplicateMeta' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '404': - description: Not Found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /transactional/govsg/send: - post: - security: - - bearerAuth: [] - summary: 'Send a transactional GovSG message' - tags: - - GovSG - requestBody: - content: - application/json: - schema: - required: - - recipient - - template_id - - params - properties: - recipient: - type: string - description: > - Mobile phone number of the recipient without special formatting - (only contains numerical characters and prefixed with plus - sign if country code is included). - - - **Notes**: - - Country code will need to be provided for non-SG mobile numbers. - - If country code is not provided, the phone number will be defaulted to be an SG number. - example: '81234567' - template_id: - type: number - example: 2 - language_code: - type: string - default: en_GB - params: - type: object - additionalProperties: - type: string - example: { officer_name: 'John Tan' } - responses: - '201': - description: Created. The message is being sent. - content: - application/json: - schema: - $ref: '#/components/schemas/GovSGMessageTransactional' - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '429': - description: Rate limit exceeded. Too many requests. - content: - application/json: - schema: - $ref: '#/components/schemas/Error429Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /transactional/govsg: - get: - security: - - bearerAuth: [] - tags: - - GovSG - summary: 'List transactional GovSG messages' - parameters: - - in: query - name: limit - description: max number of messages returned - required: false - schema: - type: integer - minimum: 1 - maximum: 100 - default: 10 - - in: query - name: offset - description: offset to begin returning messages from - required: false - schema: - type: integer - minimum: 0 - default: 0 - - in: query - name: status - description: status of messages to filter for - required: false - schema: - type: array - items: - type: string - enum: [UNSENT, ACCEPTED, SENT, DELIVERED, READ, ERROR] - style: form - explode: true - - in: query - name: created_at - description: > - Filter for created_at timestamp of messages: - - - gt: greater than - - gte: greater than or equal - - lt: less than - - lte: less than or equal - required: false - schema: - type: object - minProperties: 1 - properties: - gt: - type: string - format: date-time - gte: - type: string - format: date-time - lt: - type: string - format: date-time - lte: - type: string - format: date-time - - in: query - name: sort_by - description: > - Array of fields to sort by, default order is desc, but can be configured by adding a prefix of: - - - plus sign (+) for ascending order - - minus sign (-) for descending order - required: false - schema: - type: array - default: [created_at] - items: - type: string - enum: - [ - created_at, - +created_at, - -created_at, - updated_at, - -updated_at, - +updated_at, - ] - - responses: - 200: - description: Successfully retrieved a list of messages - content: - application/json: - schema: - type: object - required: - - has_more - - data - properties: - has_more: - type: boolean - data: - type: array - items: - $ref: '#/components/schemas/GovSGMessageTransactional' - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /transactional/govsg/{messageId}: - get: - security: - - bearerAuth: [] - tags: - - GovSG - summary: 'Get transactional GovSG by ID' - parameters: - - in: path - name: messageId - required: true - schema: - type: string - example: 42 - responses: - 200: - description: Successfully retrieved transactional GovSG message with corresponding ID - content: - application/json: - schema: - $ref: '#/components/schemas/GovSGMessageTransactional' - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '404': - description: Not Found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - - /campaign/{campaignId}/sms: - get: - security: - - bearerAuth: [] - tags: - - SMS - summary: Get sms campaign details - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - - responses: - '200': - description: Successfully retrieved sms campaign details - content: - application/json: - schema: - allOf: - - $ref: '#/components/schemas/SMSCampaign' - - type: object - properties: - cost_per_message: - type: number - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/sms/template: - put: - security: - - bearerAuth: [] - tags: - - SMS - summary: Stores body template for sms campaign - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - body: - type: string - minLength: 1 - maxLength: 200 - - responses: - 200: - description: Success - content: - application/json: - schema: - required: - - message - - valid - - num_recipients - - template - properties: - message: - type: string - extra_keys: - type: array - items: - type: string - valid: - type: boolean - num_recipients: - type: integer - template: - type: object - properties: - body: - type: string - params: - type: array - items: - type: string - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '404': - description: Not Found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/sms/upload/start: - get: - security: - - bearerAuth: [] - summary: 'Get a presigned URL for upload with Content-MD5 header' - tags: - - SMS - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - - name: mime_type - in: query - required: true - schema: - type: string - - name: md5 - required: true - in: query - schema: - type: string - responses: - 200: - description: Success - content: - application/json: - schema: - type: object - properties: - presigned_url: - type: string - transaction_id: - type: string - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/sms/upload/complete: - post: - security: - - bearerAuth: [] - summary: 'Complete upload session with ETag verification' - tags: - - SMS - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - requestBody: - content: - application/json: - schema: - required: - - transaction_id - - filename - properties: - transaction_id: - type: string - filename: - type: string - etag: - type: string - responses: - '202': - description: Accepted. The uploaded file is being processed. - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/sms/upload/status: - get: - security: - - bearerAuth: [] - summary: 'Get csv processing status' - tags: - - SMS - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - responses: - 200: - description: Success - content: - application/json: - schema: - properties: - is_csv_processing: - type: boolean - csv_filename: - type: string - temp_csv_filename: - type: string - csv_error: - type: string - num_recipients: - type: number - preview: - type: object - properties: - body: - type: string - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - delete: - security: - - bearerAuth: [] - description: 'Deletes error status from previous failed upload' - tags: - - SMS - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - responses: - '200': - description: Success - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/sms/new-credentials: - post: - security: - - bearerAuth: [] - tags: - - SMS - summary: Validate twilio credentials and assign to campaign, if label is provided - store credentials for user - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - allOf: - - $ref: '#/components/schemas/TwilioCredentials' - - type: object - properties: - recipient: - type: string - label: - type: string - pattern: '/^[a-z0-9-]+$/' - minLength: 1 - maxLength: 50 - description: should only consist of lowercase alphanumeric characters and dashes - responses: - 200: - description: OK - content: - application/json: - schema: - type: object - required: - - message - properties: - message: - type: string - example: OK - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/sms/credentials: - post: - security: - - bearerAuth: [] - tags: - - SMS - summary: Validate stored credentials and assign to campaign - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - recipient: - type: string - label: - type: string - responses: - 200: - description: OK - content: - application/json: - schema: - type: object - required: - - message - properties: - message: - type: string - example: OK - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/sms/preview: - get: - security: - - bearerAuth: [] - tags: - - SMS - summary: Preview templated message - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - - responses: - '200': - description: Success - content: - application/json: - schema: - type: object - properties: - preview: - type: object - properties: - body: - type: string - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/sms/send: - post: - security: - - bearerAuth: [] - tags: - - SMS - summary: Start sending campaign - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - requestBody: - required: false - content: - application/json: - schema: - type: object - properties: - rate: - example: 10 - type: integer - minimum: 1 - responses: - '200': - description: Success - content: - application/json: - schema: - type: object - properties: - campaign_id: - type: integer - job_id: - type: array - items: - type: number - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/sms/retry: - post: - security: - - bearerAuth: [] - tags: - - SMS - summary: Retry sending campaign - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - responses: - '200': - description: Success - content: - application/json: - schema: - type: object - properties: - campaign_id: - type: integer - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/sms/stats: - get: - security: - - bearerAuth: [] - tags: - - SMS - summary: Get sms campaign stats - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - responses: - '200': - description: Success - content: - application/json: - schema: - $ref: '#/components/schemas/CampaignStats' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/sms/refresh-stats: - post: - security: - - bearerAuth: [] - tags: - - SMS - summary: Forcibly refresh sms campaign stats, then retrieves them - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - responses: - '200': - description: Success - content: - application/json: - schema: - $ref: '#/components/schemas/CampaignStats' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/sms/export: - get: - security: - - bearerAuth: [] - tags: - - SMS - summary: Get invalid recipients in campaign - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - - responses: - '200': - description: Success - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/CampaignRecipient' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '410': - description: Data No Longer Available - content: - application/json: - schema: - $ref: '#/components/schemas/Error410Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/sms/duplicate: - post: - security: - - bearerAuth: [] - tags: - - SMS - summary: Duplicate the campaign and its template - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - name - properties: - name: - type: string - responses: - '201': - description: A duplicate of the campaign was created - content: - application/json: - schema: - $ref: '#/components/schemas/CampaignDuplicateMeta' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '404': - description: Not Found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/sms/select-list: - post: - security: - - bearerAuth: [] - tags: - - SMS - summary: Select the list of recipients from an existing managed list - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - list_id: - type: number - - responses: - '201': - description: A duplicate of the campaign was created - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /transactional/sms/send: - post: - security: - - bearerAuth: [] - summary: 'Send a transactional SMS' - tags: - - SMS - requestBody: - content: - application/json: - schema: - required: - - body - - recipient - - label - properties: - body: - type: string - recipient: - type: string - label: - type: string - responses: - '201': - description: Created. The message is being sent. - content: - application/json: - schema: - $ref: '#/components/schemas/SmsMessageTransactional' - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '429': - description: Rate limit exceeded. Too many requests. - content: - application/json: - schema: - $ref: '#/components/schemas/Error429Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /transactional/sms: - get: - security: - - bearerAuth: [] - tags: - - SMS - summary: 'List transactional SMSes' - parameters: - - in: query - name: limit - description: max number of messages returned - required: false - schema: - type: integer - minimum: 1 - maximum: 100 - default: 10 - - in: query - name: offset - description: offset to begin returning messages from - required: false - schema: - type: integer - minimum: 0 - default: 0 - - in: query - name: status - description: status of messages to filter for - required: false - schema: - type: array - items: - type: string - enum: [UNSENT, ACCEPTED, SENT, DELIVERED, ERROR] - style: form - explode: true - - in: query - name: created_at - description: > - Filter for created_at timestamp of messages: - - gt: greater than - - gte: greater than or equal - - lt: less than - - lte: less than or equal - required: false - schema: - type: object - minProperties: 1 - properties: - gt: - type: string - format: date-time - gte: - type: string - format: date-time - lt: - type: string - format: date-time - lte: - type: string - format: date-time - - in: query - name: sort_by - description: > - Array of fields to sort by, default order is desc, but can be configured by adding a prefix of: - - plus sign (+) for ascending order - - minus sign (-) for descending order - required: false - schema: - type: array - default: [created_at] - items: - type: string - enum: - [ - created_at, - +created_at, - -created_at, - updated_at, - -updated_at, - +updated_at, - ] - - responses: - 200: - description: Successfully retrieved a list of messages - content: - application/json: - schema: - type: object - required: - - has_more - - data - properties: - has_more: - type: boolean - data: - type: array - items: - $ref: '#/components/schemas/SmsMessageTransactional' - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /transactional/sms/{smsId}: - get: - security: - - bearerAuth: [] - tags: - - SMS - summary: 'Get transactional SMS by ID' - parameters: - - in: path - name: smsId - required: true - schema: - type: string - example: 42 - responses: - 200: - description: Successfully retrieved transactional SMS with corresponding ID - content: - application/json: - schema: - $ref: '#/components/schemas/SmsMessageTransactional' - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '404': - description: Not Found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - /campaign/{campaignId}/telegram: - get: - security: - - bearerAuth: [] - tags: - - Telegram - summary: Get telegram campaign details - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - - responses: - '200': - description: Success - content: - application/json: - schema: - type: object - properties: - campaign: - $ref: '#/components/schemas/TelegramCampaign' - num_recipients: - type: number - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - - /campaign/{campaignId}/telegram/template: - put: - security: - - bearerAuth: [] - tags: - - Telegram - summary: Stores body template for telegram campaign - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - body: - type: string - minLength: 1 - maxLength: 200 - - responses: - 200: - description: Success - content: - application/json: - schema: - required: - - message - - valid - - num_recipients - - template - properties: - message: - type: string - extra_keys: - type: array - items: - type: string - valid: - type: boolean - num_recipients: - type: integer - template: - type: object - properties: - body: - type: string - params: - type: array - items: - type: string - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - - /campaign/{campaignId}/telegram/upload/start: - get: - security: - - bearerAuth: [] - summary: 'Get a presigned URL for upload with Content-MD5 header' - tags: - - Telegram - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - - name: mime_type - in: query - required: true - schema: - type: string - - name: md5 - required: true - in: query - schema: - type: string - responses: - 200: - description: Success - content: - application/json: - schema: - type: object - properties: - presigned_url: - type: string - transaction_id: - type: string - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - - /campaign/{campaignId}/telegram/upload/complete: - post: - security: - - bearerAuth: [] - summary: 'Complete upload session with ETag verification' - tags: - - Telegram - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - requestBody: - content: - application/json: - schema: - required: - - transaction_id - - filename - properties: - transaction_id: - type: string - filename: - type: string - etag: - type: string - responses: - '202': - description: Accepted. The uploaded file is being processed. - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - - /campaign/{campaignId}/telegram/upload/status: - get: - security: - - bearerAuth: [] - summary: 'Get csv processing status' - tags: - - Telegram - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - responses: - 200: - description: Success - content: - application/json: - schema: - properties: - is_csv_processing: - type: boolean - csv_filename: - type: string - temp_csv_filename: - type: string - csv_error: - type: string - num_recipients: - type: number - preview: - type: object - properties: - body: - type: string - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - delete: - security: - - bearerAuth: [] - description: 'Deletes error status from previous failed upload' - tags: - - Telegram - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - responses: - 200: - description: Success - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - - /campaign/{campaignId}/telegram/preview: - get: - security: - - bearerAuth: [] - tags: - - Telegram - summary: Preview templated message - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - - responses: - '200': - description: Success - content: - application/json: - schema: - type: object - properties: - preview: - type: object - properties: - body: - type: string - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - - /campaign/{campaignId}/telegram/new-credentials: - post: - security: - - bearerAuth: [] - tags: - - Telegram - summary: Validate Telegram bot token and assign to campaign, if label is provided store new telegram credentials for user - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - telegram_bot_token: - type: string - label: - type: string - pattern: '/^[a-z0-9-]+$/' - minLength: 1 - maxLength: 50 - description: should only consist of lowercase alphanumeric characters and dashes - responses: - 200: - description: OK - content: - application/json: - schema: - type: object - required: - - message - properties: - message: - type: string - example: OK - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - - /campaign/{campaignId}/telegram/credentials: - post: - security: - - bearerAuth: [] - tags: - - Telegram - summary: Validate stored credentials and assign to campaign - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - label: - type: string - - responses: - 200: - description: OK - content: - application/json: - schema: - type: object - required: - - message - properties: - message: - type: string - example: OK - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - - /campaign/{campaignId}/telegram/credentials/verify: - post: - security: - - bearerAuth: [] - tags: - - Telegram - summary: Send a validation message using the campaign credentials. - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - recipient: - type: string - responses: - 200: - description: OK - content: - application/json: - schema: - type: object - required: - - message - properties: - message: - type: string - example: OK - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '404': - description: Not Found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - - /campaign/{campaignId}/telegram/send: - post: - security: - - bearerAuth: [] - tags: - - Telegram - summary: Start sending campaign - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - requestBody: - required: false - content: - application/json: - schema: - type: object - properties: - rate: - type: integer - default: 30 - minimum: 1 - maximum: 30 - - responses: - '200': - description: OK - content: - application/json: - schema: - type: object - properties: - campaign_id: - type: integer - job_id: - type: array - items: - type: number - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400Response' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - - /campaign/{campaignId}/telegram/retry: - post: - security: - - bearerAuth: [] - tags: - - Telegram - summary: Retry sending campaign - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - responses: - '200': - description: OK - content: - application/json: - schema: - type: object - properties: - campaign_id: - type: integer - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - - /campaign/{campaignId}/telegram/stats: - get: - security: - - bearerAuth: [] - tags: - - Telegram - summary: Get telegram campaign stats - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/CampaignStats' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - - /campaign/{campaignId}/telegram/refresh-stats: - post: - security: - - bearerAuth: [] - tags: - - Telegram - summary: Get telegram campaign stats - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/CampaignStats' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - - /campaign/{campaignId}/telegram/export: - get: - security: - - bearerAuth: [] - tags: - - Telegram - summary: Get invalid recipients in campaign - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - - responses: - '200': - description: OK - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/CampaignRecipient' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '410': - description: Data No Longer Available - content: - application/json: - schema: - $ref: '#/components/schemas/Error410Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - - /campaign/{campaignId}/telegram/duplicate: - post: - security: - - bearerAuth: [] - tags: - - Telegram - summary: Duplicate the campaign and its template - parameters: - - name: campaignId - in: path - required: true - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - name: - type: string - - responses: - '201': - description: A duplicate of the campaign was created - content: - application/json: - schema: - $ref: '#/components/schemas/CampaignDuplicateMeta' - '401': - description: Unauthenticated - content: - application/json: - schema: - $ref: '#/components/schemas/Error401Response' - '403': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error403Response' - '404': - description: Not Found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404Response' - '500': - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500Response' - -components: - securitySchemes: - bearerAuth: - type: http - description: API key to authorize requests. - scheme: bearer - schemas: - ChannelType: - type: string - enum: - - SMS - - EMAIL - - TELEGRAM - - GOVSG - - CampaignIdObject: - type: object - required: - - id - properties: - id: - type: string - example: '20' - CampaignMeta: - type: object - properties: - id: - type: number - name: - type: string - created_at: - type: string - format: date-time - has_credential: - type: boolean - valid: - type: boolean - halted: - type: boolean - protect: - type: boolean - redacted: - type: boolean - should_save_list: - type: boolean - type: - $ref: '#/components/schemas/ChannelType' - job_queue: - type: array - items: - $ref: '#/components/schemas/JobQueue' - - CampaignDuplicateMeta: - type: object - required: - - name - - id - - created_at - - type - - protect - properties: - id: - type: number - name: - type: string - type: - $ref: '#/components/schemas/ChannelType' - created_at: - type: string - format: date-time - protect: - type: boolean - example: false - - JobQueue: - type: object - properties: - status: - $ref: '#/components/schemas/JobStatus' - sent_at: - type: string - format: date-time - status_updated_at: - type: string - format: date-time - - JobStatus: - type: string - enum: - - READY - - ENQUEUED - - SENDING - - SENT - - STOPPED - - LOGGED - ErrorStatus: - type: object - properties: - status: - type: integer - message: - type: string - EmailCampaign: - type: object - properties: - id: - type: number - name: - type: string - created_at: - type: string - format: date-time - has_credential: - type: boolean - valid: - type: boolean - protect: - type: boolean - redacted: - type: boolean - csv_filename: - type: string - is_csv_processing: - type: boolean - num_recipients: - type: number - type: - $ref: '#/components/schemas/ChannelType' - default: 'EMAIL' - job_queue: - type: array - items: - $ref: '#/components/schemas/JobQueue' - email_templates: - type: object - properties: - body: - type: string - subject: - type: string - params: - type: array - items: - type: string - GovSGCampaign: - type: object - properties: - id: - type: number - name: - type: string - created_at: - type: string - format: date-time - has_credential: - type: boolean - valid: - type: boolean - protect: - type: boolean - redacted: - type: boolean - csv_filename: - type: string - is_csv_processing: - type: boolean - num_recipients: - type: number - type: - $ref: '#/components/schemas/ChannelType' - default: 'GOVSG' - job_queue: - type: array - items: - $ref: '#/components/schemas/JobQueue' - govsg_templates: - $ref: '#/components/schemas/GovSGTemplate' - GovSGTemplate: - type: object - required: - - id - - body - - params - properties: - id: - type: number - body: - type: string - params: - type: array - items: - type: string - multilingual_support: - type: array - items: - type: object - required: - - language_code - - language - - body - properties: - language_code: - type: string - example: zh_CN - language: - type: string - example: Chinese (CHN) - body: - type: string - CampaignStats: - type: object - properties: - error: - type: number - unsent: - type: number - sent: - type: number - invalid: - type: number - halted: - type: boolean - redacted: - type: boolean - status: - $ref: '#/components/schemas/JobStatus' - updated_at: - type: string - format: date-time - CampaignRecipient: - type: object - properties: - recipient: - type: string - status: - type: string - updated_at: - type: string - format: date-time - error_code: - type: string - EmailMessageTransactional: - type: object - properties: - id: - type: string - example: 42 - from: - type: string - example: Postman - recipient: - type: string - example: hello@example.com - params: - type: object - properties: - body: - type: string - example: Hello World - from: - type: string - example: Postman - subject: - type: string - example: Hello World - reply_to: - type: string - example: hello@example.com - attachments_metadata: - nullable: true - type: array - items: - type: object - required: - - fileName - - fileSize - - hash - properties: - fileName: - type: string - fileSize: - type: number - hash: - type: string - status: - type: string - enum: [UNSENT, ACCEPTED, SENT, BOUNCED, DELIVERED, OPENED, COMPLAINT] - description: > - * `UNSENT` - initial state of a newly created transactional email (this status is not returned in the course of a successful request to send an email) - - * `ACCEPTED` - email has been accepted by our email provider (this status is returned in the course of a successful request to send an email) - - * `SENT` - the send request was successfully forwarded to our email provider and our email provider will attempt to deliver the message to the recipient’s mail server (API user can check this and all subsequent statuses via the `/transactional/email/{emailId}` endpoint) - - * `BOUNCED` - the recipient's mail server rejected the email - - * `DELIVERED` - the email provider has successfully delivered the email to the recipient's mail server - - * `OPENED` - the recipient received the message and opened it in their email client - - * `COMPLAINT` - the email was successfully delivered to the recipient’s mail server, but the recipient marked it as spam - error_code: - nullable: true - type: string - error_sub_type: - nullable: true - type: string - created_at: - nullable: false - type: string - format: date-time - updated_at: - nullable: true - type: string - format: date-time - accepted_at: - nullable: true - type: string - format: date-time - sent_at: - nullable: true - type: string - format: date-time - delivered_at: - nullable: true - type: string - format: date-time - opened_at: - nullable: true - type: string - format: date-time - TwilioCredentials: - type: object - properties: - twilio_account_sid: - type: string - twilio_api_key: - type: string - twilio_api_secret: - type: string - twilio_messaging_service_sid: - type: string - SMSCampaign: - type: object - properties: - id: - type: number - name: - type: string - created_at: - type: string - format: date-time - has_credential: - type: boolean - valid: - type: boolean - redacted: - type: boolean - csv_filename: - type: string - is_csv_processing: - type: boolean - num_recipients: - type: number - type: - $ref: '#/components/schemas/ChannelType' - default: 'SMS' - job_queue: - type: array - items: - $ref: '#/components/schemas/JobQueue' - sms_templates: - type: object - properties: - body: - type: string - params: - type: array - items: - type: string - SmsMessageTransactional: - type: object - properties: - id: - type: string - example: 42 - credentials_label: - type: string - example: 'ogp-twilio-creds' - recipient: - type: string - example: '98765432' - body: - type: string - example: 'Hello world' - created_at: - type: string - format: date-time - updated_at: - type: string - format: date-time - accepted_at: - type: string - format: date-time - sent_at: - type: string - format: date-time - delivered_at: - type: string - format: date-time - errored_at: - type: string - format: date-time - error_code: - type: string - description: This error code corresponds with [error codes from Twilio](https://www.twilio.com/docs/sms/api/message-resource#delivery-related-errors). - status: - type: string - enum: [UNSENT, ACCEPTED, SENT, DELIVERED, ERROR] - description: > - * `UNSENT` - initial state of a newly created transactional SMS (this status is not returned in the course of a successful request to send an SMS) - - * `ACCEPTED` - SMS has been accepted by our SMS provider (this status is returned in the course of a successful request to send an SMS) - - * `SENT` - the send request was successfully forwarded to our SMS provider and our SMS provider will attempt to deliver the message to the recipient’s phone number (API user can check this and all subsequent statuses via the `/transactional/sms/{smsId}` endpoint) - - * `DELIVERED` - the SMS provider has successfully delivered the SMS to the recipient's phone number - - * `ERROR` - an error happened when the SMS provider is trying to deliver the message - GovSGMessageTransactional: - type: object - properties: - id: - type: string - example: 42 - recipient: - type: string - example: '98765432' - template_id: - type: number - example: 2 - params: - type: object - additionalProperties: - type: string - language_code: - type: string - example: en_GB - created_at: - type: string - format: date-time - updated_at: - type: string - format: date-time - accepted_at: - type: string - format: date-time - sent_at: - type: string - format: date-time - delivered_at: - type: string - format: date-time - read_at: - type: string - format: date-time - errored_at: - type: string - format: date-time - error_code: - type: string - description: > - This error code corresponds with error codes from our service providers: - - [WhatsApp](https://developers.facebook.com/docs/whatsapp/cloud-api/support/error-codes/#error-codes) - error_description: - type: string - description: Human-readable details about the message error. - status: - type: string - enum: [UNSENT, ACCEPTED, SENT, DELIVERED, ERROR] - description: > - * `UNSENT` - initial state of a newly created transactional GovSG message (this status is not returned in the course of a successful request to send a GovSG message) - - * `ACCEPTED` - Message has been accepted by our service provider (this status is returned in the course of a successful request to send a GovSG message) - - * `SENT` - the send request was successfully forwarded to our service provider and our service provider will attempt to deliver the message to the recipient’s phone number (API user can check this and all subsequent statuses via the `/transactional/govSG/{messageId}` endpoint) - - * `DELIVERED` - the service provider has successfully delivered the message to the recipient's phone number - - * `READ` - the recipient has received the message and viewed it - - * `ERROR` - an error happened when the service provider is trying to deliver the message - TelegramCampaign: - type: object - properties: - id: - type: number - name: - type: string - created_at: - type: string - format: date-time - has_credential: - type: boolean - valid: - type: boolean - redacted: - type: boolean - csv_filename: - type: string - type: - $ref: '#/components/schemas/ChannelType' - default: 'TELEGRAM' - job_queue: - type: array - items: - $ref: '#/components/schemas/JobQueue' - telegram_templates: - type: object - properties: - body: - type: string - params: - type: array - items: - type: string - Error401Response: - type: object - required: - - code - - message - properties: - code: - type: string - enum: ['unathenticated'] - message: - type: string - example: Unauthenticated request. Please try again with correct authentication - Error500Response: - type: object - required: - - code - - message - properties: - code: - type: string - enum: ['internal_server'] - message: - type: string - example: Intetnal server error. Please try again later - Error404Response: - type: object - required: - - code - - message - properties: - code: - type: string - enum: ['not_found'] - message: - type: string - example: Campaign with ID 20 not found. - Error403Response: - type: object - required: - - code - - message - properties: - code: - type: string - enum: ['unauthorized', 'already_sent'] - message: - type: string - example: Campaign can't be edited at the moment as there're ongoing uploads or jobs - Error400Response: - type: object - required: - - code - - message - properties: - code: - type: string - enum: - [ - 'api_validation', - 'invalid_recipient', - 'invalid_credentials', - 'malformed', - 'invalid_parameters', - 'invalid_from_address', - 'invalid_template', - 'invalid_credential_label', - ] - message: - type: string - example: Phone number 84206920 is invalid - Error410Response: - type: object - required: - - code - - message - properties: - code: - type: string - enum: ['campaign_redacted'] - message: - type: string - example: Campaign 20 has been redacted - Error413Response: - type: object - required: - - code - - message - properties: - code: - type: string - enum: ['attachment_limit'] - message: - type: string - example: Size of attachments exceeds limit - Error429Response: - type: object - required: - - code - - message - properties: - code: - type: string - enum: ['rate_limit'] - message: - type: string - example: Too many requests. Please try again later. diff --git a/backend/package-lock.json b/backend/package-lock.json index 548a1d3e6..ca539d108 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -39,7 +39,7 @@ "module-alias": "2.2.2", "moment": "^2.29.4", "nanoid": "3.3.6", - "nodemailer": "^6.9.9", + "nodemailer": "6.9.9", "papaparse": "5.2.0", "pg": "8.5.1", "pg-connection-string": "2.4.0", @@ -47,10 +47,9 @@ "redis": "3.1.2", "reflect-metadata": "0.1.13", "sequelize": "6.29.1", - "sequelize-typescript": "^2.1.6", + "sequelize-typescript": "2.1.6", "source-map-support": "0.5.19", "starkbank-ecdsa": "1.1.4", - "swagger-ui-express": "4.6.3", "telegraf": "3.38.0", "threads": "1.7.0", "tiny-worker": "2.3.0", @@ -88,7 +87,6 @@ "@types/rate-limit-redis": "1.7.1", "@types/redis": "2.8.17", "@types/supertest": "2.0.12", - "@types/swagger-ui-express": "4.1.2", "@types/umzug": "2.3.0", "@types/uuid": "7.0.2", "@types/validator": "12.0.1", @@ -8839,16 +8837,6 @@ "@types/superagent": "*" } }, - "node_modules/@types/swagger-ui-express": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.2.tgz", - "integrity": "sha512-t9teFTU8dKe69rX9EwL6OM2hbVquYdFM+sQ0REny4RalPlxAm+zyP04B12j4c7qEuDS6CnlwICywqWStPA3v4g==", - "dev": true, - "dependencies": { - "@types/express": "*", - "@types/serve-static": "*" - } - }, "node_modules/@types/triple-beam": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.2.tgz", @@ -16767,25 +16755,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/swagger-ui-dist": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.18.2.tgz", - "integrity": "sha512-oVBoBl9Dg+VJw8uRWDxlyUyHoNEDC0c1ysT6+Boy6CTgr2rUcLcfPon4RvxgS2/taNW6O0+US+Z/dlAsWFjOAQ==" - }, - "node_modules/swagger-ui-express": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.6.3.tgz", - "integrity": "sha512-CDje4PndhTD2HkgyKH3pab+LKspDeB/NhPN2OF1j+piYIamQqBYwAXWESOT1Yju2xFg51bRW9sUng2WxDjzArw==", - "dependencies": { - "swagger-ui-dist": ">=4.11.0" - }, - "engines": { - "node": ">= v0.10.32" - }, - "peerDependencies": { - "express": ">=4.0.0 || >=5.0.0-beta" - } - }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", diff --git a/backend/package.json b/backend/package.json index 6e0e864c8..5001bae78 100644 --- a/backend/package.json +++ b/backend/package.json @@ -70,7 +70,6 @@ "sequelize-typescript": "2.1.6", "source-map-support": "0.5.19", "starkbank-ecdsa": "1.1.4", - "swagger-ui-express": "4.6.3", "telegraf": "3.38.0", "threads": "1.7.0", "tiny-worker": "2.3.0", @@ -108,7 +107,6 @@ "@types/rate-limit-redis": "1.7.1", "@types/redis": "2.8.17", "@types/supertest": "2.0.12", - "@types/swagger-ui-express": "4.1.2", "@types/umzug": "2.3.0", "@types/uuid": "7.0.2", "@types/validator": "12.0.1", diff --git a/backend/src/core/loaders/index.ts b/backend/src/core/loaders/index.ts index 1314e21ef..688e04bf9 100644 --- a/backend/src/core/loaders/index.ts +++ b/backend/src/core/loaders/index.ts @@ -1,7 +1,6 @@ import { Application } from 'express' import securityHeadersLoader from './security-headers.loader' import expressLoader from './express.loader' -import swaggerLoader from './swagger.loader' import sessionLoader from './session.loader' import sequelizeLoader from './sequelize.loader' import cloudwatchLoader from './cloudwatch.loader' @@ -22,7 +21,6 @@ const loaders = async ({ app }: { app: Application }): Promise => { await sequelizeLoader() await sessionLoader({ app }) await expressLoader({ app }) - await swaggerLoader({ app }) await uploadQueueLoader() ;(app as any).cleanup = async function (): Promise { await (app as any).redisService.shutdown() diff --git a/backend/src/core/loaders/swagger.loader.ts b/backend/src/core/loaders/swagger.loader.ts deleted file mode 100644 index 4a18122b6..000000000 --- a/backend/src/core/loaders/swagger.loader.ts +++ /dev/null @@ -1,51 +0,0 @@ -import express, { Application, Request, Response, NextFunction } from 'express' -import cors from 'cors' -import path from 'path' -import swaggerUi from 'swagger-ui-express' -import YAML from 'yamljs' - -import { loggerWithLabel } from '@core/logger' - -const logger = loggerWithLabel(module) - -const swaggerUiOptions = { - explorer: false, - customCss: '.swagger-ui .topbar { display: none; }', - url: '/v1/swagger.json', -} - -const removeCspHeader = ( - _req: Request, - res: Response, - next: NextFunction -): void => { - res.removeHeader('Content-Security-Policy') - next() -} - -const swaggerDocument = YAML.load( - path.resolve(__dirname, '../../../openapi.yaml') -) - -const swaggerLoader = ({ app }: { app: Application }): void => { - app.get('/v1/swagger.json', (_req, res) => res.json(swaggerDocument)) - app.use( - '/docs', - removeCspHeader, - swaggerUi.serve, - swaggerUi.setup(swaggerDocument, swaggerUiOptions) - ) - app.use( - '/openapi.yaml', - cors({ - origin: '*', - methods: ['GET'], - }), - express.static(path.resolve(__dirname, '../../../openapi.yaml')) - ) - logger.info({ - message: 'Swagger docs generated.', - }) -} - -export default swaggerLoader diff --git a/backend/src/core/routes/index.ts b/backend/src/core/routes/index.ts index 662078d0f..2d77e620c 100644 --- a/backend/src/core/routes/index.ts +++ b/backend/src/core/routes/index.ts @@ -184,16 +184,6 @@ export const InitV1Route = (app: Application): Router => { router.use('/protect', protectedMailRoutes) router.use('/unsubscribe', unsubscriberRoutes) - /** - * @swagger - * components: - * securitySchemes: - * bearerAuth: - * type: http - * scheme: bearer - * bearerFormat: username_versionNumber_apiKey - */ - router.use( '/campaigns', authMiddleware.getAuthMiddleware([AuthType.Cookie, AuthType.ApiKey]), diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b2c332c60..7e83726d0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -81,6 +81,7 @@ "@typescript-eslint/eslint-plugin": "5.31.0", "@typescript-eslint/parser": "5.31.0", "babel-core": "7.0.0-bridge.0", + "enquirer": "^2.4.1", "eslint": "8.8.0", "eslint-config-prettier": "8.3.0", "eslint-import-resolver-typescript": "2.5.0", @@ -6300,7 +6301,6 @@ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, - "peer": true, "engines": { "node": ">=6" } @@ -9175,7 +9175,6 @@ "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", "dev": true, - "peer": true, "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" diff --git a/frontend/package.json b/frontend/package.json index b3b3641cb..d4fcb3007 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -94,6 +94,7 @@ "@typescript-eslint/eslint-plugin": "5.31.0", "@typescript-eslint/parser": "5.31.0", "babel-core": "7.0.0-bridge.0", + "enquirer": "^2.4.1", "eslint": "8.8.0", "eslint-config-prettier": "8.3.0", "eslint-import-resolver-typescript": "2.5.0", diff --git a/package-lock.json b/package-lock.json index 6ec2421ab..c9663d6d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -98,6 +98,17 @@ "node": ">=8" } }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-escapes": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", @@ -358,6 +369,21 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",