Custom activities guide

Activities describe user behavior like clicking on an email, making a purchase or logging in to your application. They help you understand what your customers are doing, segment customers into audiences and create detailed reports. Custom activities allow you to design your own activities that represent behaviors across the customer lifecycle.

Activities are used to enter people into playbooks and create target audiences, reports and dashboards.

This guide shows you how to add your own custom activities to Ortto via the API. Custom activities will help you find deep insights into your business via reporting and filtering, and have fine-grained control about how and when you communicate to your audiences.

At the end of this guide you will be sending custom activities from your own system into Ortto.

Getting started

The example activity for this guide is Logged in. We create an activity each time someone logs into our app.

In addition, we would like to know:

  • Which payment plan they are on.
  • The location they logged in.
  • If they are the account owner.
  • Which web browser they are using.

This information needs to be associated with the customer who logged in. We would also like to add a new person to Ortto if one does not exist to match this activity.

Generating a private API key

Learn more about this process in Configuring a custom API key.

NOTE: Do I need more than one data source?

Each Custom API data source has its own API Key associated with it. It is fine to use the same API key for sending many kinds of activities from within your application. However, if they are different systems, and you may wish to disconnect or regenerate the key separately, perhaps if one system was compromised in some way, then you can create more than one.

Each data source has its own statistics, logs, activities location graph and activities list associated with it, so if you would like to separate data on that basis you can create a separate Custom API data source for each.rere

See also

Creating a custom activity

The next step is to go to Activities and create a login activity. To do this navigate to the Activities screen and click New activity.

Activity name

This should be a verb such as PurchasedVisitedDownloaded or Logged in - it will be used in the activity feed for items such as:

Art Vandalay logged in.

You will be able to edit the activity name later, but it will not update previously recorded activities in the activity feed for a person. It will update it in the filters, reports and other places where it is displayed dynamically.


The icon is associated with your custom activity and will appear in the person activity feed and filters. While it isn’t necessary to have a unique icon per activity, it helps you identify them quickly.


Any attributes you want to send along with your activities must be defined here. If they are not, they will be ignored by the API.

When you name attributes you don’t have to give them programmatic names. These names will appear in filters and likely be used by non-technical people in your company. So you can call them things like Product name, or Size preference.

Once you create the activity the system will give them internal field names based on their name and type, e.g. str:cm:size-preference, which you will use when sending requests to the API.

You are required to define the type of each field, these include:



Code example


Any combination of letters and numbers below 255 characters.

{"str:cm:browser-type":"Microsoft Windows"}

Long text

Text longer than 255 characters

{"txt:cm:story":"It was a dark and stormy night. In her attic bedroom Margaret Murry, wrapped in an old patchwork quilt, sat on the foot of her bed and watched the trees tossing in the frenzied lashing of the wind. Behind the trees clouds scudded frantically across the sky. Every few moments the moon ripped through them, creating wraithlike shadows that raced along the ground."}


A whole number.


Decimal number

A decimal number.



A decimal number displayed as a currency.



A specific day, month and year.

{"dtz:cm:renewal-date":{"year": 2022,"month": 3,"day": 4,"timezone":"Australia/Sydney"}}

Time and date

A specific time, day, month and year.



True or false.


Phone number

A local or international phone number.



A web page or URL.


Single select

A single value selected from a list you define


Multi select

Multiple values selected from a list you define

{“sst:cm:multiple”:[“value 1”,”value 2”]}

JSON object

A JSON object with custom data

{"obj:cm:product-array":[{"item_id":"12345","item_name":"Laminate desk","price": 1000}]}

NOTE: Why are decimals and currencies sent as "int"?

In order to not lose precision in decimal operations internally, the Ortto API treats decimals as integers, multiplied by 1000.

For a decimal number, multiply the number by 1000. So for 64.28, you send 64280. For currency, multiply the number by 1000 and no $. So for $24.99, you send 24990.

