Integrating webhooks

Discover how you can listen to Transatel events by registering webhooks


Learn how to receive Transatel events on your platform using the steps in this guide:

  1. Identify relevant events for your use cases and their payload definition
  2. Create a webhook endpoint accepting HTTP POST JSON payload
  3. Handle Transatel events and execute your own logic
  4. Check event signature
  5. Register your webhook for the selected events
  6. Test your integration by sending test events of your choice
  7. Activate your webhook whenever you are ready
  8. Implement mTLS authentication

Endpoints

Below are all APIs url that you will need for this guide:

Transatel APIsURL
Authenticationhttps://api.transatel.com/authentication/api
Webhookshttps://api.transatel.com/webhooks/api

All Transatel APIs are available through our unique API gateway https://api.transatel.com

1. Identify relevant events

Start by reviewing all available Transatel events and identify which ones may be useful to trigger your logic:

Then review the specifications of those events that your endpoint will have to process.

2. Create a webhook endpoint

To receive events, you will need to create an HTTP endpoint accepting POST requests of JSON payload. This endpoint should in return respond with a HTTP/1.1 204 No content.

You can of course, implement this endpoint in the language / framework of your choice. Below a simple example in Java using the Spark micro framework.

import static spark.Spark.port;
import static spark.Spark.post;
public class WebhookEndpoint {
public static void main(String[] args) {
port(8080);
post("/events", (request, response) -> {
response.status(204);
return "";
});
}
}

3. Handle Transatel events

Now that your endpoint is defined, you will need to add the event processing logic.
It's generally a 3 steps implementation:

  1. Parse the event and detect its type
  2. Execute / trigger your specific logic
  3. Returns a HTTP/1.1 204 No content response

Parse the event

Every Transatel event shares a common structure with an header holding information such as the eventId or the eventType and a body holding all other information specific to the event.

Below is a partial example of an event:

{
"header": {
"eventId": "627e77fc-7694-4188-8f19-13ca3dbf8f51",
"eventType": "OCS/PRODUCT/ACTIVATED",
"eventDate": "2023-04-19T13:30:00Z"
},
"body": {
"mvnoRef": "<Customer account name>",
"cos": "<Name of the class of service>",
"msisdn": "33612345678",
"iccid": "8988247076000000319",
"externalReference": "SCOT78LH27",
...
}
}

Parsing the header will allow you to detect which type of event you receive and as such determine which logic you have to trigger. The body will also inform your of the detail of the event and more specifically which subscriber was concerned thanks to the msisdn and iccid fields.

Very important, when parsing events, always:

  • declare only useful fields
  • systematically ignore all unknown fields

This will guarantee your integration to be compatible with future backward compatible evolutions.

Execute your specific logic

Once you have detected which type of event you received and for which subscriber, you can execute your specific logic based on your use cases.

For complex logic, we recommend that you perform it asynchronously to avoid timeout issues.

Return a 204 response

Finally, your endpoint should respond with a HTTP/1.1 204 No content indicating that the event was well-received. In case, your endpoint incorrectly responds with a KO response status (3xx, 4xx, 5xx) , the event delivery will be automatically retried.

Below is an example of implementation:

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.slf4j.Slf4j;
import static spark.Spark.port;
import static spark.Spark.post;
@Slf4j
public class WebhookEndpoint {
public static void main(String[] args) {
final ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
port(8080);
post("/events", (request, response) -> {
final var event = objectMapper.readValue(request.body(), Event.class);
final var eventType = event.getHeader().getEventType();
log.info("Received an {} event for subscriber: {}", eventType, event.getBody().getMsisdn());
switch (eventType) {
case OCS_PRODUCT_ACTIVATED:
// Execute your specific product activation logic
break;
case OCS_PRODUCT_EXPIRED:
// Execute your specific product expiration logic
break;
//...
}
response.status(204);
return "";
});
}
}

Now that your endpoint is ready and before registering it in your webhooks, you will need to make it publicly accessible over HTTPS.

