We're looking for talented devs to join our team! Check out our job openings
Photo of Taylor Reece
Taylor Reece, Developer Advocate
October 27, 2021 • 11 min read
Tutorial

Writing Triggers to Handle Unique Third-Party Needs

We recently introduced the ability for you to write your own triggers within your custom components. This gives you more control over the functionality of webhook triggers.

Why did we do this? The third-party apps and services that you integrate with often have unique requirements when it comes to webhooks. Some third parties expect confirmation messages or particular responses when they send webhook requests, and others send proprietary data that needs to be parsed before it's useful in an integration.

Using Prismatic's existing custom component SDK, developers now have the flexibility to customize how a webhook trigger responds to a request from a third party and to encapsulate third-party-specific logic within triggers. Bundling up these nitty-gritty details within the trigger abstracts many complexities from integration builders. This means your integration builders - who are often non-developers - don't need deep familiarity with webhooks and can now build integrations faster and with fewer steps.

In this post, we'll look at a couple examples of third-party services - Salesforce and Amazon SNS - and examine how we built component-specific triggers to help make integrations with Salesforce and Amazon SNS easier to build. We're using these two common services as examples - the industry-specific third-party services you integrate with will have different idiosyncrasies that you can account for by writing your own triggers.

How the Standard Prismatic Webhook Trigger Works

Before we dive in to writing new triggers, let's first look at Prismatic's standard webhook trigger. A Prismatic integration that starts with a standard webhook trigger can be invoked by any HTTP client. An invocation with with some JSON data might like this:

curl 'https://hooks.prismatic.io/trigger/EXAMPLE1FjMmUtOWNiZS00MmI3LT' \
  --request POST \
  --header "Content-Type: application/json" \
  --data '{"renderId":51266,"status":"queued"}'

When a Prismatic webhook trigger receives this sort of request, an execution of an instance is kicked off and the trigger responds with the ID of the execution that was started:

{ "executionId": "cd794a44-5edb-44ec-bf9e-d945a464fd21" }

The standard webhook trigger works well in many situations - lots of third-party systems can be configured to fire off a webhook payload to Prismatic and are happy to get an HTTP 200 ("OK") back.

Many Third Parties Expect Unique Behavior From Webhooks

While the standard webhook trigger works for many third-party services, other third parties expect unique behavior from webhooks beyond returning an execution ID and "OK" response.

For example, Salesforce sends XML (not JSON), and expects a very particular XML response back. If Salesforce doesn't get the XML response back that it wants, it'll repeatedly try the same request dozens of times over 24 hours until it eventually gives up.

Amazon SNS is fine with any response, but when you first set up SNS to point to a webhook endpoint it sends a "confirm subscription" payload to that endpoint. It expects the owner of the webhook to make a request to a URL that SNS specifies in its payload to confirm that it's listening properly.

Now, you can technically handle both Salesforce and Amazon SNS integrations through a series of integration steps, but it gets messy.

Every Salesforce integration would need to be called synchronously, and the last step would need to return an "Acknowledgment" (ACK) XML response back to Salesforce. Additionally, every Salesforce integration would need to start with a Deserialize XML step in order to parse the payload that came in.

Every integration with Amazon SNS would need to start with some "confirm subscription" logic, and SNS integrators would need to reinvent that wheel with each new SNS integration they create.

These are just a couple of examples, but you get the idea - not all webhooks are the same and different vendors have different needs.

Addressing Unique Challenges With Your Own Triggers

To address these unique challenges with third-party vendors, we've extended our custom component SDK to allow you write your own triggers. You can encapsulate whatever logic you want into your own trigger, so integration builders don't need to add additional steps after the trigger to do things like confirm a subscription or deserialize XML.

The big take away here is that you can encapsulate common tasks, like creating custom responses, preprocessing data, or managing subscriptions, within your own trigger so your integration builders don't need to worry about them.

If you're familiar with writing custom components using the Prismatic SDK, writing a trigger will feel very familiar - a trigger is pretty much an action with a couple of special features.

By writing your own trigger you can:

  • Customize responses. If you're integrating with a third party similar to Salesforce, you can customize the response that Prismatic sends back to the webhook caller.
  • Process data that comes in. Does your third party send XML, CSV, or YAML data? Your trigger can take care of deserializing that data so it's ready for the rest of your integrations to consume. Integration builders won't need to add additional data processing steps.
  • Validate webhook payloads. Third-party apps and services often use signing keys to sign the payloads they send to webhook URLs. You can handle the request validation as part of your trigger so validation doesn't become an additional step in each integration.
  • Manage subscriptions. Amazon SNS is just one of many platforms that require you to confirm a webhook subscription (other services like Mailchimp and Microsoft Teams work similarly). You can handle that confirmation logic in your trigger, so your integration builders don't need to.

