# Building an app

To successfully integrate with Virtuoso, a custom app needs to implement at least one of the following optional mechanisms:

  • a webhook endpoint, which will be used by Virtuoso to notify the app of an organization requesting to install or uninstall it, or the occurrence of one of the events that the app is subscribed to;
  • a configuration endpoint, through which an organization with an active installation of the app can configure its particular settings.
  • an interactive endpoint, which will be used by Virtuoso to notify the app of an action on one of the interactive menu items (see below) defined by the app.

To build an app:

  1. Click on the organization management icon on the left side of the Dashboard;
  2. Select the Apps tab;
  3. Click on the New app button at the top of the page and you'll be taken to an empty app editor page.

# App fields

This page is divided into four main areas which, together, contain all the fields needed to set up an app.

App edition page

App details
  • Name: the name which will identify the app (it must be unique).
  • Icon (optional): an image which can help users identify the app.
  • Enabled: if toggled on, it will activate the app within Virtuoso, i.e., Virtuoso will start listening to requests from the app and send requests to it, based on the list of subscribed events and other app configurations. See some extra details on enabling / disabling an app below.
  • Description (optional): a brief explanation of what the app does.
  • Webhook URL (optional): the app URL to which at least installation and uninstallation requests will be sent. Virtuoso will also send to this URL events specified in the Event subscription area, and actions requests, in case you don't provide a Request URL (see Interactive menus below).
  • Configuration (optional): define the mechanism that will be used to configure the app installation. Two options are available:
    • Form: shows a configuration form directly in the details page of the app installation (see Configuring an app installation). You can find instructions on how to build the form in the UI augmentation section.
    • URL: uses an external page from where you can configure the app installation.
Security

The fields in this area will be filled after building the app.

  • Client key (read only): a unique identifier which can be used by the app to authenticate itself within Virtuoso. See security considerations below.
  • Client secret (read only): a unique secret which can be used by the app to authenticate itself within Virtuoso. See security considerations below.
  • Signing secret (read only): a unique secret which can be used by the app to validate if the received requests originated in Virtuoso and were not tampered with. See security considerations below.
Event subscription
  • Event subscription enabled: if toggled on, it will allow the app to subscribe to virtuoso events, being notified of when they happen.
  • Subscribed events (optional): a list of events available for subscription. See the list of available virtuoso events below.
Interactive menus
  • Interactive menus enabled: when toggled on, the defined app actions will become visible in their respective Virtuoso menus.
  • Request URL (optional): URL used in the POST request that is made when you perform an action (e.g. a click) on an interactive menu item. When enabling interactive menus, you need to provide a valid request URL or webhook URL. Virtuoso sends the request to the latter when you don't provide the request URL.
  • Actions (optional): menu items composed by a name, a description, a Virtuoso menu, and a callback ID.
    • Virtuoso adds the actions in the specified menus, showing the app icon followed by the action's name, and displays the action's description in a tooltip when you hover the added menu item.
    • When creating an action, you can also build a form to collect additional data from the user before Virtuoso sends the POST request (see UI augmentation on how to build one). The payload will depend on the menu where the action is shown, and the callback ID is sent alongside with the payload to help apps identify the triggered action.
    • If the action contains a form, its context will also depend on the menu where the action is shown, and the payload will contain an additional payload property with the values of the submitted form.
    • See the list of available target menus and the payload format for each of them below.

App action modal

While editing an app, you can test the Webhook URL and the Request URL by clicking on the icon near the field . This will trigger a test request to the app, which might be useful also to test the signature verification. See Verifying the event notification request below.

# Build confirmation

After filling the fields needed for the app, click on the Add app button. You'll be redirected to the apps tab in the organization management page, and a confirmation message will be shown.

App build confirmation

This message will also feature the aforementioned security fields, client key, client secret, and signing secret. See the security considerations below.

The newly built app should now appear in the Recommended list and be available for installation. To change the set-up of any custom app, open its context menu and click on Edit.

# Enabling / disabling an app

If you do not want to receive events from Virtuoso, you can disable your organization's custom apps.

You may do this either through the app's context menu or the edition page, by turning off the Enabled switch at the top of the page and then clicking the Update button.

Disabling an app

Disabling an app will affect all its installations. If your app has been made public, be aware that installations by other organizations will also stop working.

# Security considerations

The client key and client secret pair is used to identify and authenticate the app within Virtuoso, while the signing secret is used to validate if the events originated in Virtuoso and weren't tampered with.

For security reasons, the client secret and the signing secret are only shown to you at the time of their creation. You should copy the values and store them in a secure location so that your custom application can access and use them when needed. You can, however, generate a new client secret or signing secret at any time through the edition page, invalidating the previous ones for that app.

App client key / secret

These credentials can be used to trigger actions in Virtuoso. Please, keep them secure and be careful with whom you share them with.

Note that we will never require you to send the client secret in a request. This is used only locally to sign each of the messages that you send.

# Talking with Virtuoso

A common information flow for an app would be described by the following diagram:

App flow

  1. Upon first installation, Virtuoso will send an installation notification event to the app when an organization installs it, containing the installationId. Important: for Virtuoso to consider the installation successful, the app must respond with a 2XX HTTP status code.
  2. After an app is installed, when any of the events that the app has subscribed to is triggered, Virtuoso will send the event to app's webhook. Each event notification request is structured as shown below.
  3. If upon receiving the event, you need more information from Virtuoso, the app can use the Virtuoso API to retrieve it. Virtuoso offers a simple and secure way for apps to obtain authentication tokens on the fly.

# App installation notification event

When the app has the Webhook URL defined, it will automatically receive install and uninstall app events. It must send a 2XX response to the app install event in order for the action to be accepted by Virtuoso.

Example of an installation notification event request
curl --location --request POST '<your-webhook-url>' \
--header 'Content-Type: application/json' \
--header 'X-Virtuoso-Request-Timestamp: 1590064214' \
--header 'X-Virtuoso-Signature: f206dfcf0cd63d23b4a287d9b85a5ed4cc39b2fb85b87469f1d93673cc6eff66' \
--data-raw '{
	"installationId": "bd411a74-99ff-4b3e-b5f5-97b0d6b55d8c",
	"user": {
		"userId": 1,
		"organizationId": 1,
		"username": "admin",
		"email": "[email protected]",
		"name": "admin"
	},
	"event":  {
		"kind": "INTEGRATION",
		"eventName": "integration:install",
		"eventCategory": "integration",
		"organizationId": 1,
		"userId": 1,
		"installationId": "bd411a74-99ff-4b3e-b5f5-97b0d6b55d8c",
		"eventType":"INSTALL"
	}
}'

# Virtuoso event notification request

Every event notification sent by Virtuoso will provide the necessary information to allow the app to react accordingly.

An event notification payload contains the following fields:

  • installationId: The unique app installation identifier associated with the organization;
  • event: An object representing the event (for more information see Available virtuoso events).
  • user (optional): An object containing information like the username, name, and email;
Example of an event notification request - plan execution finished
curl --location --request POST '<your-webhook-url>' \
--header 'Content-Type: application/json' \
--header 'X-Virtuoso-Request-Timestamp: 1590064214' \
--header 'X-Virtuoso-Signature: f206dfcf0cd63d23b4a287d9b85a5ed4cc39b2fb85b87469f1d93673cc6eff66' \
--data-raw '{
	"installationId": "bd411a74-99ff-4b3e-b5f5-97b0d6b55d8c",
	"event":  {
        "kind": "PLAN_EXECUTION",
        "eventName": "execution_plan:execution_finished",
        "eventCategory": "execution_plan",
		"organizationId": 1,
		"userId": 1,
		"projectId": 1,
		"planId": 1,
		"eventType":"EXECUTION_FINISHED"
	}
	"user": {
		"userId": 1,
		"organizationId": 1,
		"username": "member",
		"email": "[email protected]",
		"name": "SpotQA Member"
	},
}'

# Verifying the event notification request

Virtuoso uses the signing secret to create a signature that is sent as header (X-Virtuoso-Signature) in every event notification request that Virtuoso makes. By comparing the signature received with the signature that your app computes you can ensure that the request originated in Virtuoso, who is the only entity who has access to the signing secret besides the app.

The signature is computed from the $timestamp:$payload string and signed using the app signing secret. The code examples below show you how to verify it.

Java
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class Signer {

  private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";

  public static String sign(String textToSign, String secret)
      throws NoSuchAlgorithmException, InvalidKeyException {

    var decodedSecret = new SecretKeySpec(Base64.getDecoder().decode(secret),
        HMAC_SHA256_ALGORITHM);
    Mac mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
    mac.init(decodedSecret);

    StringBuilder sb = new StringBuilder();
    for (byte b : mac.doFinal(textToSign.getBytes())) {
      sb.append(String.format("%02x", b));
    }

    return sb.toString();
  }

  public static boolean verify(String timeInMillis, String payload, String secret,
      String receivedSignature) throws InvalidKeyException, NoSuchAlgorithmException {

    String textToSign = String.format("%s:%s", timeInMillis, payload);
    String generatedSignature = Signer.sign(textToSign, secret);

    return generatedSignature.equals(receivedSignature);
  }

  public static void main(String[] args) throws InvalidKeyException, NoSuchAlgorithmException {
    final String SIGNING_SECRET = "K2eWUZiDp+EREJNLFrk6vFIB6augrXnPx+XOvq+X6uw=";

    // Content of the "X-Virtuoso-Request-Timestamp" header
    String timeInMillis = String.valueOf(System.currentTimeMillis());

    // Request payload
    var payload = "A json containing the event data";

    String textToSign = String.format("%s:%s", timeInMillis, payload);

    // Content of the "X-Virtuoso-Signature" header
    String receivedSignature = Signer.sign(textToSign, SIGNING_SECRET);

    if (Signer.verify(timeInMillis, payload, SIGNING_SECRET, receivedSignature)) {
      System.out.println("Request is valid");
    } else {
      System.out.println("Request is invalid");
    }
  }
}
Javascript
const crypto = require('crypto');

function sign(textToSign, secret) {
  const signature = crypto.createHmac('SHA256', Buffer.from(secret, 'base64'))
    .update(textToSign)
    .digest('hex');

  return signature
}

function verify(timeInMillis, payload, secret, receivedSignature) {
  const textToSign = `${timeInMillis}:${payload}`
  const generatedSignature = sign(textToSign, secret);

  return generatedSignature === receivedSignature
}

const SIGNING_SECRET = 'K2eWUZiDp+EREJNLFrk6vFIB6augrXnPx+XOvq+X6uw=';

// Content of the "X-Virtuoso-Request-Timestamp" header
const timeInMillis = Date.now();

// Request payload
const payload = 'A json containing the event data';

const textToSign = `${timeInMillis}:${payload}`;

// Content of the "X-Virtuoso-Signature" header
const receivedSignature = sign(textToSign, SIGNING_SECRET);

if (verify(timeInMillis, payload, SIGNING_SECRET, receivedSignature)) {
  console.log('Request is valid');
} else {
  console.log('Request is invalid');
}

# Authenticating an app in Virtuoso

To access the context of the organization where the app is installed (e.g., to read data or perform actions), you need to authenticate first.

The request payload must contain the following information:

  • installationId (UUID): You receive this in every event (including the installation event)
  • clientKey (String): This is provided to you in the app configuration page.
  • timeInMillis (Long): Current epoch in millis.
  • signature (string value of HMacSha256): See the section below on how this is computed.
Example authentication request (payload and response)
curl --location --request POST 'https://api.virtuoso.qa/api/auth/login/integration?envelope=false' \
--header 'Content-Type: application/json' \
--data-raw '{
    "installationId": "<installation identifier>",
    "clientKey": "<app client key>",
    "timeInMillis": <time in millis used in the signature>,
    "signature": "<signed string>"
}'

Response:
{
    "success": true,
    "item": {
        "token": "5ca64217-9f05-49e9-8b03-b77c8d58c2f4",
        "expires": "2020-04-07 15:15:58.554"
    }
}

# Generating the authentication signature

The authentication signature is computed from the following string: $installationId:$timeMillis:$clientKey signed using the app client secret.

Note that the client key is a binary value that is base64 encoded, so you may have to decode this before passing it to your signature function; but fear not, we have code examples below, in different programming languages, to show you how.

Java

See Verifying the event notification request for the definition of the Signer class.

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

public class Authenticator {

  public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException {
    final String INSTALLATION_ID = "32ad2b70-a520-4765-98ff-764d482aaf6e";
    final String CLIENT_KEY = "ApZuVA9s1TS27Ngy5vrE1Fa7jtyBwV40";
    final String CLIENT_SECRET = "F/eromwqZl3b4P1imIo8uNi9MBMiVAnsCSScI0oFHrY=";

    Long timeInMillis = System.currentTimeMillis();
    String textToSign = String.format("%s:%s:%s", INSTALLATION_ID, timeInMillis, CLIENT_KEY);

    // Signer is the same class used in the example for "Verifying the event notification request"
    String signature = Signer.sign(textToSign, CLIENT_SECRET);

    System.out.println("signature value is: " + signature);
  }
}
Javascript

See Verifying the event notification request for the definition of the sign function.

const INSTALLATION_ID = "32ad2b70-a520-4765-98ff-764d482aaf6e";
const CLIENT_KEY = "ApZuVA9s1TS27Ngy5vrE1Fa7jtyBwV40";
const CLIENT_SECRET = "F/eromwqZl3b4P1imIo8uNi9MBMiVAnsCSScI0oFHrY=";

const timeInMillis = Date.now();
const textToSign = `${INSTALLATION_ID}:${timeInMillis}:${CLIENT_KEY}`

// sign is the same function used in the example for "Verifying the event notification request"
console.log(`signature value is:: ${sign(textToSign, CLIENT_SECRET)}`)
Golang
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
    "encoding/base64"
	"fmt"
	"time"
)

func main() {
	installationId := "32ad2b70-a520-4765-98ff-764d482aaf6e"
	timeInMillis := time.Now().Unix() * 1000
	clientKey := "ApZuVA9s1TS27Ngy5vrE1Fa7jtyBwV40"
	secretKey := "F/eromwqZl3b4P1imIo8uNi9MBMiVAnsCSScI0oFHrY="

    decodedSecret, _ := base64.StdEncoding.DecodeString(secretKey)
	signature := hmac.New(sha256.New, decodedSecret)
	signature.Write([]byte(fmt.Sprintf("%s:%d:%s", installationId, timeInMillis, clientKey)))

	stringValue := hex.EncodeToString(signature.Sum(nil))
	fmt.Println("signature value is: " + stringValue)
}
Python
import time
import base64
import hmac
import hashlib

def HmacSha256(secret, content) : 
   return hmac.new(base64.b64decode(str.encode(secret)),content.encode(), hashlib.sha256)      .hexdigest()

installationId = "32ad2b70-a520-4765-98ff-764d482aaf6e"
timeInMillis = int(round(time.time() * 1000))
clientKey = "ApZuVA9s1TS27Ngy5vrE1Fa7jtyBwV40"
secretKey = "F/eromwqZl3b4P1imIo8uNi9MBMiVAnsCSScI0oFHrY="

signature = HmacSha256(secretKey, "%s:%s:%s" % (installationId, timeInMillis, clientKey))   

print ("signature value is: " + signature)
Ruby
require 'openssl'
require 'base64'

installationId = "32ad2b70-a520-4765-98ff-764d482aaf6e"
timeInMillis = 	(Time.now.to_f * 1000).floor.to_s
clientKey = "ApZuVA9s1TS27Ngy5vrE1Fa7jtyBwV40"
secretKey = "F/eromwqZl3b4P1imIo8uNi9MBMiVAnsCSScI0oFHrY="

signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), Base64.decode64(secretKey), installationId + ":" + timeInMillis.to_s + ":" + clientKey)

puts("signature value is: " + signature)
Shell / Bash
installationId="32ad2b70-a520-4765-98ff-764d482aaf6e"
timeInMillis=$(echo $(($(date +%s%N)/1000000)))
clientKey="ApZuVA9s1TS27Ngy5vrE1Fa7jtyBwV40"
secretKey="F/eromwqZl3b4P1imIo8uNi9MBMiVAnsCSScI0oFHrY="

hexSecretKey=$(echo $secretKey | base64 -d -i | hexdump -v -e '/1 "%02x" ' )
signature=$(echo -n $installationId:$timeInMillis:$clientKey | openssl dgst -sha256 -mac HMAC -macopt hexkey:$hexSecretKey )

echo "signature value is: $signature"
Google Script (e.g., Google Sheets)
function loginAndGetToken(installationId){
  
  var clientKey = "APP_CLIENT_KEY"
  var clientSecretBinary = "APP_CLIENT_SECRET"
  
  var timeMillis = Number(new Date().getTime()).toFixed(0)
  var signatureStringBinary = Utilities.base64Decode(Utilities.base64Encode(`${installationId}:${timeMillis}:${clientKey}`))
  
  var signature = Utilities.computeHmacSha256Signature(signatureStringBinary, Utilities.base64Decode(clientSecretBinary)).reduce(function(str,chr){
    chr = (chr < 0 ? chr + 256 : chr).toString(16);
    return str + (chr.length==1?'0':'') + chr;
  },'')
    
  var payload = {
    "installationId": installationId,
    "clientKey": clientKey,
    "timeInMillis": timeMillis,
    "signature": signature
  }
  
  var res = UrlFetchApp.fetch(
    `https://api.virtuoso.qa/api/auth/login/integration?envelope=false`,
    {
      method             : 'post',
      contentType        : 'application/json',
      payload            : JSON.stringify(payload),
      muteHttpExceptions : true,
    })
  
  Logger.log(`Virtuoso App Login status: ${res.getResponseCode()}`)
  
  return JSON.parse(res.getContentText()).token
}

# Available virtuoso events

Virtuoso has more than 100 event types available for subscription (e.g., project:created, goal:updated, execution:finished, etc.). When a subscribed event occurs, a request is sent to the Webhook URL with a payload which identifies the type of event and all the information associated with it. The list of all Virtuoso events can be fetched using Virtuoso API /api/integrations/events with your preferred language or using one of the following examples:

Javascript
(await fetch('https://api.virtuoso.qa/api/integrations/events?envelope=false')).json()
curl
curl -s GET https://api.virtuoso.qa/api/integrations/events?envelope=false

# Available target menus for app actions

App action events take, as payload, an extended version of the fields listed above:

  • installationId: The unique app installation identifier associated with the organization;
  • user: An object containing information about the user performing the app action;
  • event: An object representing the app action event. Among other data, it contains some properties that are useful to identify which app action it refers to:
    • callbackId: The user defined callback id set for this action at its configuration;
    • eventType: The resource type the action is associated with;
    • stage: When actions make use of dynamic forms, this identifies at what stage of the form the action event was sent. It may be any of the following:
      • ÌNIT: Sent when the action form is open. The app may respond with an object containing field names and, for each, property values to be applied to the form. Only properties marked as updatable for each field will be taken into account;
      • UPDATE: Sent when a field triggers other's updates or when certain fields require more information from the app, such as during a search operation. The payload will contain more information on what type of update is needed. The app may respond accordingly to update field properties;
      • SUBMIT: The final stage of the form. The payload will contain the final values of the form's fields;
  • configuration: The app configuration values, set after the app is installed in each organization;
  • payload: Contains form values and extra details to identify the action event during the UPDATE stage:
    • values: The form values at the point where the action event was sent;
    • action: Action details during the UPDATE stage of the form:
      • type: The subtype of update being required. It may be either SEARCH or UPDATE;
      • fieldName: The name of the field that triggered the update action event;
      • update: The list of field names that may be updated as a response to this event. Only present if type = UPDATE;
      • query: The search term to use. Only present if type = SEARCH. This is directly associated with the search field;

The following list contains all Virtuoso menus that can hold app actions. When you perform an action, a request is sent to the Request URL with a payload that identifies the action and contain information associated with it.

Checkpoint
{
  "installationId": "c9cfe1b1-a71e-4597-adc2-5011d0a15172",
  "user": {
    "userId": 1,
    "username": "admin",
    "email": "[email protected]",
    "name": "Admin",
    "avatar": "",
    "active": true
  },
  "event": {
    "kind": "CHECKPOINT_INTEGRATION_ACTION",
    "timestamp": 1593191530755,
    "callbackId": "callbackId",
    "checkpoints": [
      {
        "id": 5632,
        "snapshotId": 12268,
        "goalId": 1,
        "name": "TC1",
        "title": "Go to Dynamic Graph",
        "archived": false,
        "clonedFrom": 5625,
        "canonicalId": "be92094f-c313-4913-a618-f9e4dfa9c20c"
      }
    ],
    "eventType": "CHECKPOINT",
    "eventCategory": "action",
    "eventName": "action:checkpoint",
    "stage": "SUBMIT"
  },
  "configuration": {
    "Configuration field name": "https://my.config.url"
  },
  "payload": {
    "values": {
      "a name": "User text"
    }
  }
}
Execution
{
  "installationId": "c9cfe1b1-a71e-4597-adc2-5011d0a15172",
  "user": {
    "userId": 1,
    "username": "admin",
    "email": "[email protected]",
    "name": "Admin",
    "avatar": "",
    "active": true
  },
  "event": {
    "kind": "EXECUTION_INTEGRATION_ACTION",
    "timestamp": 1593196341124,
    "callbackId": "callbackId",
    "job": {
      "id": 7870,
      "snapshotId": 12236,
      "goalId": 2,
      "status": 3,
      "submitDate": 1592325096157,
      "startDate": 1592325096199,
      "endDate": 1592325108657,
      "launchedBy": 254,
      "triggerType": 1,
      "type": "EXECUTION",
      "statisticsCalculated": true
    },
    "journeyIds": [],
    "eventType": "EXECUTION",
    "eventCategory": "action",
    "eventName": "action:execution",
    "stage":  "SUBMIT"
  },
  "configuration": {
    "Configuration field name": "https://my.config.url"
  },
  "payload": {
    "values": {
      "a name": "User text"
    }
  }
}
Goal item
{
  "installationId": "c9cfe1b1-a71e-4597-adc2-5011d0a15172",
  "user": {
    "userId": 1,
    "username": "admin",
    "email": "[email protected]",
    "name": "Admin",
    "avatar": "",
    "active": true
  },
  "event": {
    "kind": "GOAL_INTEGRATION_ACTION",
    "timestamp": 1593191324659,
    "callbackId": "callbackId",
    "project": {
      "id": 1,
      "name": "Test Project",
      "archived": false
    },
    "goals": [
      {
        "id": 1,
        "projectId": 1,
        "name": "Desktop Functional",
        "archived": false
      }
    ],
    "eventType": "GOAL",
    "eventCategory": "action",
    "eventName": "action:goal",
    "stage":  "SUBMIT"
  },
  "configuration": {
    "Configuration field name": "https://my.config.url"
  }
}
Goals
{
  "installationId": "c9cfe1b1-a71e-4597-adc2-5011d0a15172",
  "user": {
    "userId": 1,
    "username": "admin",
    "email": "[email protected]",
    "name": "Admin",
    "avatar": "",
    "active": true
  },
  "event": {
    "kind": "GOAL_INTEGRATION_ACTION",
    "timestamp": 1593191736477,
    "callbackId": "callbackId",
    "project": {
      "id": 1,
      "name": "Test Project",
      "archived": false
    },
    "goals": [],
    "eventType": "GOAL",
    "eventCategory": "action",
    "eventName": "action:goal",
    "stage":  "SUBMIT"
  },
  "configuration": {
    "Configuration field name": "https://my.config.url"
  },
  "payload": {
    "values": {
      "a name": "User text"
    }
  }
}
Extension item
{
  "installationId": "c9cfe1b1-a71e-4597-adc2-5011d0a15172",
  "user": {
    "userId": 1,
    "username": "admin",
    "email": "[email protected]",
    "name": "Admin",
    "avatar": "",
    "active": true
  },
  "event": {
    "kind": "EXTENSION_INTEGRATION_ACTION",
    "timestamp": 1593196167117,
    "callbackId": "callbackId",
    "project": {
      "id": 1,
      "name": "Test Project",
      "archived": false
    },
    "extensions": [
      {
        "id": 414,
        "name": "org script",
        "script": "TextDecoderStream",
        "inputs": [],
        "modifiedDate": 1580987833272,
        "async": false,
        "directive": false,
        "resources": []
      }
    ],
    "eventType": "EXTENSION",
    "eventCategory": "action",
    "eventName": "action:extension",
    "stage":  "SUBMIT"
  },
  "configuration": {
    "Configuration field name": "https://my.config.url"
  },
  "payload": {
    "values": {
      "a name": "User text"
    }
  }
}
Extensions
{
  "installationId": "c9cfe1b1-a71e-4597-adc2-5011d0a15172",
  "user": {
    "userId": 1,
    "username": "admin",
    "email": "[email protected]",
    "name": "Admin",
    "avatar": "",
    "active": true
  },
  "event": {
    "kind": "EXTENSION_INTEGRATION_ACTION",
    "timestamp": 1593196175795,
    "callbackId": "callbackId",
    "project": {
      "id": 1,
      "name": "Test Project",
      "archived": false
    },
    "extensions": [],
    "eventType": "EXTENSION",
    "eventCategory": "action",
    "eventName": "action:extension",
    "stage":  "SUBMIT"
  },
  "configuration": {
    "Configuration field name": "https://my.config.url"
  },
  "payload": {
    "values": {
      "a name": "User text"
    }
  }
}
Journey
{
  "installationId": "c9cfe1b1-a71e-4597-adc2-5011d0a15172",
  "user": {
    "userId": 1,
    "username": "admin",
    "email": "[email protected]",
    "name": "Admin",
    "avatar": "",
    "active": true
  },
  "event": {
    "kind": "JOURNEY_INTEGRATION_ACTION",
    "timestamp": 1593191523783,
    "callbackId": "callbackId",
    "journey": {
      "id": 8010,
      "snapshotId": 12268,
      "goalId": 1,
      "name": "Suite 1",
      "title": "Dynamic Graph 0-2",
      "archived": false,
      "canonicalId": "552d004b-d0b1-46c3-ab1a-10eafad8b192",
      "tags": []
    },
    "eventType": "JOURNEY",
    "eventCategory": "action",
    "eventName": "action:journey",
    "stage":  "SUBMIT"
  },
  "configuration": {
    "Configuration field name": "https://my.config.url"
  },
  "payload": {
    "values": {
      "a name": "User text"
    }
  }
}
Journey execution
{
  "installationId": "c9cfe1b1-a71e-4597-adc2-5011d0a15172",
  "user": {
    "userId": 1,
    "username": "admin",
    "email": "[email protected]",
    "name": "Admin",
    "avatar": "",
    "active": true
  },
  "action": {
    "interactionConfig": {
      "id": 2817,
      "name": "Journey execution action",
      "type": "JOURNEY_EXECUTION",
      "callbackId": "callbackId"
    },
    "journeyId": 7971,
    "executionId": 7804,
    "kind": "JOURNEY_EXECUTION"
  }
}
Plan item
{
  "installationId": "c9cfe1b1-a71e-4597-adc2-5011d0a15172",
  "user": {
    "userId": 1,
    "username": "admin",
    "email": "[email protected]",
    "name": "Admin",
    "avatar": "",
    "active": true
  },
  "event": {
    "kind": "PLAN_INTEGRATION_ACTION",
    "timestamp": 1593196278131,
    "callbackId": "callbackId",
    "project": {
      "id": 1,
      "name": "Test Project",
      "archived": false
    },
    "plans": [
      {
        "id": 265,
        "goalId": 1,
        "snapshotId": 4,
        "name": "Simple plan",
        "createdDate": 1571133589021,
        "modifiedBy": 254,
        "modifiedDate": 1591031942899,
        "archived": false,
        "dataDriven": false,
        "uuid": "1eb96220-8770-4530-ac17-10786f453882"
      }
    ],
    "eventType": "PLAN",
    "eventCategory": "action",
    "eventName": "action:plan",
    "stage":  "SUBMIT"
  },
  "configuration": {
    "Configuration field name": "https://my.config.url"
  },
  "payload": {
    "values": {
      "a name": "User text"
    }
  }
}
Plans
{
  "installationId": "c9cfe1b1-a71e-4597-adc2-5011d0a15172",
  "user": {
    "userId": 1,
    "username": "admin",
    "email": "[email protected]",
    "name": "Admin",
    "avatar": "",
    "active": true
  },
  "event": {
    "kind": "PLAN_INTEGRATION_ACTION",
    "timestamp": 1593196271185,
    "callbackId": "callbackId",
    "project": {
      "id": 1,
      "name": "Test Project",
      "archived": false
    },
    "plans": [],
    "eventType": "PLAN",
    "eventCategory": "action",
    "eventName": "action:plan",
    "stage":  "SUBMIT"
  },
  "configuration": {
    "Configuration field name": "https://my.config.url"
  },
  "payload": {
    "values": {
      "a name": "User text"
    }
  }
}
Project
{
  "installationId": "c9cfe1b1-a71e-4597-adc2-5011d0a15172",
  "user": {
    "userId": 1,
    "username": "admin",
    "email": "[email protected]",
    "name": "Admin",
    "avatar": "",
    "active": true
  },
  "event": {
    "kind": "PROJECT_INTEGRATION_ACTION",
    "timestamp": 1593189691024,
    "callbackId": "callbackId",
    "project": {
      "id": 1,
      "name": "Test Project",
      "archived": false
    },
    "eventType": "PROJECT",
    "eventCategory": "action",
    "eventName": "action:project",
    "stage":  "SUBMIT"
  },
  "configuration": {
    "Configuration field name": "https://my.config.url"
  },
  "payload": {
    "values": {
      "a name": "User text"
    }
  }
}
Test data table item
{
  "installationId": "c9cfe1b1-a71e-4597-adc2-5011d0a15172",
  "user": {
    "userId": 1,
    "username": "admin",
    "email": "[email protected]",
    "name": "Admin",
    "avatar": "",
    "active": true
  },
  "event": {
    "kind": "DATA_TABLE_INTEGRATION_ACTION",
    "timestamp": 1593196189579,
    "callbackId": "callbackId",
    "project": {
      "id": 1,
      "name": "Test Project",
      "archived": false
    },
    "dataTables": [
      {
        "id": 1285,
        "projectId": 1,
        "name": "Created from csv",
        "title": ""
      }
    ],
    "eventType": "DATA_TABLE",
    "eventCategory": "action",
    "eventName": "action:data_table",
    "stage":  "SUBMIT"
  },
  "configuration": {
    "Configuration field name": "https://my.config.url"
  },
  "payload": {
    "values": {
      "a name": "User text"
    }
  }
}
Test data tables
{
  "installationId": "c9cfe1b1-a71e-4597-adc2-5011d0a15172",
  "user": {
    "userId": 1,
    "username": "admin",
    "email": "[email protected]",
    "name": "Admin",
    "avatar": "",
    "active": true
  },
  "event": {
    "kind": "DATA_TABLE_INTEGRATION_ACTION",
    "timestamp": 1593196182601,
    "callbackId": "callbackId",
    "project": {
      "id": 1,
      "name": "Test Project",
      "archived": false
    },
    "dataTables": [],
    "eventType": "DATA_TABLE",
    "eventCategory": "action",
    "eventName": "action:data_table",
    "stage":  "SUBMIT"
  },
  "configuration": {
    "Configuration field name": "https://my.config.url"
  },
  "payload": {
    "values": {
      "a name": "User text"
    }
  }
}

# Updating app action forms

Some action event allow the app to actively set some form fields' properties. These events, and their accepted response formats, are detailed below. Always remember that, for each field, only properties marked as updatable can have their value changed as a result of this response.

# An action form is being initialized

This type of event happens when an app action form is open, at the INIT stage. The response accepts an object containing field names and then, for each, a list of updatable field properties and their values. Any of the form's fields may be updated at this stage.

// Response payload
{
  "username": {
    "value": "awesomeVirtuosoUser"
  },
  "password": {
    "required": true
  }
}

This response will cause the field username to get the value awesomeVirtuosoUser and the field password to become required, independently of what was defined in the form description. These values may be the result of some operation being performed at the app.

# An action form field causes other fields to be updated

This type of event happens when an app action form field value changes, and it has "dependent" fields that need updating, at the UPDATE stage. The response accepts an object containing field names and then, for each, a list of updatable field properties and their values. Only the form fields declared in the request payload may be updated at this stage.

// The form includes the following fields in its description:
[
  ...
  {
    "type": "TEXT",
    "name": "username",
    "label": "Username (optional)",
    "update": ["password"]
  },
  {
    "type": "TEXT",
    "name": "password",
    "label": "Password",
    "secret": true,
    "required": false
  },
  {
    "type": "TEXT",
    "name": "address",
    "label": "Address"
  },
  ...
]