NOTE: When a search is performed against activity attributes using the Has any value filter option, results containing 0 or "" will be included, and results set as null will be excluded. Learn more about empty values.

See also

WARNING: Do not add attribute for person fields like First name, Last name and Email.

Person fields can be sent with the payload and not need to add attributes for these. You can pass these into the create activity call along with the person to either automatically create people to go with the activity, or update existing people with the new data.

Map value to CDP

The map value to CDP option allows you to use attributes sent in your payload to update the fields on the person in question for that activity. For example, you might want to create custom fields on people for each of the attributes you are sending, then update them each time you send an activity payload. You can also update fields like First name and Last name in case the activity caused an internal update to this information. Mapping to fields is optional.

Conversion value

If you enable conversion value, you can provide an extra value in your payload like so "int::v": 15300. This value is then considered to have been attributed to this activity. Since the value is a currency, you multiply the number you are sending by 1000. In the above example the value being sent represents $15.30. So we would do this if we thought every login to our web app was worth $15.30.

Learn more about how Ortto handles attribution.

Track as touch

This option will update the "Last seen" on the person associated with the activity. A login event is the perfect example of when you would want to track the activity as touch because we know we have seen the user at the time they logged in.

Passive activities which were not initiated by the user should leave this option disabled.

Create an activity in Ortto (via API)

Requests to create an activity event in Ortto are submitted as a single POST method to the following URL:

NOTE: Ortto customers who have their instance region set to Australia or Europe will need to use specific service endpoints relative to the region:

  • Australia:
  • Europe:

For example:

All other Ortto users will use the default service endpoint (

For our login demo we will use the Private API key generated in previous steps.

Below is a basic example of sending a login activity to Ortto via the API using the cURL command. This sends through the attributes we specified above when creating the activity.


curl --location\ --request POST ''\ --header 'X-Api-Key: PUT-YOUR-REAL-KEY-HERE'\ --header 'Content-Type: application/json'\ --data-raw '{ "activities": [ { "activity_id":"act:cm:logged-in", "fields": { "str::first": "Chris", "str::last": "Smith", "str::email": "" }, "attributes": { "str:cm:payment-plan": "$100 starter plan", "bol:cm:account-owner": true, "str:cm:web-browser": "Google Chrome", "int::v": 1530 }, "location": { "source_ip": "" } } ]}'

Create activity response

If the request is successful you will receive a 200 OK response, and a JSON body like the following:


{ "activities": [ { "person_id": "00600e153df0a3b67ce60f00", "person_status": "created", "activity_id": "00600e153c548806f6a4dbda", "status": "ingested" } ] }

person_id is the ID of the person which the activity was added to. This could be a new person_id if the person was never seen before based on your merge strategy. You can also provide person_id in subsequent requests instead of email.

person_status tells you what happened to the person associated with this activity:




A field needed to identify or create the person — as specified in your data source merge criteria — was not provided. Or you provided a person field which does not exist or does exist but the wrong kind of data was provided.


A person was created for this activity as it did not previously exist.


A person already existed based on your data source merge criteria, and its fields were updated with any provided.


A person already existed and your data source merge strategy asked for these to be skipped, so the person was not updated based on the fields provided. The activity, however, was recorded against this person.


You specified a person_id in the request and the person was found and matched. The activity was applied to this person.

Ortto is strongly typed which means if you provide a type of data which cannot be unmarshaled into the correct type, your activity ingestion will fail.

Additionally, if you provide person fields which do not exist, activity ingestion will fail. Note that this doesn’t apply for unknown attributes on activities, which will simply be ignored.

Why won’t activities work from my website javascript?

You might notice that if you try to run these API calls via your own website you will get a CORS error. This is because the API is only accessible via a backend call, due to security reasons. If activities were allowed to be send from the public website, then the API could be bombarded with fake requests damaging your CDP data.

However, we do have a way for you to send these activities from your website.

Read the guide to sending website activities here.

Providing person fields as part of the payload

When you set up your data source, you specify your merge preferences. This is how the system decides what to do when it encounters a person it has seen before.

In order for an activity to create a new person if the person hasn’t been seen before, or apply the activity to an existing person, you need to provide person fields in your payload.

Our previous code example included the fields section, and that is what we are referring to here:


{ "fields": { "str::first": "Chris", "str::last": "Smith", "str::email": "" } }

If you provide unknown person fields, the activity ingestion will fail.

Custom activity request limits

It is possible to send up to 100 activities in the one payload to Ortto. The limits are as follows:

  • 100 activities max
  • 2 MB total payload size

In addition to the above, the number of activities per contact per day is limited. The limit is 50 activity events per activity per contact per 24 hours.

To illustrate, a request like the following, which has 1 activity event for 1 activity, can be sent for the same contact a maximum of 50 times per 24 hours.


curl --location\ --request POST ''\ --header 'X-Api-Key: PUT-YOUR-REAL-KEY-HERE'\ --header 'Content-Type: application/json'\ --data-raw '{ "activities": [ { "activity_id":"act:cm:logged-in", "fields": { "str::first": "Chris", "str::last": "Smith", "str::email": "" }, "attributes": { "str:cm:payment-plan": "$100 starter plan", "bol:cm:account-owner": true, "str:cm:web-browser": "Google Chrome", "int::v": 1530 }, "location": { "source_ip": "" } } ]}'

See also

Here is an example sending two login activities at once:


curl --location\ --request POST ''\ --header 'X-Api-Key: PUT-YOUR-REAL-KEY-HERE'\ --header 'Content-Type: application/json'\ --data-raw '{ "activities": [ { "activity_id":"act:cm:logged-in", "fields": { "str::first": "Alex", "str::last": "Smith", "str::email": "" }, "attributes": { "str:cm:payment-plan": "$1900 maxi plan", "bol:cm:account-owner": false, "str:cm:web-browser": "Edge" }, "location": { "source_ip": "" } }, { "activity_id":"act:cm:logged-in", "fields": { "str::first": "Chris", "str::last": "Smith", "str::email": "" }, "attributes": { "str::email": "", "str:cm:payment-plan": "$100 starter plan", "bol:cm:account-owner": true, "str:cm:web-browser": "Google Chrome" }, "location": { "custom": { "country_name": "USA", "region_name": "NY", "city_name": "New York", "position_name": "Vandalay Industries", "lat": 40.730610, "lng": -73.935242 } } } ] }'

A successful response will contain multiple results to let you know how each activity in the payload went:


{ "activities": [ { "person_id": "00600e15bbf0a3b67ce61200", "status": "ingested", "person_status": "created", "activity_id": "00600e15bb548806f6a4dfde" }, { "person_id": "00600e15bbf0a3b67ce61500", "status": "ingested", "person_status": "created", "activity_id": "00600e15bb548806f6a4e1e0" } ] }

Asynchronous activity ingestion

If you do not want to wait for a response from the API in order to ingest activities, you can provide the async: true flag in your request. You might want to do this if your code runs synchronously and the requests to Ortto delay your response to the user.

When the request contains the async: true flag, the system will queue ingestion of the activities and respond to you immediately.


curl --location\ --request POST ''\ --header 'X-Api-Key: PUT-YOUR-REAL-KEY-HERE'\ --header 'Content-Type: application/json'\ --data-raw '{ "async": true, "activities": [ { "activity_id":"act:cm:logged-in", "fields": { "str::first": "Chris", "str::last": "Smith", "str::email": "" }, "attributes": { "str:cm:payment-plan": "$100 starter plan", "bol:cm:account-owner": true, "str:cm:web-browser": "Google Chrome" }, "location": { "source_ip": "" } } ] }'

If the request is successful you will get a response like the following:


{ "activities": [ { "status":"queued", "person_status":"queued" } ] }

Note the "status": "queued" in the response.

NOTE: When sending activity events for activities that are older than 90 days (where data retention is enabled), the activity ingestion will be buffered, even if "async": false.

Geo locating activities

You can optionally provide a location with each activity you send Ortto. In fact, if you do not know the location we can even look it up for you based on IP address. Read about geo locating activities here.

Back-dating activities

If you would like to back-date activity data, you can use the created field in your request to specify the date on which the activity occurred. Ortto allows you to send in backdated events up to 90 days in the past, or if you have data retention enabled on your activity, you may provide backdated events up to your selected data retention period.

In addition, to make sure you do not backfill the same piece of data more than once, you can include a key attribute, which gets combined with the created date to create a unique identifier for each activity. If you provide the same pair for key and created multiple times (such as if you need to run your backfill scripts multiple times), it will merge those requests so you do not get duplicate activities created.


curl --location\ --request POST ''\ --header 'X-Api-Key: PUT-YOUR-REAL-KEY-HERE'\ --header 'Content-Type: application/json'\ --data-raw '{ "activities": [ { "activity_id":"act:cm:logged-in", "created": "2020-02-17T01:30:17.601Z", "key": "125432", "fields": { "str::first": "Chris", "str::last": "Smith", "str::email": "" }, "attributes": { "str:cm:payment-plan": "Free plan", "bol:cm:account-owner": true, "str:cm:web-browser": "Netscape Navigator" }, "location": { "source_ip": "" } } ] }'

As this documentation will age, if you copy the above example verbatim you might get a response like below:


{ "request_id": "19ccd5c1-112d-4dfa-bf80-0a3312896742", "code": 400, "error":" Created date is more than 90 days ago" }

If you provide a created date within the last 90 days (or within your activity’s selected data retention period), you will receive a successful response:


{ "activities": [ { "person_id":"00602c446af92b7aa1f77a00", "status":"ingested", "person_status":"created", "activity_id":"00602c71a9b346c32c9b7473" } ] }

If you provide a created date outside of the activity’s selected data retention period, you will receive an error response:


{ "request_id": "19ccd5c1-112d-4dfa-bf80-0a3312896742", "code": 400, "error":" Created date is outside of activity cold storage" }

NOTE: Activities that are more than 90 days old and within the activity’s selected data retention period will be added to a queue for processing which may take up to an hour.

To read more about Ortto’s data retention policy, see the link below:

See also

Email subscription permission

All people added via the API are considered to be "opted in" for email subscription permission. They will appear with "Subscribed via API" on their profile. It is very important that you do not add people via the API if you do not have permission to email them.

If you would like to control either their subscription status, such as setting them as "opted out", or setting their subscribed or unsubscribed reason, you can do this by providing the following fields:


{ "bol::p": true, "str::s-ctx": "Subscribed via internal API" }

Above is an example of email subscription permission being enabled with the message "Subscribed via internal API" Here is an example of someone who starts off unsubscribed:


{ "bol::p": false, "str::u-ctx": "Unsubscribed via internal API" }

The default, if you do not provide these fields is:


{ "bol::p": true, "str::s-ctx": "Subscribed via API" }

Here is a full cURL request example for how to do this:


curl --location\ --request POST ''\ --header 'X-Api-Key: PUT-YOUR-REAL-KEY-HERE'\ --header 'Content-Type: application/json'\ --data-raw '{ "activities":[ { "activity_id":"act:cm:logged-in", "fields": { "str::first": "Chris", "str::last": "Smith", "str::email": "", "bol::p": false, "str::u-ctx": "Unsubscribed via internal api" }, "attributes": { "str:cm:payment-plan": "Free plan", "bol:cm:account-owner": true, "str:cm:web-browser": "Netscape Navigator" } } ] }'

Displaying custom activities

When you send custom activities into Ortto, you can display them in custom formats within Ortto using attributes from the activities.

Here is an example of an activity using custom code to display:

Order number {{attribute.number}} placed for item {{attribute.item_name}}

The syntax of this is slightly different to the Liquid syntax used in campaigns. All attributes from the custom activity are available and are put as lowercase and spaces are replaced with underscores.