diff --git a/compile.sh b/compile.sh
index 5b6a7d4..54d16b3 100755
--- a/compile.sh
+++ b/compile.sh
@@ -3,6 +3,7 @@
mvn -f pom-ci.xml clean spotless:apply
(cd templates/client && mvn clean package) || exit 1
+(cd templates/server && mvn clean package) || exit 1
(cd tutorials && ./compile.sh) || exit 1
(cd getting-started && ./compile.sh) || exit 1
diff --git a/templates/server/README.md b/templates/server/README.md
new file mode 100644
index 0000000..9f6c0dc
--- /dev/null
+++ b/templates/server/README.md
@@ -0,0 +1,78 @@
+# Backend application built using Sinch Java SDK to handle incoming webhooks
+
+This directory contains a server application based onto [Sinch SDK java](https://github.com/sinch/sinch-sdk-java)
+
+## Requirements
+
+- JDK 8 or later (Sinch SDK Java is requiring java 8 only but client application can use latest available version)
+- [Maven](https://maven.apache.org/)
+- [SpringBoot](https://spring.io/projects/spring-boot)
+- [Sinch account](https://dashboard.sinch.com)
+- [ngrok](https://ngrok.com/docs)
+
+## Configuration
+
+### Configure application settings
+
+com.mycompany.app.Application settings are using the `SpringBoot` configuration file: [`application.yaml`](src/main/resources/application.yaml) file and enable to configure:
+
+#### Sinch credentials
+Located in `credentials` section (*you can find all of the credentials you need on your [Sinch dashboard](https://dashboard.sinch.com)*):
+- `project-id`: YOUR_project_id
+- `key-id`: YOUR_access_key_id
+- `key-secret`: YOUR_access_key_secret
+
+#### Server port
+Default: 8090
+
+Located in `server` section:
+- port: The port to be used to listen to incoming requests. Default: 8090
+
+## Usage
+
+### Start server
+1. Edit configuration file
+
+ See above for Configuration paragraph
+
+2. Start server locally.
+
+ Compile and run the application as server locally.
+ ```bash
+ mvn spring-boot:run
+ ```
+### EndPoints
+When server is online, declared controllers will respond to following endpoints
+
+| Service | Endpoint |
+|--------------|--------------------|
+| Numbers | /NumbersEvent |
+| SMS | /SmsEvent |
+| Verification | /VerificationEvent |
+| Voice | /VoiceEvent |
+
+## Use ngrok to forward request to local server
+
+Forwarding request to same `8090` port used above:
+
+*Note: The `8090` value is coming from default config and can be changed (see [Server port](#Server port) configuration section)*
+
+```bash
+ngrok http 8090
+```
+
+ngrok output will contains output like:
+```
+ngrok (Ctrl+C to quit)
+
+...
+Forwarding https://0e64-78-117-86-140.ngrok-free.app -> http://localhost:8090
+
+```
+The line
+```
+Forwarding https://0e64-78-117-86-140.ngrok-free.app -> http://localhost:8090
+```
+Contains `https://0e64-78-117-86-140.ngrok-free.app` value.
+
+This value must be used to configure callback's URL from your [Sinch dashboard](https://dashboard.sinch.com/sms/api/services)
\ No newline at end of file
diff --git a/templates/server/pom.xml b/templates/server/pom.xml
new file mode 100644
index 0000000..034dda8
--- /dev/null
+++ b/templates/server/pom.xml
@@ -0,0 +1,51 @@
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.2.5
+
+
+
+ my.company.com
+ sinch-java-sdk-server-application
+ 0.0.1-SNAPSHOT
+ Sinch Java SDK Server Application
+
+
+ [1.0.0,)
+ 21
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ com.sinch.sdk
+ sinch-sdk-java
+ ${sinch.sdk.java.version}
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/templates/server/src/main/java/com/mycompany/app/Application.java b/templates/server/src/main/java/com/mycompany/app/Application.java
new file mode 100644
index 0000000..03b399f
--- /dev/null
+++ b/templates/server/src/main/java/com/mycompany/app/Application.java
@@ -0,0 +1,12 @@
+package com.mycompany.app;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+}
diff --git a/templates/server/src/main/java/com/mycompany/app/Config.java b/templates/server/src/main/java/com/mycompany/app/Config.java
new file mode 100644
index 0000000..425c5a8
--- /dev/null
+++ b/templates/server/src/main/java/com/mycompany/app/Config.java
@@ -0,0 +1,53 @@
+package com.mycompany.app;
+
+import com.sinch.sdk.SinchClient;
+import com.sinch.sdk.core.utils.StringUtil;
+import com.sinch.sdk.models.Configuration;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+
+@org.springframework.context.annotation.Configuration
+public class Config {
+
+ @Value("${credentials.project-id}")
+ String projectId;
+
+ @Value("${credentials.key-id}")
+ String keyId;
+
+ @Value("${credentials.key-secret}")
+ String keySecret;
+
+ @Value("${credentials.application-api-key}")
+ String applicationKey;
+
+ @Value("${credentials.application-api-secret}")
+ String applicationSecret;
+
+ @Bean
+ public SinchClient sinchClient() {
+
+ Configuration.Builder builder = Configuration.builder();
+
+ if (!StringUtil.isEmpty(projectId)) {
+ builder.setProjectId(projectId);
+ }
+
+ if (!StringUtil.isEmpty(keyId)) {
+ builder.setKeyId(keyId);
+ }
+ if (!StringUtil.isEmpty(keySecret)) {
+ builder.setKeySecret(keySecret);
+ }
+
+ if (!StringUtil.isEmpty(applicationKey)) {
+ builder.setApplicationKey(applicationKey);
+ }
+
+ if (!StringUtil.isEmpty(applicationSecret)) {
+ builder.setApplicationSecret(applicationSecret);
+ }
+
+ return new SinchClient(builder.build());
+ }
+}
diff --git a/templates/server/src/main/java/com/mycompany/app/numbers/Controller.java b/templates/server/src/main/java/com/mycompany/app/numbers/Controller.java
new file mode 100644
index 0000000..e312503
--- /dev/null
+++ b/templates/server/src/main/java/com/mycompany/app/numbers/Controller.java
@@ -0,0 +1,41 @@
+package com.mycompany.app.numbers;
+
+import com.sinch.sdk.SinchClient;
+import com.sinch.sdk.domains.numbers.api.v1.WebHooksService;
+import com.sinch.sdk.domains.numbers.models.v1.webhooks.NumberEvent;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController("Numbers")
+public class Controller {
+
+ private final SinchClient sinchClient;
+ private final ServerBusinessLogic webhooksBusinessLogic;
+
+ @Autowired
+ public Controller(SinchClient sinchClient, ServerBusinessLogic webhooksBusinessLogic) {
+ this.sinchClient = sinchClient;
+ this.webhooksBusinessLogic = webhooksBusinessLogic;
+ }
+
+ @PostMapping(
+ value = "/NumbersEvent",
+ consumes = MediaType.APPLICATION_JSON_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity NumbersEvent(@RequestBody String body) {
+
+ WebHooksService webhooks = sinchClient.numbers().v1().webhooks();
+
+ // decode the request payload
+ NumberEvent event = webhooks.parseEvent(body);
+
+ // let business layer process the request
+ webhooksBusinessLogic.numbersEvent(event);
+
+ return ResponseEntity.ok().build();
+ }
+}
diff --git a/templates/server/src/main/java/com/mycompany/app/numbers/ServerBusinessLogic.java b/templates/server/src/main/java/com/mycompany/app/numbers/ServerBusinessLogic.java
new file mode 100644
index 0000000..773b933
--- /dev/null
+++ b/templates/server/src/main/java/com/mycompany/app/numbers/ServerBusinessLogic.java
@@ -0,0 +1,16 @@
+package com.mycompany.app.numbers;
+
+import com.sinch.sdk.domains.numbers.models.v1.webhooks.NumberEvent;
+import java.util.logging.Logger;
+import org.springframework.stereotype.Component;
+
+@Component("NumbersServerBusinessLogic")
+public class ServerBusinessLogic {
+
+ private static final Logger LOGGER = Logger.getLogger(ServerBusinessLogic.class.getName());
+
+ public void numbersEvent(NumberEvent event) {
+
+ LOGGER.info("Handle event: " + event);
+ }
+}
diff --git a/templates/server/src/main/java/com/mycompany/app/sms/Controller.java b/templates/server/src/main/java/com/mycompany/app/sms/Controller.java
new file mode 100644
index 0000000..890f1ca
--- /dev/null
+++ b/templates/server/src/main/java/com/mycompany/app/sms/Controller.java
@@ -0,0 +1,51 @@
+package com.mycompany.app.sms;
+
+import com.sinch.sdk.SinchClient;
+import com.sinch.sdk.domains.sms.WebHooksService;
+import com.sinch.sdk.domains.sms.models.DeliveryReportBatch;
+import com.sinch.sdk.domains.sms.models.DeliveryReportRecipient;
+import com.sinch.sdk.domains.sms.models.InboundBinary;
+import com.sinch.sdk.domains.sms.models.InboundText;
+import com.sinch.sdk.domains.sms.models.webhooks.WebhooksEvent;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController("SMS")
+public class Controller {
+
+ private final SinchClient sinchClient;
+ private final ServerBusinessLogic webhooksBusinessLogic;
+
+ @Autowired
+ public Controller(SinchClient sinchClient, ServerBusinessLogic webhooksBusinessLogic) {
+ this.sinchClient = sinchClient;
+ this.webhooksBusinessLogic = webhooksBusinessLogic;
+ }
+
+ @PostMapping(
+ value = "/SmsEvent",
+ consumes = MediaType.APPLICATION_JSON_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity smsDeliveryEvent(@RequestBody String body) {
+
+ WebHooksService webhooks = sinchClient.sms().webHooks();
+
+ // decode the request payload
+ WebhooksEvent event = webhooks.parse(body);
+
+ // let business layer process the request
+ switch (event) {
+ case InboundBinary e -> webhooksBusinessLogic.processInboundEvent(e);
+ case InboundText e -> webhooksBusinessLogic.processInboundEvent(e);
+ case DeliveryReportRecipient e -> webhooksBusinessLogic.processDeliveryReportEvent(e);
+ case DeliveryReportBatch e -> webhooksBusinessLogic.processDeliveryReportEvent(e);
+ default -> throw new IllegalStateException("Unexpected value: " + event);
+ }
+
+ return ResponseEntity.ok().build();
+ }
+}
diff --git a/templates/server/src/main/java/com/mycompany/app/sms/ServerBusinessLogic.java b/templates/server/src/main/java/com/mycompany/app/sms/ServerBusinessLogic.java
new file mode 100644
index 0000000..561012e
--- /dev/null
+++ b/templates/server/src/main/java/com/mycompany/app/sms/ServerBusinessLogic.java
@@ -0,0 +1,35 @@
+package com.mycompany.app.sms;
+
+import com.sinch.sdk.domains.sms.models.DeliveryReportBatch;
+import com.sinch.sdk.domains.sms.models.DeliveryReportRecipient;
+import com.sinch.sdk.domains.sms.models.InboundBinary;
+import com.sinch.sdk.domains.sms.models.InboundText;
+import com.sinch.sdk.domains.sms.models.webhooks.WebhooksEvent;
+import java.util.logging.Logger;
+import org.springframework.stereotype.Component;
+
+@Component("SMSServerBusinessLogic")
+public class ServerBusinessLogic {
+
+ private static final Logger LOGGER = Logger.getLogger(ServerBusinessLogic.class.getName());
+
+ public void processInboundEvent(InboundText event) {
+ trace(event);
+ }
+
+ public void processInboundEvent(InboundBinary event) {
+ trace(event);
+ }
+
+ public void processDeliveryReportEvent(DeliveryReportRecipient event) {
+ trace(event);
+ }
+
+ public void processDeliveryReportEvent(DeliveryReportBatch event) {
+ trace(event);
+ }
+
+ private void trace(WebhooksEvent event) {
+ LOGGER.info("Handle event: " + event);
+ }
+}
diff --git a/templates/server/src/main/java/com/mycompany/app/verification/Controller.java b/templates/server/src/main/java/com/mycompany/app/verification/Controller.java
new file mode 100644
index 0000000..ccaa633
--- /dev/null
+++ b/templates/server/src/main/java/com/mycompany/app/verification/Controller.java
@@ -0,0 +1,78 @@
+package com.mycompany.app.verification;
+
+import com.sinch.sdk.SinchClient;
+import com.sinch.sdk.domains.verification.api.v1.WebHooksService;
+import com.sinch.sdk.domains.verification.models.v1.webhooks.VerificationRequestEvent;
+import com.sinch.sdk.domains.verification.models.v1.webhooks.VerificationResultEvent;
+import java.util.Map;
+import java.util.logging.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.server.ResponseStatusException;
+
+@RestController("Verification")
+public class Controller {
+
+ private static final Logger LOGGER = Logger.getLogger(Controller.class.getName());
+ private final SinchClient sinchClient;
+ private final ServerBusinessLogic webhooksBusinessLogic;
+
+ @Autowired
+ public Controller(SinchClient sinchClient, ServerBusinessLogic webhooksBusinessLogic) {
+ this.sinchClient = sinchClient;
+ this.webhooksBusinessLogic = webhooksBusinessLogic;
+ }
+
+ @PostMapping(
+ value = "/VerificationEvent",
+ consumes = MediaType.APPLICATION_JSON_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity VerificationEvent(
+ @RequestHeader Map headers, @RequestBody String body) {
+
+ WebHooksService webhooks = sinchClient.verification().v1().webhooks();
+
+ // ensure valid authentication to handle request
+ var validAuth =
+ webhooks.validateAuthenticationHeader(
+ // The HTTP verb this controller is managing
+ "POST",
+ // The URI this controller is managing
+ "/VerificationEvent",
+ // request headers
+ headers,
+ // request payload body
+ body);
+
+ // token validation failed
+ if (!validAuth) {
+ throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
+ }
+
+ // decode the request payload
+ var event = webhooks.parseEvent(body);
+
+ // let business layer process the request
+ var response =
+ switch (event) {
+ case VerificationRequestEvent e -> webhooksBusinessLogic.verificationEvent(e);
+ case VerificationResultEvent e -> {
+ webhooksBusinessLogic.verificationEvent(e);
+ yield null;
+ }
+ default -> throw new IllegalStateException("Unexpected value: " + event);
+ };
+
+ var serializedResponse = webhooks.serializeResponse(response);
+
+ LOGGER.finest("JSON response: " + serializedResponse);
+
+ return ResponseEntity.ok().body(serializedResponse);
+ }
+}
diff --git a/templates/server/src/main/java/com/mycompany/app/verification/ServerBusinessLogic.java b/templates/server/src/main/java/com/mycompany/app/verification/ServerBusinessLogic.java
new file mode 100644
index 0000000..6485d3e
--- /dev/null
+++ b/templates/server/src/main/java/com/mycompany/app/verification/ServerBusinessLogic.java
@@ -0,0 +1,26 @@
+package com.mycompany.app.verification;
+
+import com.sinch.sdk.domains.verification.models.v1.webhooks.VerificationRequestEvent;
+import com.sinch.sdk.domains.verification.models.v1.webhooks.VerificationRequestEventResponse;
+import com.sinch.sdk.domains.verification.models.v1.webhooks.VerificationResultEvent;
+import java.util.logging.Logger;
+import org.springframework.stereotype.Component;
+
+@Component("VerificationServerBusinessLogic")
+public class ServerBusinessLogic {
+
+ private static final Logger LOGGER = Logger.getLogger(ServerBusinessLogic.class.getName());
+
+ public VerificationRequestEventResponse verificationEvent(VerificationRequestEvent event) {
+
+ LOGGER.info("Handle event :" + event);
+
+ // add your logic here according to SMS, FlashCall, PhoneCall, ... verification
+ return null;
+ }
+
+ public void verificationEvent(VerificationResultEvent event) {
+
+ LOGGER.info("Handle event: " + event);
+ }
+}
diff --git a/templates/server/src/main/java/com/mycompany/app/voice/Controller.java b/templates/server/src/main/java/com/mycompany/app/voice/Controller.java
new file mode 100644
index 0000000..8150f29
--- /dev/null
+++ b/templates/server/src/main/java/com/mycompany/app/voice/Controller.java
@@ -0,0 +1,93 @@
+package com.mycompany.app.voice;
+
+import com.sinch.sdk.SinchClient;
+import com.sinch.sdk.domains.voice.WebHooksService;
+import com.sinch.sdk.domains.voice.models.webhooks.AnsweredCallEvent;
+import com.sinch.sdk.domains.voice.models.webhooks.DisconnectCallEvent;
+import com.sinch.sdk.domains.voice.models.webhooks.IncomingCallEvent;
+import com.sinch.sdk.domains.voice.models.webhooks.NotifyEvent;
+import com.sinch.sdk.domains.voice.models.webhooks.PromptInputEvent;
+import java.util.Map;
+import java.util.logging.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.server.ResponseStatusException;
+
+@RestController("Voice")
+public class Controller {
+
+ private final SinchClient sinchClient;
+ private final ServerBusinessLogic webhooksBusinessLogic;
+ private static final Logger LOGGER = Logger.getLogger(Controller.class.getName());
+
+ @Autowired
+ public Controller(SinchClient sinchClient, ServerBusinessLogic webhooksBusinessLogic) {
+ this.sinchClient = sinchClient;
+ this.webhooksBusinessLogic = webhooksBusinessLogic;
+ }
+
+ @PostMapping(
+ value = "/VoiceEvent",
+ consumes = MediaType.APPLICATION_JSON_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity VoiceEvent(
+ @RequestHeader Map headers, @RequestBody String body) {
+
+ WebHooksService webhooks = sinchClient.voice().webhooks();
+
+ // ensure valid authentication to handle request
+ var validAuth =
+ webhooks.validateAuthenticatedRequest(
+ // The HTTP verb this controller is managing
+ "POST",
+ // The URI this controller is managing
+ "/VoiceEvent",
+ // request headers
+ headers,
+ // request payload body
+ body);
+
+ // token validation failed
+ if (!validAuth) {
+ throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
+ }
+
+ // decode the payload request
+ var event = webhooks.unserializeWebhooksEvent(body);
+
+ // let business layer process the request
+ var response =
+ switch (event) {
+ case IncomingCallEvent e -> webhooksBusinessLogic.incoming(e);
+ case AnsweredCallEvent e -> webhooksBusinessLogic.answered(e);
+ case DisconnectCallEvent e -> {
+ webhooksBusinessLogic.disconnect(e);
+ yield null;
+ }
+ case PromptInputEvent e -> {
+ webhooksBusinessLogic.prompt(e);
+ yield null;
+ }
+ case NotifyEvent e -> {
+ webhooksBusinessLogic.notify(e);
+ yield null;
+ }
+ default -> throw new IllegalStateException("Unexpected value: " + event);
+ };
+
+ String serializedResponse = "";
+ if (null != response) {
+ serializedResponse = webhooks.serializeWebhooksResponse(response);
+ }
+
+ LOGGER.finest("JSON response: " + serializedResponse);
+
+ return ResponseEntity.ok().body(serializedResponse);
+ }
+}
diff --git a/templates/server/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java b/templates/server/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java
new file mode 100644
index 0000000..0e34f80
--- /dev/null
+++ b/templates/server/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java
@@ -0,0 +1,45 @@
+package com.mycompany.app.voice;
+
+import com.sinch.sdk.domains.voice.models.svaml.SVAMLControl;
+import com.sinch.sdk.domains.voice.models.webhooks.AnsweredCallEvent;
+import com.sinch.sdk.domains.voice.models.webhooks.DisconnectCallEvent;
+import com.sinch.sdk.domains.voice.models.webhooks.IncomingCallEvent;
+import com.sinch.sdk.domains.voice.models.webhooks.NotifyEvent;
+import com.sinch.sdk.domains.voice.models.webhooks.PromptInputEvent;
+import java.util.logging.Logger;
+import org.springframework.stereotype.Component;
+
+@Component("VoiceServerBusinessLogic")
+public class ServerBusinessLogic {
+
+ private static final Logger LOGGER = Logger.getLogger(ServerBusinessLogic.class.getName());
+
+ public SVAMLControl incoming(IncomingCallEvent event) {
+
+ LOGGER.info("Handle event :" + event);
+
+ return SVAMLControl.builder().build();
+ }
+
+ public SVAMLControl answered(AnsweredCallEvent event) {
+
+ LOGGER.info("Handle event: " + event);
+
+ return SVAMLControl.builder().build();
+ }
+
+ public void disconnect(DisconnectCallEvent event) {
+
+ LOGGER.info("Handle event: " + event);
+ }
+
+ public void prompt(PromptInputEvent event) {
+
+ LOGGER.info("Handle event: " + event);
+ }
+
+ public void notify(NotifyEvent event) {
+
+ LOGGER.info("Handle event: " + event);
+ }
+}
diff --git a/templates/server/src/main/resources/application.yaml b/templates/server/src/main/resources/application.yaml
new file mode 100644
index 0000000..1251ccf
--- /dev/null
+++ b/templates/server/src/main/resources/application.yaml
@@ -0,0 +1,22 @@
+# springboot related config file
+
+logging:
+ level:
+ com: INFO
+
+server:
+ port: 8090
+
+credentials:
+ # Unified related credentials, used by:
+ # - Numbers
+ # - SMS
+ project-id:
+ key-id:
+ key-secret:
+
+ # Application related credentials, used by:
+ # - Verification
+ # - Voice
+ application-api-key:
+ application-api-secret: