Incoming Webhooks Connector Documentation Recompute external properties and emit events with Javascript logic whenever user is send to connector by webhook


Hull Incoming Webhooks

The Incoming Webhooks connector enables you to capture incoming Webhooks from an external service and update user and account data in Hull by writing Javascript.

Getting Started

Go to the Connectors page of your Hull organization, click the button “Add Connector” and click “Install” on the card.

After installation, open the Code Editor, copy the displayed Webhook Endpoint and paste it in your external service and define how received payloads will be manipulated and sent to Hull.

After installation and configuring the URL to call in the Settings click the Code Editor button. You will be presented with the three column Dashboard layout.

  • The left column displays the Input which shows the data that was received
  • The middle column will hold your Javascript Code that transforms it to the Output of the right column. Write code jere that will be executed on each API call to manipulate users, accounts, and/or events. You can toggle between the current code that it is being developed and the code that was run at the time of the request. See below for tips on how to write your code.
  • The Output itself displays the attributes and events that will be applied to the User(s) and Account(s) targeted by the code.

To populate the input column, you need to have the service emit at least one call to Hull. Each API call, up to 100, will be stored and will be available to select as input.

Features

The Connector allows you to receive data from external systems, write Javascript to transform the received data to update users and accounts in Hull, as well as emit events in Hull to track behavioral data.

Available actions that can be coded: - create users/accounts - create user/account attributes - update user/account attributes - create user events - link users to accounts

To make the Connector even more powerful, you can use the request library https://github.com/request/request to call external services.

Code Basics

You can access the request data directly using the following variables:

Variable Name Description
body Contains the parsed data as a json object.
requestBody Object containing the body that was sent.
requestHeaders Object containing HTTP headers.
responseHeader Object containing HTTP headers.
url The url that was called.
method A string describing the HTTP method.
status The response status code
params Object containing the route parameters of the request.

Please note that the availability of these variables may be dependent on the external service.

Create or Update Users

Use the hull.asUser() function to reference an existing user or create a new one.

Function signature:

hull
  .asUser({external_id: <value>, email: <value>})

It is recommended to use the external_id if your payload contains it or rely on the email as fallback option. You can pass both identifiers if they are available, but the external_id will always take precedence over the email. For the purpose of simplicity, the following code will only show the external_id identifier.

Set top-level and ungrouped attributes:

hull
  .asUser({external_id: <value>})
  .traits({ ATTRIBUTE_NAME: <value> })

Set multiple attributes at once by passing in an object of attributes:

hull
  .asUser({external_id: <value>})
  .traits({ ATTRIBUTE_NAME: <value>, ATTRIBUTE2_NAME: <value> })

By default, non top-level attributes are stored in the traits attributes group. To group the attributes in a custom group, pass the group name as source in the second parameter:

hull
  .asUser({external_id: <value>})
  .traits({ ATTRIBUTE_NAME: <value> }, { source: <group_name> })

Or provide the group name per attribute:

hull
  .asUser({external_id: <value>})
  .traits({ GROUP_NAME/ATTRIBUTE_NAME: <value> })

To delete an attribute:

hull
  .asUser({external_id: <value>})
  .traits({ ATTRIBUTE_NAME: null })

Create or Update Accounts

Use the hull.asAccount function to reference an existing account or create a new one.

Function signature:

hull.asAccount({external_id: <value>, domain: <value>})

It is recommended to use the external_id if your payload contains it or rely on the domain as fallback option. You can pass both identifiers if they are available, but the external_id will always take precedence over the domain. For the purpose of simplicity, the following code will only show the external_id identifier.

Set top-level and ungrouped attributes:

hull
  .asAccount({external_id: <value>})
  .traits({ ATTRIBUTE_NAME: <value> })

Set multiple attributes at once by passing in an object of attributes:

hull
  .asAccount({external_id: <value>})
  .traits({ ATTRIBUTE_NAME: <value>, ATTRIBUTE2_NAME: <value> })

By default, non top-level attributes are stored in the traits attributes group. To group the attributes in a custom group, pass the group name as source in the second parameter:

hull
  .asAccount({external_id: <value>})
  .traits({ ATTRIBUTE_NAME: <value> }, { source: <group_name> })

Or provide the group name per attribute:

hull
  .asAccount({external_id: <value>})
  .traits({ GROUP_NAME/ATTRIBUTE_NAME: <value> })

To delete an attribute:

hull
  .asAccount({external_id: <value>})
  .traits({ ATTRIBUTE_NAME: null })

How to track events

Use the hull.asUser().track() function to emit events. Note that there is 10 track call limit per received API call.

Function signature:

hull
  .asUser({external_id: <value>})
  .track( "<event_name>" , {
      PROPERTY_NAME: <value>,
      PROPERTY2_NAME: <value>
    }, {
      ip: "0", //Or the source IP - if present, Event will be geolocated
      created_at: "created_at_timestamp", //Defaults to `now()`
      event_id: "unique_event_id", //To prevent duplication
      referer: "https://referrer.com", //null for Server calls
      source: "calendly", //a namespace such as "zendesk", "mailchimp", "stripe"...
      type: "meeting"
    }
  );

The first parameter is a string defining the name of the event while the second parameter is an object that defines the properties of the event.

Use the hull.asUser().account() function to link users to accounts. Provide the claims, domain, id and/or external_id of the account.

Function signature:

hull
  .asUser({external_id: <value>})
  .account({ domain: <value> })

How to alias / unalias identifiers

You can add or remove anonymous_ids to Users and Accounts with the following syntax:

hull
  .asUser({ email: "foo@bar.com" })
  .alias({ anonymous_id: "foobar:1234" });
hull
  .asUser({ email: "foo@bar.com" })
  .unalias ({ anonymous_id: "foobar:1234" });

hull
  .asAccount({ domain: "bar.com" })
  .alias({ anonymous_id: "foobar:1234" });
hull
  .asAccount({ domain: "bar.com" })
  .unalias ({ anonymous_id: "foobar:1234" });

External Libraries

The processor exposes several external libraries that can be used:

Variable Library name
_ The lodash library. (https://lodash.com/)
moment The Moment.js library(https://momentjs.com/)
urijs The URI.js library (https://github.com/medialize/URI.js/)
request (deprecated) The simplified request client (https://github.com/request/request)
superagent The simple and elegant request library (https://github.com/visionmedia/superagent)
uuid The uuid library (https://github.com/uuidjs/uuid)
LibPhoneNumber The google-LibPhoneNumber library (https://ruimarinho.github.io/google-libphonenumber/)

Please visit the linked pages for documentation and further information about these third party libraries.

uuid Library

The uuid library exposes the version 4 of the algorithm, and only accepts the first options argument - other arguments will be ignored. As a result, here’s the way to use it:

const user_id = uuid()
//or
const user_id = uuid({ random: [ 0x10, 0x91, 0x56, 0xbe, 0xc4, 0xfb, 0xc1, 0xea, 0x71, 0xb4, 0xef, 0xe1, 0x67, 0x1c, 0x58, 0x36, ] });

LibPhoneNumber Library

The LibPhoneNumber library exposes a subset of the google-libphonenumber library. Here’s how to use it

//PhoneNumberFormat is the PhoneNumberFormat object from the library;
//PhoneNumberUtil is an INSTANCE of the PhoneNumberUtil methods
const { CountrySourceCode, PhoneNumberType, PhoneNumberFormat, PhoneNumberUtil } = LibPhoneNumber;

const number = PhoneNumberUtil.parseAndKeepRawInput('202-456-1414', 'US');
console.log(number.getCountryCode()); //1
// Print the phone's national number.
console.log(number.getNationalNumber());
// => 2024561414

// Result from isPossibleNumber().
console.log(PhoneNumberUtil.isPossibleNumber(number));
// => true

Supported Methods for PhoneNumberUtil

Checkout i18n.phonenumbers.PhoneNumberUtil: https://ruimarinho.github.io/google-libphonenumber/#google-libphonenumber-methods-i18nphonenumbersphonenumberutil

Calling PhoneNumberUtil.parse("1234-1234") will return an instance of PhoneNumber, which has the following methods: https://ruimarinho.github.io/google-libphonenumber/#google-libphonenumber-methods-i18nphonenumbersphonenumber

Checkout the Docs for CountryCodeSource, PhoneNumberFormat, PhoneNumberType which are statics

[Deprecated] Using Request

The request library is now deprecated. Processors using the request library will be still operational, but we advise you to migrate to the super-agent request library which is much more intuitive and elegant to use.

If you are about to write new code to perform any API request, please refer to the Using Superagent section.

The library exposes request-promise to allow you to call external APIs seamlessly:

const response = await request({
    uri: 'https://api.github.com/user/repos',
    qs: {
        access_token: 'xxxxx xxxxx' // -> uri + '?access_token=xxxxx%20xxxxx'
    },
    headers: {
        'User-Agent': 'Request-Promise'
    },
    json: true // Automatically parses the JSON string in the response
})
console.log(response)

Using Superagent

To perform API requests, the processor connector exposes the superagent library through the superagent keyword. It is an instance of the original superagent library with additional plugins added behind the scenes to make it run smoothly in your processor code. This comes with some syntax restrictions that our instance of superagent won’t work with, more on that right below.

Differences

The exposed superagent instances cannot be called as function, so following code won’t work:

const res = await superagent('GET', 'https://www.foobar.com/search');

Instead always call a method on superagent object choosing which HTTP method you want to use. See examples Below.

Usage

Here are a few code snippets to use the super-agent request library in your processor code:

const response = await superagent
    .get("https://example.com/foo")
    .set("accept", "json")                    // Set a header variable by using the set() function.
    .set(`Authorization: Bearer ${api_key}`)
    .send({                                   // Set a body by using the send() function
      body_variable: "something"              // and by giving it an object.
    })
    .query({                                  // Set a query by using the query() function
      orderBy: "asc"                          // and by giving it an object.
    })

You can also perform asynchronous requests by using promises as such:

superagent
    .get("https://example.com/foo")
    .set("accept", "json")
    .set(`Authorization: Bearer ${api_key}`)
    .send({
      body_variable: "something"
    })
    .query({
      orderBy: "asc"
    })
    .then(res => {
      console.log(res.body);
    })

Handling errors is also possible, either by using promises or by wrapping the code in a try catch statement:

superagent
    .get("https://example.com/foo")
    .set("accept", "json")
    .set(`Authorization: Bearer ${api_key}`)
    .then(res => {
      console.log(res.body);
    })
    .catch(err => {
      console.log(`Error: ${err}`);
    })
try {
  const response = await superagent
    .get("https://example.com/foo")
    .set("accept", "json")
    .set(`Authorization: Bearer ${api_key}`);
} catch (err) {
  console.log(`Error: ${err}`);
}

You can find full documentation of the superagent library here. Keep in mind that calling superagent as function does not work.

Migrating from the Request library to the Superagent library

You might have noticed a warning message coming on your processor saying that your code is using a deprecated request library. In order to fix that, you need to replace request with the superagent library.

There are mostly two things to adjust. First you need to replace your request options object with set of chained methods on superagent instance. Second you will need to look for the response.body object instead of looking directly at the data object.

To illustrate that, let’s have a look at a code block using the deprecated request library, and another code block with the result of migrating it.

// Old request library

const reqOpts = {
  method: "GET",
  uri: "http://www.omdbapi.com/?t=James+Bond"
};

return new Promise((resolve, reject) => {
    request(reqOpts, (err, res, data) => {
      if (err) {
        console.info("Error:", err);
        return reject(err);
      }
      // data contains the response body
      if(_.isString(data)) {
        data = JSON.parse(data);
      }
      resolve(data);
    });
});
// With super-agent library

return superagent
    .get("http://www.omdbapi.com/?t=James+Bond")
    .then(res => {
      // res.body is parsed response body
      return res.body;
    })
    .catch(err => {
      console.info("Error:", err);
    })

Golden Rules

  • DO use snake_case rather than camelCase in your naming.
  • DO write human readable keys for traits. Don’t use names like ls for lead score, just name it lead_score.
  • DO use _at or _date as suffix to your trait name to let hull recognize the values as valid dates. You can pass either
    • a valid unix timestamp in seconds or milliseconds or
    • a valid string formatted according to ISO-8601
  • DO make sure that you use the proper type for new traits because this cannot be changed later. For example, if you pass "1234" as the value for trait customerId, the trait will be always a treated as string, even if you intended it to be a number.
  • DO NOT write code that generates dynamic keys for traits
  • DO NOT use large arrays because they are slowing down the compute performance of your data. Arrays with up to 50 values are okay.
  • DO NOT create infinite loops because they count towards the limits of your plan. Make sure to guard emitting events with track calls and to plan accordingly when setting a trait to the current timestamp.

Debugging and Logging

When operating you might want to log certain information so that it is available for debugging or auditing purposes while other data might be only of interest during development. The processor allows you to do both:

  • console.log is used for development purposes only and will display the result in the console of the user interface but doesn’t write into the operational logs.
  • console.info is used to display the result in the console of the user interface and does also write an operational log.

You can access the operational logs via the tab “Logs” in the user interface. The following list explains the various log messages available:

Message Description
compute.console.info The manually logged information via console.info.
compute.user.debug Logged when the computation of a user is started.

See how Hull works

Learn how Hull unifies and syncs customer data by watching our product tour