4. Check event signature (recommended)

As an extra step, we highly recommended that you secure your webhook integration by verifying the signature sent with the events. This will guarantee that those events are originating from our platform and not from another server acting as Transatel.

To do so, two things are required:

  1. Define a secret when registering your webhook
  2. Verify the signature of each event received on your endpoint

Define a secret on your webhook

This is as easy as adding a field secret when creating your webhook (See below for explanations on how to create your webhook). This secret will then be used by our platform to sign all events sent to your webhook endpoint.

The signature will be provided as a X-TSL-Signature-256 HTTP header with the following definition: sha256=hex(HMACSHA256(payload))
(Example: sha256=a4ecb55b23be547f1b82e24f46beef8a0856675d85bd5baebd9ef0e3a05e7e3e)

Verify the payload signature

To verify, the signature you will need to adapt a little your endpoint implementation in order to compute the signature based on the secret you know and compare it to the signature received with the event. Only if those signatures match you should accept the event and proceed further.

Below is the previous implementation example modified to include payload signature verification:

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.HmacAlgorithms;
import org.apache.commons.codec.digest.HmacUtils;
import spark.Request;
import static spark.Spark.*;
import static spark.utils.StringUtils.isEmpty;
@Slf4j
public class WebhookEndpoint {
private static final String HEADER_HMAC_SIGNATURE = "X-TSL-Signature-256";
private static final String HMAC_PREFIX = "sha256=";
private static final String SECRET = "my-secret";
public static void main(String[] args) {
final ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
port(8080);
post("/events", (request, response) -> {
checkSignature(request);
final var event = objectMapper.readValue(request.body(), Event.class);
final var eventType = event.getHeader().getEventType();
log.info("Received an {} event for subscriber: {}", eventType, event.getBody().getMsisdn());
switch (eventType) {
case OCS_PRODUCT_ACTIVATED:
// Execute your specific product activation logic
break;
case OCS_PRODUCT_EXPIRED:
// Execute your specific product expiration logic
break;
//...
}
response.status(204);
return "";
});
}
private static void checkSignature(Request req) {
final String requestSignature = req.headers(HEADER_HMAC_SIGNATURE);
if (isEmpty(requestSignature)) {
log.error("Missing payload signature");
halt(403);
}
final String sign = HMAC_PREFIX + new HmacUtils(HmacAlgorithms.HMAC_SHA_256, SECRET)
.hmacHex(req.bodyAsBytes());
if (!sign.equals(requestSignature)) {
log.error("Invalid request signature");
halt(403);
}
}
}

5. Register your webhook

First thing first, let's get your access token by following the getting started guide!

Now that everything is ready to receive those events, let's register your first webhook. In this example below, we will only subscribe to OCS product activation event, but you can of course subscribe to any events of your choice depending on your requirements.

To do so, we will use the Webhooks API.

The below example shows how to register an inactive webhook and subscribe to the OCS product activation event:

curl --location --request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJidXNpbmVzc19lbnRpdGllcyI6WyJBbnkiXSwicHJpbmNpcGFsIjoib2NzLXN1YnNjcmlwdGlvbi1vcmRlci1tYW5hZ2VyIiwic2NvcGUiOlsiQ0FUQUxPR19QUk9EVUNUU19SRUFEX1RFQ0hOSUNBTCIsIkNBVEFMT0dfUFJPRFVDVFNfUkVBRCIsIlVTRVJfUkVBRCIsIklOVkVOVE9SWV9TVUJTQ1JJUFRJT05TX1JFQUQiXSwiZXhwIjoxNjE3MjYwNTYxLCJhdXRob3JpdGllcyI6WyJST0xFX0lOVEVSTkFMX1NZU1RFTSJdLCJqdGkiOiJmOTYwNTFmNS05ZmQzLTQxMDEtYjNkZC0xZjY2Y2Q2MWUzZjIiLCJjbGllbnRfaWQiOiJvY3Mtc3Vic2NyaXB0aW9uLW9yZGVyLW1hbmFnZXIifQ.KBTlltd459_4kPV0O3OfsBTFGBtoqRxG65o_NohK1U7IuJAvIjAa8Fj4Qon1ptFrrTR5M2o8l0f9Dl0D7r0Us6Ej6OsVlFXfQKUiXwVytNUTP7POB6l56Svc5CaVqkXgFKyt7_8h6Ii6R_RteujtSzJSWX7zeLSeemxsxKNtHLLIS_HLyNdQyIsVCZWRfFhdPJVQId_p6B08_A54sAwhrT7qssSbtpG1nGzdqsHdFfaqJt9ABhPedatHNlwcWtCnFthrOrD2rv0Yf8OnRLnt-ZWtypr0cqm6U-brtZeBMhcxgzF4afKlJbKN3K3drPJRrRPjL8L9bf2YvzsDWxY5lA' \
--data \
"{
\"mvnoRef\": \"M2MA_WW_TSL_MWC_INTERNAL\",
\"status\": \"inactive\",
\"targetUrl\": \"https://example.com/events\",
\"secret\": \"my-secret\",
\"events\": [
\"OCS/PRODUCT/ACTIVATED\"
]
}" \
'https://api.transatel.com/webhooks/api/webhooks'

Above are highlighted the main information to provide to the API:

  • Authorization header: This is where you provide the previously retrieved access_token
  • mvnoRef: Customer account identifier provided by Transatel at setup
  • status: The status of the webhook, here inactive which means that events occurring on the platform won't be sent immediately to your endpoint
  • targetUrl: The HTTPS url to which events will be posted
  • secret: An optional but recommended secret used to sign the payload
  • events: An array with the events you want to subscribe to, here the product activation event

If successful, the response returned will be as follows:

HTTP/1.1 201 Created
Content-Type: application/json
{
"id": "257728ad-01ed-447d-b923-062604926f95",
"mvnoRef": "M2MA_WW_TSL_MWC_INTERNAL",
"status": "inactive",
"targetUrl": "https://example.com/events",
"events": [
{
"eventType": "OCS/PRODUCT/ACTIVATED",
"description": "A subscription to a product has been activated",
"domain": "OCS",
"category": "products",
"_links": {
"doc": {
"href": "https://developers.transatel.com/api/ocs/events/#tag/Products/paths/OCS~1PRODUCT~1ACTIVATED/post"
}
}
}
],
"_links": {
"self": {
"href": "https://api.transatel.com/webhooks/api/webhooks/257728ad-01ed-447d-b923-062604926f95"
}
}
}

A successful response returns a HTTP/1.1 201 Created with the details of the webhook.

We recommend that at first you create webhooks as inactive, this way you won't receive any platform event yet, giving you time to validate your integration thanks to our simulation event operation.

6. Test your integration

You are now ready to test your webhook by simulating an OCS product activation event!

To do so, we will use the Webhooks API.

The below example shows how to simulate an OCS product activation event:

curl --location --request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJidXNpbmVzc19lbnRpdGllcyI6WyJBbnkiXSwicHJpbmNpcGFsIjoib2NzLXN1YnNjcmlwdGlvbi1vcmRlci1tYW5hZ2VyIiwic2NvcGUiOlsiQ0FUQUxPR19QUk9EVUNUU19SRUFEX1RFQ0hOSUNBTCIsIkNBVEFMT0dfUFJPRFVDVFNfUkVBRCIsIlVTRVJfUkVBRCIsIklOVkVOVE9SWV9TVUJTQ1JJUFRJT05TX1JFQUQiXSwiZXhwIjoxNjE3MjYwNTYxLCJhdXRob3JpdGllcyI6WyJST0xFX0lOVEVSTkFMX1NZU1RFTSJdLCJqdGkiOiJmOTYwNTFmNS05ZmQzLTQxMDEtYjNkZC0xZjY2Y2Q2MWUzZjIiLCJjbGllbnRfaWQiOiJvY3Mtc3Vic2NyaXB0aW9uLW9yZGVyLW1hbmFnZXIifQ.KBTlltd459_4kPV0O3OfsBTFGBtoqRxG65o_NohK1U7IuJAvIjAa8Fj4Qon1ptFrrTR5M2o8l0f9Dl0D7r0Us6Ej6OsVlFXfQKUiXwVytNUTP7POB6l56Svc5CaVqkXgFKyt7_8h6Ii6R_RteujtSzJSWX7zeLSeemxsxKNtHLLIS_HLyNdQyIsVCZWRfFhdPJVQId_p6B08_A54sAwhrT7qssSbtpG1nGzdqsHdFfaqJt9ABhPedatHNlwcWtCnFthrOrD2rv0Yf8OnRLnt-ZWtypr0cqm6U-brtZeBMhcxgzF4afKlJbKN3K3drPJRrRPjL8L9bf2YvzsDWxY5lA' \
--data \
"{
\"eventType\": \"OCS/PRODUCT/ACTIVATED\"
}" \
'https://api.transatel.com/webhooks/api/webhooks/257728ad-01ed-447d-b923-062604926f95'

Above are highlighted the main information to provide to the API:

  • Authorization header: This is where you provide the previously retrieved access_token
  • eventType: The event type of the event you want to simulate
  • webhookId: The ID of the webhook in the URL for which we need to send the simulation event

If successful, the response returned will be as follows:

HTTP/1.1 200 OK
{
"success": true,
"response": {
"status": "204 NO_CONTENT",
"headers": {
"foo": "bar"
},
"body": {
"field": "value"
}
}
}

A successful response returns a HTTP/1.1 200 OK with the details of the response that we received from your webhook endpoint.

This operation can be used to:

  • Troubleshoot communication issues between our platform and yours
  • Validate the correct implementation on your webhook endpoint by simulating the different Transatel events

7. Activate your webhook

Now that everything works great, let's activate your webhook and start receiving real events!

To do so, we will use the Webhooks API.

The below example shows how to update the webhook and switch its status to active:

curl --location --request PUT \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJidXNpbmVzc19lbnRpdGllcyI6WyJBbnkiXSwicHJpbmNpcGFsIjoib2NzLXN1YnNjcmlwdGlvbi1vcmRlci1tYW5hZ2VyIiwic2NvcGUiOlsiQ0FUQUxPR19QUk9EVUNUU19SRUFEX1RFQ0hOSUNBTCIsIkNBVEFMT0dfUFJPRFVDVFNfUkVBRCIsIlVTRVJfUkVBRCIsIklOVkVOVE9SWV9TVUJTQ1JJUFRJT05TX1JFQUQiXSwiZXhwIjoxNjE3MjYwNTYxLCJhdXRob3JpdGllcyI6WyJST0xFX0lOVEVSTkFMX1NZU1RFTSJdLCJqdGkiOiJmOTYwNTFmNS05ZmQzLTQxMDEtYjNkZC0xZjY2Y2Q2MWUzZjIiLCJjbGllbnRfaWQiOiJvY3Mtc3Vic2NyaXB0aW9uLW9yZGVyLW1hbmFnZXIifQ.KBTlltd459_4kPV0O3OfsBTFGBtoqRxG65o_NohK1U7IuJAvIjAa8Fj4Qon1ptFrrTR5M2o8l0f9Dl0D7r0Us6Ej6OsVlFXfQKUiXwVytNUTP7POB6l56Svc5CaVqkXgFKyt7_8h6Ii6R_RteujtSzJSWX7zeLSeemxsxKNtHLLIS_HLyNdQyIsVCZWRfFhdPJVQId_p6B08_A54sAwhrT7qssSbtpG1nGzdqsHdFfaqJt9ABhPedatHNlwcWtCnFthrOrD2rv0Yf8OnRLnt-ZWtypr0cqm6U-brtZeBMhcxgzF4afKlJbKN3K3drPJRrRPjL8L9bf2YvzsDWxY5lA' \
--data \
"{
\"mvnoRef\": \"M2MA_WW_TSL_MWC_INTERNAL\",
\"status\": \"active\",
\"targetUrl\": \"https://example.com/events\",
\"secret\": \"my-secret\",
\"events\": [
\"OCS/PRODUCT/ACTIVATED\"
]
}" \
'https://api.transatel.com/webhooks/api/webhooks/257728ad-01ed-447d-b923-062604926f95'