// When the value of the field 'username' changes, the event will include:
{
  ...
  "event": {
    ...
    "stage":  "UPDATE"
  },
  "payload": {
    "values": {
      "username": "awesomeVirtuosoUser"
    },
    "action": {
      "type": "UPDATE",
      "fieldName": "username",
      "update": ["password"]
    }
  },
  ...
}

// And the response payload may be:
{
  "password": {
    "required":  true
  }
}

The above example illustrates a situation where the password field becomes required if the username field is filled. On the same example, please note that responding with properties for the address field would not have any effect, since it is not declared in the username field's update property.

# An action form's search field is used

This type of event occurs when an app action form search field query changes, at the UPDATE stage. The response accepts an object containing updatable properties of the search field. This can be used to update the list of options shown, based on the query.

// The form includes the following search field in its description:
[
  ...
  {
    "type": "SEARCH",
    "name": "clientName",
    "label": "Client name"
  },
  ...
]

// When the fiel's search box value changes, the event will include:
{
  ...
  "event": {
    ...
    "stage":  "UPDATE"
  },
  "payload": {
    "values": {...},
    "action": {
      "type": "SEARCH",
      "fieldName": "clientName",
      "query": "Joh"
    }
  },
  ...
}

// And the response payload may be:
{
  "options": [
    {
      "value": "111",
      "label": "John Doe"
    },
    {
      "value": "321",
      "label": "John Smith"
    },
    {
      "value": "555",
      "label": "John Travolta"
    }
  ]
}
Last Updated: 9/29/2023, 12:34:53 PM