Let's look at each of these topics in more detail, using the built-in Salesforce and Amazon SNS triggers as examples.

Custom Webhook Responses

As I mentioned above, a standard webhook trigger returns the instance's execution ID (or the results of the last step of the execution, depending on if the integration is invoked asynchronously or synchronously). To send a different response to a webhook caller, all you need to do is return a custom response within your perform function. Salesforce requires a very particular acknowledgement (ACK) response in XML format, and an HTTP 200 status code.

We can see that custom response here:

import { HttpResponse, trigger } from "@prismatic-io/spectral";

export const salesforceTrigger = trigger({
  //...
  perform: async (context, payload, params) => {
    // ...

    // Create the HTTP response that Salesforce expects
    const response: HttpResponse = {
      statusCode: 200,
      contentType: "text/xml; charset=utf-8",
      body: `
      <soapenv:Envelope 
         xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
       <soapenv:Body>
        <notificationsResponse xmlns="http://soap.sforce.com/2005/09/outbound">
         <Ack>true</Ack>
        </notificationsResponse>
       </soapenv:Body>
      </soapenv:Envelope>`,
    };

    // Return the XML response
    return Promise.resolve({
      response,
    });
  },
});

With that simple bit of code we can send Salesforce the response they expect (so they don't keep sending the same payload to our integration over and over for 24 hours).

Your HTTP responses can be dynamic, too. Suppose you want to send an HTTP redirect, so callers of your webhook are bounced to somewhere else in your app after invoking a webhook. You can redirect webhook callers by responding with a statusCode of 302 (redirect), and a Location header you've dynamically generated.

Here's an example of a dynamically-generated HTTP response:

import { HttpResponse } from "@prismatic-io/spectral";

export const myRedirectTrigger = trigger({
  //...
  perform: async (context, payload, params) => {
    // ...

    // Figure out where to redirect the webhook caller
    const widgetsId = getWidgetsId(payload.rawBody.data);
    const redirectUrl = `https://app.acmeerp.com/widgets/${widgetsId}`;

    // Respond with a HTTP 302 - Redirect to Acme ERP
    const response: HttpResponse = {
      statusCode: 302,
      headers: { Location: redirectUrl },
      contentType: "text/plain",
      body: `You are being redirect to "${redirectUrl}".`,
    };
    return Promise.resolve({
      response,
    });
  },
});

Preprocess Data That Comes In

There's no point in making integration builders "reinvent the wheel" with every integration to some third-party app or service. If there are tasks that need to be taken care of whenever you integrate with a particular third party, you can wrap those tasks nicely into a trigger.

Salesforce, for example, has outbound messages (their form of webhook) that contain XML payloads. If every Salesforce webhook payload contains XML, we should probably deserialize the XML in the trigger. Otherwise, we'd have to start every integration with Salesforce with a Deserialize XML step.

Deserializing the XML payload that comes in can be done with just a few lines of code - we did it by importing the fast-xml-parser library, and feeding the body of the webhook request into the library's parse function.

The deserialized XML is saved as the trigger's payload.body.data:

import {
  trigger,
  TriggerPayload,
  HttpResponse,
  util,
} from "@prismatic-io/spectral";

import { parse } from "fast-xml-parser";

export const salesforceTrigger = trigger({
  // ...
  perform: async (context, payload, params) => {
    // ...

    // Make a copy of the payload that the trigger received
    const finalPayload: TriggerPayload = { ...payload };

    const parseOptions = {
      ignoreAttributes: false,
      ignoreNameSpace: false,
      textNodeName: "_text",
    };

    // Deserialize XML to JS Object.
    finalPayload.body.data =
      parse(util.types.toString(finalPayload.body.data), parseOptions) || {};
    delete finalPayload.body.contentType;

    // Return the payload, with a deserialized body.
    return Promise.resolve({
      payload: finalPayload,
    });
  },
});

This gives integration builders the ability to drill in to the XML data that comes in - they can reference any property of the XML data as though it were a deserialized object, rather than needing to parse a serialized XML string themselves:

Screeshot of integration trigger using XML data in an embedded iPaaS platform

Deserialization of data is not limited to common formats (XML, CSV, YAML, JSON, etc.) - you can preprocess any proprietary format of data that comes in. So long as you can process data with NodeJS, you can process it within a trigger.

Validate Webhook Payloads

Another common task besides data processing that integrators often need to deal with is validating webhook payloads. You want to be sure that a webhook request originated from a particular third-party system, and not from some random person online who figured out your webhook URL. Some third parties sign webhook requests that they send with a private signing key, and you as a webhook receiver can validate the payload's signature using a public key that the third party provides.

Once again, we could have a distinct step that validates webhook payloads, but if every message is signed, we might as well build that logic into the trigger and give integration builders one less thing to worry about.

Amazon SNS famously signs its webhook messages (as do Quickbooks, and a variety of others). Every SNS notification message contains a Signature property which can be verified using a library like AWS's sns-validator. We can pass the payload we get into validator.validate(), and our trigger will throw an error if the payload didn't originate from SNS.

If a payload message is validated, it gets passed on to the integration:

import MessageValidator from "sns-validator";

export const SNSTrigger = trigger({
  // ...
  perform: async ({ logger }, payload, params) => {
    const validator = new MessageValidator();

    // Validate the incoming message
    const message = await new Promise((resolve, reject) => {
      validator.validate(
        util.types.toString(payload.rawBody.data),
        (error, message) => {
          if (error) {
            logger.error(
              `SNS Message could not be verified with error: ${error}`
            );
            return reject(error);
          }
          return resolve(message);
        }
      );
    });

    return {
      payload: { ...payload, body: { data: message } },
    };
  },
});

Assuming the message from SNS is validated, our trigger passes along a deserialized message object as the trigger's results.body.data for the rest of the integration to reference.

Manage Subscriptions

Finally, let's talk about managing webhook subscriptions with third-party apps and services. When setting up webhooks within some third-party systems, the third party sometimes expects you to verify that your webhook endpoint is ready and able to handle their requests. So, they send a special "confirmation" request to your webhook endpoint and expect you to do something with the payload you receive.

Looking back at the Amazon SNS example, SNS sends a JSON payload with a special key, Type. If Type is either "SubscriptionConfirmation" or "UnsubscribeConfirmation", SNS expects you to reference another value from it's payload - SubscribeURL - and call out to that URL. Once once SNS receives your request to the SubscribeURL, it knows that you're processing its notifications properly.

To accomplish this logic within the SNS trigger, we can write a simple switch statement, and perform an HTTP GET request if we get a subscription or unsubscribe confirmation message:

import { trigger, util } from "@prismatic-io/spectral";
import axios from "axios";
import MessageValidator from "sns-validator";

export const SNSTrigger = trigger({
  // ...

  perform: async ({ logger }, payload, params) => {
    const validator = new MessageValidator();

    const message = await new Promise((resolve, reject) => {}); // validate stuff...

    // Confirm subscription or unsubscribe as necessary
    switch (message["Type"]) {
      case "SubscriptionConfirmation":
      case "UnsubscribeConfirmation":
        await axios.get(message["SubscribeURL"]);
        break;
      case "Notification":
        break;
      default:
        throw new Error(
          `Message type was not "Notification", "SubscriptionConfirmation" or "UnsubscribeConfirmation", but "${message["Type"]}" instead.`
        );
    }

    return {
      // Return a deserialized message as payload.body.data
      payload: { ...payload, body: { data: message } },
    };
  },
});

If a message comes through asking an integration to confirm a webhook URL, the trigger takes care of that confirmation. Once again, the trigger handles a common task so it's one less thing for integration builders to worry about.

Concluding Remarks

The important take-aways here are that you can now write your own triggers as part of custom components, and you can encapsulate a lot of common tasks and logic into your trigger so that non-dev integration builders have less to worry about. This is yet another step we've taken to ensure that developers have a good experience writing custom components, and that non-developers are empowered to build and support integrations in the most straightforward way possible.

For a more detailed look at our Salesforce trigger, check out our quickstart. For full documentation on trigger development, including unit testing your triggers, I'll point you towards our docs. If you're new to Prismatic and would like a demo, please reach out on our contact form - we'd love to chat about integrations!


About Prismatic

Prismatic is the integration platform for B2B software companies. It's the quickest way to build integrations to the other apps your customers use and to add a native integration marketplace to your product. A complete embedded iPaaS solution that empowers your whole organization, Prismatic encompasses an intuitive integration designer, embedded integration marketplace, integration deployment and support, and a purpose-built cloud infrastructure. Prismatic was built in a way developers love and provides the tools to make it perfectly fit the way you build software.

Get the latest from Prismatic

Subscribe to receive updates, product news, blog posts, and more.