Above are highlighted the main information to provide to the API:

  • Authorization header: This is where you provide the previously retrieved access_token
  • status: The status of the webhook, here we update it to active
  • webhookId: The ID of the webhook in the URL for which we need to send the simulation event

If successful, the response returned will be as follows:

HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "257728ad-01ed-447d-b923-062604926f95",
"mvnoRef": "M2MA_WW_TSL_MWC_INTERNAL",
"status": "active",
"targetUrl": "https://example.com/events",
"events": [
{
"eventType": "OCS/PRODUCT/ACTIVATED",
"description": "A subscription to a product has been activated",
"domain": "OCS",
"category": "products",
"_links": {
"doc": {
"href": "https://developers.transatel.com/api/ocs/events/#tag/Products/paths/OCS~1PRODUCT~1ACTIVATED/post"
}
}
}
],
"_links": {
"self": {
"href": "https://api.transatel.com/webhooks/api/webhooks/257728ad-01ed-447d-b923-062604926f95"
}
}
}

A successful response returns a HTTP/1.1 200 OK with the details of the webhook.

As an extra step, we highly recommend that you secure your webhook integration by implementing mTLS. mTLS ensures that the parties at each end of a network connection are who they claim to be by verifying that they both have the correct private key.

To do so, two things are required:

  1. Implement Transatel certificate CA chain in your truststore
  2. Verify the CN client certificate received on your endpoint

Implement Transatel certificate CA chain in your truststore

Regardless of your HTTPS endpoint, you must implement a trusted list of certificate authorities. It’s often a PEM file containing a concatenated list of trusted CAs, base64 encoded.

Transatel use Sectigo CA as public trusted authority, with this complete chain:

  • webservice-client.transatel.com - subject: CN = webservice-client.transatel.com
  • Intermediate 1 - subject: C = GB, ST = Greater Manchester, L = Salford, O = Sectigo Limited, CN = Sectigo RSA Domain Validation Secure Server CA
  • Intermediate 2 - subject: C = US, ST = New Jersey, L = Jersey City, O = The USERTRUST Network, CN = USERTrust RSA Certification Authority
  • Root CA - subject: C = GB, ST = Greater Manchester, L = Salford, O = Comodo CA Limited, CN = AAA Certificate Services

Your truststore can be a concatenation from intermediate 1 to Root CA from certificates page.

At this point, the hand-check can be performed with all certificates signed by this chain.

Sample of nginx configuration:

server {
listen 443 ssl default_server;
ssl_verify_client on;
ssl_client_certificate /path/to/trustore.pem;
ssl_verify_depth 2;
...
}

Sample of Apache2 configuration:

<VirtualHost *:443>
SSLVerifyClient require
SSLCACertificateFile "/path/to/trustore.pem"
SSLVerifyDepth 2
...
</VirtualHost>

Verify the CN client certificate received on your endpoint

Now, at HTTP level, you can filter allowed connections to CN webservice-client.transatel.com client certificates.

With ngnix:

location / {
if ($ssl_client_s_dn !~ ‘webservice-client.transatel.com’) {
return 403;
}

With Apache2:

<Directory /var/www/> # or Location/Proxy ...
Require expr "%{SSL_CLIENT_S_DN_CN} == 'webservice-client.transatel.com'"
...
</Directory>

What to do next?

Learn more about our Webhooks API and Transatel events by checking API references and overviews: