diff --git a/.cspell.json b/.cspell.json
index 8c798e6f979..ca91640c256 100644
--- a/.cspell.json
+++ b/.cspell.json
@@ -17,6 +17,7 @@
"amannn",
"analagous",
"Angularjs",
+ "sinch",
"evalsha",
"antd",
"anthonybaxter",
diff --git a/apps/dashboard/public/images/providers/light/square/sinch.svg b/apps/dashboard/public/images/providers/light/square/sinch.svg
new file mode 100644
index 00000000000..21acf438412
--- /dev/null
+++ b/apps/dashboard/public/images/providers/light/square/sinch.svg
@@ -0,0 +1,8 @@
+
diff --git a/apps/web/public/static/images/providers/dark/sinch.png b/apps/web/public/static/images/providers/dark/sinch.png
new file mode 100644
index 00000000000..0f2032305c0
--- /dev/null
+++ b/apps/web/public/static/images/providers/dark/sinch.png
@@ -0,0 +1 @@
+iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==
diff --git a/apps/web/public/static/images/providers/dark/square/sinch.svg b/apps/web/public/static/images/providers/dark/square/sinch.svg
new file mode 100644
index 00000000000..21acf438412
--- /dev/null
+++ b/apps/web/public/static/images/providers/dark/square/sinch.svg
@@ -0,0 +1,8 @@
+
diff --git a/apps/web/public/static/images/providers/light/sinch.png b/apps/web/public/static/images/providers/light/sinch.png
new file mode 100644
index 00000000000..0f2032305c0
--- /dev/null
+++ b/apps/web/public/static/images/providers/light/sinch.png
@@ -0,0 +1 @@
+iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==
diff --git a/apps/web/public/static/images/providers/light/square/sinch.svg b/apps/web/public/static/images/providers/light/square/sinch.svg
new file mode 100644
index 00000000000..21acf438412
--- /dev/null
+++ b/apps/web/public/static/images/providers/light/square/sinch.svg
@@ -0,0 +1,8 @@
+
diff --git a/apps/web/src/pages/integrations/components/multi-provider/sort-providers.ts b/apps/web/src/pages/integrations/components/multi-provider/sort-providers.ts
index d6ee50919ac..2af0347d0b4 100644
--- a/apps/web/src/pages/integrations/components/multi-provider/sort-providers.ts
+++ b/apps/web/src/pages/integrations/components/multi-provider/sort-providers.ts
@@ -76,6 +76,7 @@ const providers: Record = {
SmsProviderIdEnum.BrevoSms,
SmsProviderIdEnum.ISendSms,
SmsProviderIdEnum.EazySms,
+ SmsProviderIdEnum.Sinch,
].sort(),
],
};
diff --git a/libs/application-generic/src/factories/sms/handlers/index.ts b/libs/application-generic/src/factories/sms/handlers/index.ts
index beffe9cae7b..f85320dc077 100644
--- a/libs/application-generic/src/factories/sms/handlers/index.ts
+++ b/libs/application-generic/src/factories/sms/handlers/index.ts
@@ -25,6 +25,7 @@ export * from './plivo.handler';
export * from './ring-central.handler';
export * from './sendchamp.handler';
export * from './simpletexting.handler';
+export * from './sinch.handler';
export * from './sms-central.handler';
export * from './sms77.handler';
export * from './sns.handler';
diff --git a/libs/application-generic/src/factories/sms/handlers/sinch.handler.ts b/libs/application-generic/src/factories/sms/handlers/sinch.handler.ts
new file mode 100644
index 00000000000..03225eb5db0
--- /dev/null
+++ b/libs/application-generic/src/factories/sms/handlers/sinch.handler.ts
@@ -0,0 +1,19 @@
+import { SinchSmsProvider } from '@novu/providers';
+import { ChannelTypeEnum, ICredentials, SmsProviderIdEnum } from '@novu/shared';
+import { BaseSmsHandler } from './base.handler';
+
+export class SinchHandler extends BaseSmsHandler {
+ constructor() {
+ super(SmsProviderIdEnum.Sinch, ChannelTypeEnum.SMS);
+ }
+
+ buildProvider(credentials: ICredentials) {
+ const config = credentials as Record;
+ this.provider = new SinchSmsProvider({
+ servicePlanId: config.servicePlanId,
+ apiToken: config.apiToken,
+ from: config.from,
+ region: config.region,
+ });
+ }
+}
diff --git a/libs/application-generic/src/factories/sms/sms.factory.ts b/libs/application-generic/src/factories/sms/sms.factory.ts
index 265d7955468..58846ed1eb6 100644
--- a/libs/application-generic/src/factories/sms/sms.factory.ts
+++ b/libs/application-generic/src/factories/sms/sms.factory.ts
@@ -27,6 +27,7 @@ import {
RingCentralHandler,
SendchampSmsHandler,
SimpletextingSmsHandler,
+ SinchHandler,
Sms77Handler,
SmsCentralHandler,
SnsHandler,
@@ -60,6 +61,7 @@ export class SmsFactory implements ISmsFactory {
new SendchampSmsHandler(),
new ClicksendSmsHandler(),
new SimpletextingSmsHandler(),
+ new SinchHandler(),
new BandwidthHandler(),
new GenericSmsHandler(),
new MessageBirdHandler(),
diff --git a/libs/dal/src/repositories/integration/integration.schema.ts b/libs/dal/src/repositories/integration/integration.schema.ts
index 489093eb8d1..67d9c687518 100644
--- a/libs/dal/src/repositories/integration/integration.schema.ts
+++ b/libs/dal/src/repositories/integration/integration.schema.ts
@@ -64,6 +64,7 @@ const integrationSchema = new Schema(
accessKey: Schema.Types.String,
appSid: Schema.Types.String,
senderId: Schema.Types.String,
+ servicePlanId: Schema.Types.String,
},
configurations: {
inboundWebhookEnabled: Schema.Types.Boolean,
diff --git a/packages/framework/src/schemas/providers/sms/index.ts b/packages/framework/src/schemas/providers/sms/index.ts
index f8b5d38393b..979f81ebce7 100644
--- a/packages/framework/src/schemas/providers/sms/index.ts
+++ b/packages/framework/src/schemas/providers/sms/index.ts
@@ -40,4 +40,5 @@ export const smsProviderSchemas = {
'afro-message': genericProviderSchemas,
unifonic: genericProviderSchemas,
imedia: genericProviderSchemas,
+ sinch: genericProviderSchemas,
} as const satisfies Record;
diff --git a/packages/framework/src/shared.ts b/packages/framework/src/shared.ts
index 440b766fb2a..13420eff91a 100644
--- a/packages/framework/src/shared.ts
+++ b/packages/framework/src/shared.ts
@@ -159,6 +159,7 @@ export enum SmsProviderIdEnum {
Unifonic = 'unifonic',
Smsmode = 'smsmode',
IMedia = 'imedia',
+ Sinch = 'sinch',
}
export enum ChatProviderIdEnum {
diff --git a/packages/providers/src/lib/sms/index.ts b/packages/providers/src/lib/sms/index.ts
index e6e4fc8e64c..bb7bb0ee6c7 100644
--- a/packages/providers/src/lib/sms/index.ts
+++ b/packages/providers/src/lib/sms/index.ts
@@ -24,6 +24,7 @@ export * from './plivo/plivo.provider';
export * from './ring-central/ring-central.provider';
export * from './sendchamp/sendchamp.provider';
export * from './simpletexting/simpletexting.provider';
+export * from './sinch/sinch.provider';
export * from './sms-central/sms-central.provider';
export * from './sms77/sms77.provider';
export * from './smsmode/smsmode.provider';
diff --git a/packages/providers/src/lib/sms/sinch/sinch.provider.spec.ts b/packages/providers/src/lib/sms/sinch/sinch.provider.spec.ts
new file mode 100644
index 00000000000..d38de234f28
--- /dev/null
+++ b/packages/providers/src/lib/sms/sinch/sinch.provider.spec.ts
@@ -0,0 +1,111 @@
+import axios from 'axios';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { SinchSmsProvider } from './sinch.provider';
+
+vi.mock('axios');
+
+describe('SinchSmsProvider', () => {
+ const mockConfig = {
+ servicePlanId: 'test-service-plan-id',
+ apiToken: 'test-api-token',
+ from: '+1234567890',
+ region: 'eu',
+ };
+
+ let provider: SinchSmsProvider;
+
+ beforeEach(() => {
+ provider = new SinchSmsProvider(mockConfig);
+ vi.clearAllMocks();
+ });
+
+ describe('sendMessage', () => {
+ it('should send an SMS message successfully', async () => {
+ const mockResponse = {
+ data: {
+ id: 'batch-123',
+ created_at: '2023-01-01T00:00:00Z',
+ },
+ };
+
+ vi.mocked(axios.post).mockResolvedValue(mockResponse);
+
+ const result = await provider.sendMessage({
+ to: '+9876543210',
+ content: 'Test message',
+ });
+
+ expect(axios.post).toHaveBeenCalledWith(
+ 'https://eu.sms.api.sinch.com/xms/v1/test-service-plan-id/batches',
+ {
+ from: '+1234567890',
+ to: ['+9876543210'],
+ body: 'Test message',
+ },
+ {
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer test-api-token',
+ },
+ }
+ );
+
+ expect(result).toEqual({
+ id: 'batch-123',
+ date: '2023-01-01T00:00:00Z',
+ });
+ });
+
+ it('should use custom from number if provided', async () => {
+ const mockResponse = {
+ data: {
+ id: 'batch-456',
+ created_at: '2023-01-02T00:00:00Z',
+ },
+ };
+
+ vi.mocked(axios.post).mockResolvedValue(mockResponse);
+
+ await provider.sendMessage({
+ to: '+9876543210',
+ content: 'Test message',
+ from: '+1111111111',
+ });
+
+ expect(axios.post).toHaveBeenCalledWith(
+ expect.any(String),
+ expect.objectContaining({
+ from: '+1111111111',
+ }),
+ expect.any(Object)
+ );
+ });
+
+ it('should use different region if configured', async () => {
+ const caProvider = new SinchSmsProvider({
+ ...mockConfig,
+ region: 'ca',
+ });
+
+ const mockResponse = {
+ data: {
+ id: 'batch-789',
+ created_at: '2023-01-03T00:00:00Z',
+ },
+ };
+
+ vi.mocked(axios.post).mockResolvedValue(mockResponse);
+
+ await caProvider.sendMessage({
+ to: '+9876543210',
+ content: 'Test message',
+ });
+
+ expect(axios.post).toHaveBeenCalledWith(
+ 'https://ca.sms.api.sinch.com/xms/v1/test-service-plan-id/batches',
+ expect.any(Object),
+ expect.any(Object)
+ );
+ });
+ });
+});
diff --git a/packages/providers/src/lib/sms/sinch/sinch.provider.ts b/packages/providers/src/lib/sms/sinch/sinch.provider.ts
new file mode 100644
index 00000000000..0c695f886bc
--- /dev/null
+++ b/packages/providers/src/lib/sms/sinch/sinch.provider.ts
@@ -0,0 +1,49 @@
+import { SmsProviderIdEnum } from '@novu/shared';
+import { ChannelTypeEnum, ISendMessageSuccessResponse, ISmsOptions, ISmsProvider } from '@novu/stateless';
+
+import axios from 'axios';
+import { BaseProvider, CasingEnum } from '../../../base.provider';
+import { WithPassthrough } from '../../../utils/types';
+
+export class SinchSmsProvider extends BaseProvider implements ISmsProvider {
+ id = SmsProviderIdEnum.Sinch;
+ protected casing = CasingEnum.CAMEL_CASE;
+ channelType = ChannelTypeEnum.SMS as ChannelTypeEnum.SMS;
+
+ constructor(
+ private config: {
+ servicePlanId?: string;
+ apiToken?: string;
+ from?: string;
+ region?: string;
+ }
+ ) {
+ super();
+ }
+
+ async sendMessage(
+ options: ISmsOptions,
+ bridgeProviderData: WithPassthrough> = {}
+ ): Promise {
+ const region = this.config.region || 'eu';
+ const url = `https://${region}.sms.api.sinch.com/xms/v1/${this.config.servicePlanId}/batches`;
+
+ const payload = this.transform>(bridgeProviderData, {
+ from: options.from || this.config.from,
+ to: [options.to],
+ body: options.content,
+ }).body;
+
+ const response = await axios.post(url, payload, {
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${this.config.apiToken}`,
+ },
+ });
+
+ return {
+ id: response.data.id,
+ date: response.data.created_at || new Date().toISOString(),
+ };
+ }
+}
diff --git a/packages/shared/src/consts/providers/channels/sms.ts b/packages/shared/src/consts/providers/channels/sms.ts
index 01c31c79123..1b16ea8e804 100644
--- a/packages/shared/src/consts/providers/channels/sms.ts
+++ b/packages/shared/src/consts/providers/channels/sms.ts
@@ -27,6 +27,7 @@ import {
ringCentralConfig,
sendchampConfig,
simpleTextingConfig,
+ sinchConfig,
sms77Config,
smsCentralConfig,
smsmodeProviderConfig,
@@ -291,6 +292,14 @@ export const smsProviders: IProviderConfig[] = [
docReference: 'https://telkosh.com/mobishastra/',
logoFileName: { light: 'mobishastra.png', dark: 'mobishastra.png' },
},
+ {
+ id: SmsProviderIdEnum.Sinch,
+ displayName: 'Sinch',
+ channel: ChannelTypeEnum.SMS,
+ credentials: sinchConfig,
+ docReference: `https://docs.novu.co/integrations/providers/sms/sinch${UTM_CAMPAIGN_QUERY_PARAM}`,
+ logoFileName: { light: 'sinch.png', dark: 'sinch.png' },
+ },
{
id: SmsProviderIdEnum.AfroSms,
displayName: 'Afro Message',
@@ -307,7 +316,7 @@ export const smsProviders: IProviderConfig[] = [
docReference: 'https://docs.unifonic.com/articles/#!products-documentation/getting-started-with-unifonic',
logoFileName: { light: 'unifonic.svg', dark: 'unifonic.svg' },
},
- {
+ {
id: SmsProviderIdEnum.Smsmode,
displayName: 'smsmode',
channel: ChannelTypeEnum.SMS,
diff --git a/packages/shared/src/consts/providers/credentials/provider-credentials.ts b/packages/shared/src/consts/providers/credentials/provider-credentials.ts
index ca8a9235576..2d9fa378212 100644
--- a/packages/shared/src/consts/providers/credentials/provider-credentials.ts
+++ b/packages/shared/src/consts/providers/credentials/provider-credentials.ts
@@ -1274,3 +1274,35 @@ export const smsmodeProviderConfig: IConfigCredential[] = [
},
...smsConfigBase,
];
+
+export const sinchConfig: IConfigCredential[] = [
+ {
+ key: CredentialsKeyEnum.ServicePlanId,
+ displayName: 'Service Plan ID',
+ description: 'Your Sinch Service Plan ID',
+ type: 'string',
+ required: true,
+ },
+ {
+ key: CredentialsKeyEnum.ApiToken,
+ displayName: 'API Token',
+ type: 'string',
+ required: true,
+ },
+ {
+ key: CredentialsKeyEnum.Region,
+ displayName: 'Region',
+ description: 'Select your Sinch region',
+ type: 'dropdown',
+ required: true,
+ value: 'eu',
+ dropdown: [
+ { name: 'EU (Ireland, Sweden)', value: 'eu' },
+ { name: 'US', value: 'us' },
+ { name: 'Australia', value: 'au' },
+ { name: 'Brazil', value: 'br' },
+ { name: 'Canada', value: 'ca' },
+ ],
+ },
+ ...smsConfigBase,
+];
diff --git a/packages/shared/src/entities/integration/credential.interface.ts b/packages/shared/src/entities/integration/credential.interface.ts
index afcd1bda74c..ef05147de8c 100644
--- a/packages/shared/src/entities/integration/credential.interface.ts
+++ b/packages/shared/src/entities/integration/credential.interface.ts
@@ -47,4 +47,5 @@ export interface ICredentials {
apiVersion?: string;
appSid?: string;
senderId?: string;
+ servicePlanId?: string;
}
diff --git a/packages/shared/src/types/providers.ts b/packages/shared/src/types/providers.ts
index eb860628225..8b3ff9bb811 100644
--- a/packages/shared/src/types/providers.ts
+++ b/packages/shared/src/types/providers.ts
@@ -49,6 +49,7 @@ export enum CredentialsKeyEnum {
ApiVersion = 'apiVersion',
AppSid = 'appSid',
SenderId = 'senderId',
+ ServicePlanId = 'servicePlanId',
}
export type ConfigurationKey = keyof IConfigurations;
@@ -115,6 +116,7 @@ export enum SmsProviderIdEnum {
// cspell:disable-next-line
Smsmode = 'smsmode',
IMedia = 'imedia',
+ Sinch = 'sinch',
}
export enum ChatProviderIdEnum {