VTAP

Vtiger Application Platform (a.k.a VTAP) is a low-code and no-code platform that helps you extend CRM capabilities on Cloud with suite of simple and powerful tools.

Don't have an account yet? Signup now

These range of tools enables you to customize simple UI flow to develop complex business flow such as integration with other apps or data from third-party application easily.

To get started, you'll need a solid understanding of HTML, CSS, and JavaScript, along with working knowledge of REST APIs for integration. Familiarity with jQuery, VueJS and Bootstrap Vue will further enhance your ability to create more better and deeper customized solutions.

Follow this Quickstart guide to know the ways of using the tools for various customization purpose.

To discover each of the tool capability in more detail you can follow the links below:

Quick Start

In this quick-start guide you will be introduced to the capabilities of VTAP to develop a solution for an imaginary industry.

Consider MOVENT Inc. to be an entertainment agency who are offering service to book Movie shows for their customers (or clients). They are in need of CRM implementation to streamline the process of tracking requests and enhancing customer engagement.

Customers reach MOVENT Inc. through Email or Phone to know about the Movie or confirm the Show timing they would like to get booking for.
Agents are required to provide the information about the Movie on Call. Agents use external services to book the show, and they need to send an email confirmation to the Customer.

CRM adoption is expected:

  • To delight the experience of Agents by making the Movie information available within CRM.
  • Agents should have the ability to track the show booking against the Customer and Movie.
  • To automate email confirmation to Customer when show is booked.

Solution Design

  • Customers can be tracked using CRM Contacts (standard module)
  • CRM should have information about Movies. (custom module)
  • Movie Show when booked can be tracked using CRM Events (standard module) linked to CRM Contacts and Movie.
  • Email confirmation to Customer should be sent after Movie Show is booked.

Solution Implementation

Let us now see how Platform tool enables you to build custom solution using capabilities within CRM.

1. Module Designer

Navigate to Menu > Platform > Module Designer, then click Add New Module, and choose Entity Module.

Now you can create Movies custom entity module. You have four more steps to complete the creation of Movies entity module.

(i) Module Properties :

(ii) Blocks And Fields :

(iii) Default Filter : This step allows you to select the records view in the custom module. Select desired columns you would like to see in the default list view of the module. Click Next.

(iv) Relationships :

For more detailed information on creating a custom module, refer to this guide

Solution Implementation

2. Module Configuration

Events standard module needs to be configured with newly created custom module.
This can be achieved through Settings > Module Layouts & Designer > Select Module Events

Using Settings > Picklist Field Values > Select Module Events

Add (Movie Show) to Activity Type field.

Add (Booked) to Status field.

Solution Implementation

3. Process Designer

Let us now setup Email Notification on Movie Show Booked status using Platform > Process Designer

(i) Create new process flow on Events module.

(ii) Configure Trigger Properties and Save.

(iii) Add Email Action

(iv) Configure Email Task and Save.

  • To: $(contact_id : (Contacts) email)
  • Subject: $(cf_calendar_event : (vtcmmovies) fld_vtcmmoviesname) - is Booked
  • Body: We have $eventstatus - $(cf_calendar_event : (vtcmmovies) fld_vtcmmoviesname) for you.

(v) Finally Save and Publish the Process.

Solution Validation

  • Create Contact with valid email address.
  • Create Movie with Name.
  • Create Event in Full Form
    • Event Name: Movie for "Customer Name"
    • (Start Date & Time = Show start time)
    • (End Date & Time = Show end time)
    • (Activity Type = Movie Show)
    • (Status = Booked)
    • Select Movie
    • Select Contact
    • Save the Event (accept do you want to send email to all invitees)

CRM would trigger Email Notification to Contact.

Hurrah! we completed implementation of CRM for MOVENT Inc.

Enhancements

We are using themoviedb.org service to demonstrate the API integration capabilities of VTAP.

You will need to Join TMDB to obtain API Key or Token. TMDB API documentation has information about usage of APIs.

Let us now learn how to use TMDB APIs to enhance functionality within CRM.

Data Synchronization

TMDB provides GET /discover API to fetch details of movies by popularity. We shall use it to demonstrate how to build data connector which can bring data into CRM.

GET /discover Usage

curl --request GET \
     --url 'https://api.themoviedb.org/3/discover/movie?include_adult=false&sort_by=popularity.desc' \
     --header 'Authorization: Bearer api_read_only_token' \
     --header 'accept: application/json'
{
  "page": 1,
  "results": [
    {
      "adult": boolean,
      "backdrop_path": string,
      "genre_ids": array,
      "id": number,
      "original_language": string,
      "original_title": string,
      "overview": string,
      "popularity": decimal,
      "poster_path": string,
      "release_date": date,
      "title": string,
      "video": boolean,
      "vote_average": float,
      "vote_count": number
    },
}

Build Connector

To build data sync connector for Movies module in CRM.

Navigate to Main Menu > Platform > Module Designer

  1. Choose Movies
  2. Click on + Connector

Fill details of connector. Click Save

Here is the connector VADL code which enables you to get user-configuration, make-request and map-response to CRM records easily.

<?xml version='1.0'?>
<connector for="Sync">
    <!-- configuration field to accept details during connector setup. -->
    <config>
        <fields>
            <field name="tmdb_api_token" type="text" required="true" />
        </fields>
    </config>
    <!-- synchronization strategy global to CRM -->
    <synctype>app</synctype>

    <!-- Definition to pull data from external service. -->
    <service>
        <url>https://api.themoviedb.org</url>
    </service>
    <pull>
        <request method="get">
            <url>/3/discover/movie</url>
            <headers>
                <header name="content-type" value="application/json" />
                <header name="Authorization" value="$config.$fields.tmdb_api_token" />
            </headers>
            <parameters>
                <parameter name="include_adult" value="false" />
                <parameter name="sort_by" value="popularity.desc" />
            </parameters>
        </request>
        <response format="xml|json">
            <code use="200" />  <!-- expected response code -->
            <error use="errorIdentifier" />
            <records use="results" />
            <recordid use="id" />
        </response>
    </pull>
    
    <!-- Map external data to CRM data -->
    <modules>
        <!-- 
            servicemodule: short-identifier of target service 
            vtigermodule : target module of CRM
        -->
        <module servicemodule="TMDB" vtigermodule="vtcmmovies"></module>
    </modules>
    <fieldmapping>
        <vtcmmovies>
            <!-- map each row of (pull > records) to module record.  -->
            <field vtigerfield="fld_vtcmmoviesname" servicefield="original_title" />
            <field vtigerfield="fld_releasedon" servicefield="release_date" />
        </vtcmmovies>
    </fieldmapping>
</connector>

You can copy-and-paste code in IDE. Save and Publish.

It will now be ready for use.

Use Connector

Sync button will appear on Movies module as we have published TMDB Connector.

We have to activate as first step.

Click Settings and Save with default option.

Login to TMDB. Navigate to Settings > API > Copy API Read Access Token

Update Sync Settings > tmdb_api_token

With Bearer API-Read-Access-Token

Click Save. We are ready to Sync Now

With Sync Now success you will be able to check the Sync Log

Popular movies will appear in the Movies ListView.

You can learn more about Data Sync connector.

To stay focused now let us proceed with API Integration

API Integration

TMDB GET /search API enables search of movie by name and release year.

curl --request GET \
     --url 'https://api.themoviedb.org/3/search/movie?query=Atlas&include_adult=false&language=en-US&page=1&year=2024-05-23' \
     --header 'Authorization: Bearer api_read_access_token' \
     --header 'accept: application/json'

Allow Domain

Click on Gear / Cog-wheel icon.

Add Allowed domains https://api.themoviedb.org. Click Save

We now granted permission for CRM to interact with TMDB Service APIs.

Add API

Click on + Add API.

Choose Rest Api.

Publish

You can copy-and-paste the VADL code below.

<?xml version="1.0"?>
<api method="get">
    <rest method="get">
        <url>https://api.themoviedb.org/3/search/movie</url>
        <headers>
            <header name="Authorization" value="Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIwM2ZjNDdkNDIzNjFjMDc5MmQ1ZWQ0Y2NmNmFkMTQzMSIsInN1YiI6IjY2NWQ3NjU5ZTI2MDUxMTA5ODkwNmQ5ZCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.ZCr_HrIWH-PMlvq5bcZ8hh5TTsIKCKXgHwAwZB0iBm0"></header>
        </headers>
        <parameters>
            <parameter name="query" value="@moviename"></parameter>
            <parameter name="year" value="@releasedon" presence="optional"></parameter>
            <parameter name="include_adult" value="false"></parameter>
        </parameters>
    </rest>
</api>

Validate

Open Playground to validate with sample input.

It worked.

In-App Customization

Module Designer allow you to customize in-app experience.

Navigate to Main Menu > Platform > Module Designer

Custom Component

Choose Movies Module.

Click + Component and add Custom MovieDetailPopup component.

Copy-and-Paste the component definition below:

var vtcmmovies_Component_MovieDetailPopup = VTAP.Component.Core.extend({
    /* caller will send the attribute */
    props: ["movieinfo"],
    template: `<b-modal :title="movieinfo.title">{{movieinfo.overview}}
        <p><img width="100%" height="480px" 
            :src="'http://image.tmdb.org/t/p/w500/' + movieinfo.poster_path"></p>
        </b-modal>`
});

Click Save and Publish

Custom Button

Switch to UI Actions

Now Add UI Component

OnClick Javascript code with VTAP JS runtime:

VTAP.Detail.Record().then( (record) => {
    VTAP.CustomApi.Get("SearchTMDB", 
        {moviename: record["fld_vtcmmoviesname"], releasedon: record["fld_releasedon"]}, 
        (e, res) => {
            if (e) return alert(e.message);
            var results = JSON.parse(res.content).results;

            VTAP.Utility.ShowPopup({
                /* To load vtcmmovies_Component_MovieDetailPopup */
                component : VTAP.Component.Load('MovieDetailPopup', 'vtcmmovies'),  
                /* and bind to component property */
                componentData : {movieinfo : results[0]}, 
                modalOnModalMode : true
            });    
        }
    );
});

Click OK

Result

Movie Detail Button

OnClick Popup

We have now made Agent life easier to get details of movie within CRM!

Custom App

With App Creator you can enhance the UX (User-Experience) that suits your business flow. Lets now create an application that simplifies the steps Agent takes to get Movie Show Booked.

Mockup Plan

Niche UI can improve productivity to good extent. Planning & Design is the first step.

Create App

Navigate Main Menu > Platform > App Creator

Click Add App

Fill the form and Save

Click movie_shows app to edit.

views/index.html

Copy-and-paste the HTML code below which uses Bootstrap, jQuery, Select2 plugins with VCAP and index.js runtime.

<!DOCTYPE html>
<html>
    <head>
        <title>Movie Shows</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
		<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css">
		<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css">
    	<link rel="stylesheet" href="resources/index.css">
    </head>
    <body>
        <div class="container">
			<div class="row">
            	<form id="movieshowform" class="col">
					<legend>Track Movies Booking</legend>
					<fieldset>
					<p>
						<select id="formovie" class="form-control" data-placeholder="Choose Movie" required></select>
					</p>
					<p>
						<select id="forcontact" class="form-control" data-placeholder="Choose Contact" required></select>
					</p>
					<p>
						<input id="showdate" type="date" required>
					</p>
					<button type="submit">OK</button>
					</fieldset>
				</form>
				<div class="col" id="movieinfo">
					<h4>About Movie</h4>
					<div class="overview"></div>
					<div><img class="poster" height="480px" width="480px"></div>
				</div>
			</div>
        </div>

        <script src="resources/jquery.min.js"></script>
		<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.full.min.js"></script>
	    <script src="resources/vcap.js"></script>
        <script src="resources/index.js"></script>
    </body>
</html>

Click Crtl+S to save IDE changes.

resources/index.js

Copy-and-paste index.js handler code which initializes the form inputs and dynamic behaviour for the app.

window.addEventListener('load', () => {
	
	/**
	 * Config and State management for Searching.
	 */
	var searchState = {
		Contacts: {
			fields: ["firstname", "lastname"],
			filterid: 7,  /* Contacts: All Contacts filter */
			term: null,
			progress: false,
			result: null
		},
		vtcmmovies: {
			fields: ["fld_vtcmmoviesname"],
			filterid: 144, /* Movies: All Movies filter */
			term: null,
			progress: false,
			result: null
		}
	}
	
	/* helper function to build (q) clause with or-term startwith search */
	function searchWithOrClause(fields, term) {
		var q_and = [];
		var q_or = [];
		for (var i in fields) {
			q_or.push([fields[i], "startwith", term]);
		}
		return [ q_and, q_or ];
	}
	
	/* build custom transport for select2 specific to module and fields */
	function searchParamsInModule (module, fields, filterid) {
		/* transport function that gets callback from select2 */
		return function (params, success, failure) {
			var term = params.data.term;

			/* avoid search if last-one has not completed to avoid server overload */
			if (searchState[module].progress) {
				return;
			}

			/* if new-term is has not completely changed consider earlier result */
			if (searchState[module].term && term.indexOf(searchState[module].term) === 0) {
				return success(searchState[module].result);
			}
			
			/* prepare for async search */
			var response = new Promise((resolve, reject) => {
				/* mark the state */
				searchState[module].progress = true;
				searchState[module].term = term;

				/* do the search */
				VCAP.userapi.records.get({
					module: module, filterid: filterid,
					q: JSON.stringify( searchWithOrClause(fields, term) )
				}, (e, records) => {
					/* update the state */
					searchState[module].progress = false;
					searchState[module].result = records;

					/* return result */
					resolve(records);
				});
			});

			/* bind to success callback */
			response.then(success);

			return response;
		}
	}
	
	/* custom transformer to convert record to show in select2 dropdown */
	function transformRecordsToSelect2Options() {
		/* callback function after search */
		return function(records) {
			/* add text field */
			for (var i in records) records[i].text = records[i].label;
			return {results: records }
		}
	}
	
	/* select2 plugin activation for Movies */
	jQuery("#formovie").select2({
		minimumInputLength: 3,
		allowClear: true,
		ajax: {
			transport: searchParamsInModule("vtcmmovies", searchState["vtcmmovies"].fields, searchState["vtcmmovies"].filterid),
			processResults: transformRecordsToSelect2Options()
		}
	}).on("change", function(ev) {
		/* If movie selection changes */
		var choice = jQuery(this).select2('data');
		
		/* hide the last movie info */
		jQuery("#movieinfo").hide();
		
		/* movie was unselected or cleared? */
		if (!choice.length) return;
		
		/* 
		 * There was movie selection change
		 * use customapi created earlier for movie search 
		 * show the overview and poster image. 
		 */ 
		var movie = choice[0];
		VCAP.customapi.get("SearchTMDB", {
			moviename: movie["text"]
		}, (e, movieres) => {
			if (e) return alert(e.message);
			var movieinfo = JSON.parse(movieres.content).results[0];
			jQuery("#movieinfo .overview").text(movieinfo.overview);
			jQuery("#movieinfo .poster").attr(
				"src", "https://image.tmdb.org/t/p/w500" + movieinfo.poster_path);
			jQuery("#movieinfo").show();
		});
	});
	
	/* select2 plugin activation for Contact lookup */
	jQuery("#forcontact").select2({
		minimumInputLength: 3,
		allowClear: true,
		ajax: {
			transport: searchParamsInModule("Contacts", searchState["Contacts"].fields, searchState["Contacts"].filterid),
			processResults: transformRecordsToSelect2Options()
		}
	});

	/* form submit handler */
	jQuery("#movieshowform").submit(function(ev) {
		/* take custom control */
		ev.preventDefault();
		
		/* to disable when processing */
		var fieldset = jQuery("fieldset", this);
		
		/* field inputs */
		var contactid = jQuery("#forcontact").val();
		var movieid = jQuery("#formovie").val();
		var showdate = jQuery("#showdate").val();
		
		/* invalid state - return */
		if (!contactid || !movieid) return;
		
		/* about to process - disable changes */
		fieldset.attr("disabled", true);
		
		/* create event */
		VCAP.userapi.records.post({
			module      : "Events",
			subject     : "Movie Show For " + jQuery("#forcontact").select2("data")[0].text,
			eventstatus : "Booked",
			activitytype: "Movie Show",
			date_start  : showdate,
			time_start  : "09:00", /* todo: accept input */
			due_date    : showdate,
			time_end    : "10:30", /* todo: determine based on movie length */
			cf_calendar_movie: movieid,
			contact_id: contactid
		}, (e, event) => {

			/* renable form */
			fieldset.attr("disabled", null);
			
			/* error - show message */
			if (e) return alert(e.message);

			/* success - show feedback */
			fieldset.html(
				"<a target='_blank' href='/view/detail?module=Events&id=" + event.id  +"'>" + event.subject +" - Booked</a><br>" +
				"<button onclick='location.reload()'>New</button>"
			);
		});
	});
});

Click Crtl+S to save IDE changes.

Publish & Launch App

After saving file changes you need to Publish app for reflecting them others.

You can Launch app (or reload app if launched previously)

Awesome! we have now achieved enhancing user-experience with this niche custom app.

FAQs

Why am I not seeing published changes?

During development Browser cache can trouble new changes.

  • Hard refresh the app after launch.
  • You can disable cache that should help.

VCAP - Custom App Runtime.

VCAP JS

How to find the fieldname?

How to find the modulename and filterid?

Platform

To discover each of the tool capability in more detail you can follow the links below:

Availability

VTAP Tools are only available in specific versions of Vtiger CRM.

See the table's below to find out if your current Vtiger edition has access to the feature and limits.

(i) Module Designer And API Designer

EditionModule Designer (Limit)API Designer (Limit)
PilotNoNo
ExclusiveNoNo
Sales StarterNoNo
Sales ProfessionalYes (25)Yes (25)
Sales EnterpriseYes (25)Yes (50)
Support StarterNoNo
Support ProfessionalYes (25)Yes (25)
One ProfessionalYes (25)Yes (25)
One EnterpriseYes (25)Yes (50)

(ii) Process Designer

EditionStandard Processes per ModuleScheduled Processes per Instance
One Pilot1 Process0
One Growth1 Process1
One Professional3 Processes2
One Enterprise5 Processes / Module, 5 flows, 10 Actions3

(iii) App Creator
App Creator (App Designer) is now available by default for Learning and Developer editions of Vtiger CRM.


(iv) Insights Designer

EditionInsights Designer
One Pilot
One Growth
One Professional3 Dashboards
One Enterprise5 Dashboards

(v) Layouts Designer

Feature AvailabilityOne PilotOne GrowthOne ProfessionalOne Enterprise
Layout Designer--

(vi) Server Scripts

EditionAvailableServer Script execution daily limitServer Job execution daily limit
StarterNo--
ExclusiveNo--
GrowthYes1001
ProfessionalYes3003
EnterpriseYes5005

Module Designer

Module Designer allows you to create or extend the modules of Vtiger CRM.

Modules are of 3 types:

  • Entity - follows standard conventions of (fields, records and relations). Provides out-of-box features like Access control, Reports, Workflows, Approvals etc. Example: Contacts, Organizations, Deals, Invoices, and many more.

  • Extension - free of any conventions. Provides specailized features. Example: Sales Insights, Keyboard shortcuts etc.

  • Virtual - connects to remote data-source enabling interaction without copy-of-data to CRM.

Following are some of the customization use-cases where you may find Module Designer helpful:

  • Add a button in the list/detail page
  • Add a custom widget on the detail page
  • Add an icon when you hover over list page records
  • Have a notification appear when conditions are met
  • Design a custom page in a module or create a brand new module.
  • Change the look and feel of Vtiger UI.
  • Add custom components(html components) which can be used at multiple places including custom pages.
  • Build SMS, Whatsapp, Data Sync connectors to build communication channels using various providers and provide 1-way/2-way crm data sync with external applications.

API Designer

API Designer provides a workbench for developers to build custom HTTP based APIs which can be exposed to the VTAP Javascript APIs or the external application for integration easily.

APIs can be defined using VADL specification. Platfrom takes care of user authorization and record-access control as per configuration in the CRM.

You Can Use API Designer for different purposes :

  • To Build Vtiger custom REST APIs. Combine multiple API operations in one API call.
  • To Build custom incoming webhook end points.
  • When you want Vtiger Javascript APIs to connect and directly interact with 3rd party applications.
  • To integrate with any applications that has basic authentication, secret key based, bearer based or oauth2 based APIs enabled.

Process Designer

Vtiger CRM’s Process Designer module provides users with a graphical interface that simplifies the creation of systematic business workflows. This is designed to help users design and streamline their business processes using a convenient flowchart tool.

The Vtiger CRM Process Designer:

  • Provides you with an easy-to-use interface with tools and components to create a workflow or a flowchart.
  • Lets you configure various business activities including conditions, actions, and duration as needed for the process.
  • Helps you publish a workflow or activate it or save it as a draft.
  • Automatically triggers and executes the workflow for your business in the CRM once you publish it.
  • Allows you edit flows that are in saved or draft stage.
  • Is accessible to all (admin and non-admin) users in Vtiger CRM.
  • It is a very user-friendly feature enabled for all users. Any Vtiger CRM user can very conveniently access and use the Process Designer and design their business processes.

Benefits of using Process Designer :

  • Allows you to configure a business process’s workflows, etc., available on different paths into a single flow chart.
  • Helps you identify gaps and errors in the process.
  • Offers smart administration by easily translating flowcharts to real business workflows. Helps you troubleshoot and smoothen the process.

For more details and examples on Process Designer refer this guide

App Creator

VTAP platform provides an App Creator toolkit to build creative applications that solve business needs that are beyond CRM configuration. App Creator runtime provides developers capabilities to extend CRM functionality, strengthen business integration or custom needs, and requires limited coding knowledge.

Using the App Creator, you can:

  • Build specialized cloud web applications for clients.
  • Use rich client-side web applications, by developing using any framework.

Benefits

  • Develop with just HTML, CSS and Javascript knowledge.
  • Deliver advanced business UX flow using the CRM platform without reinventing the wheel.
  • Distribute the built applications so other users can use them to expand the CRM capabilities.

In this article, we will learn how to develop an application using VTAP App Creator.

Create Application

STEP 1 : Login into your CRM and navigate Main Menu > Platform > App Creator > +Add App

Enter the information for the following:

NameEnter a name for the application.
IconBrowse and choose an icon for the application.
DescriptionEnter a brief description of the application.
StatusClick the checkbox to set the application status to active.
OwnerSelect the owner of the application from the drop-down.

Click Save. Application is now created for you to code.

For more information on how to use the App Creator refer this guide

Insights Designer

If your data is voluminous and distributed across different apps, you need a tool that can pull, analyze, and present data in easy-to-understand charts and graphs.

Vtiger Insights Designer helps you analyze vast amounts of data and build custom reports. The feature provides full control to CRM users to build specific BI reports suited to their needs.

Application and Use Cases of Insights Designer

Using the Insights Designer, you can:

  • Create an Insight page with multiple report widgets
  • Fetch data from different endpoints. It supports the following queries
    • Vtiger Webservice query
    • Custom Rest APIs from Vtiger API Designer
  • Visualize and display data in various modes
    • Table
    • Pivot
    • Different types of charts
  • Pin the designed Insight (report) to the Dashboard
  • Share generated insights with other users
  • Insights Designer can be used in a wide range of industries.

Following are a few use cases:

  • E-commerce: Insights Designer can help online retailers track and analyze customer behavior, such as browsing and purchasing habits, to optimize their website and product offerings.
  • Finance: Insights Designer can be used to analyze financial data, such as stock prices and market trends, to inform investment decisions and predict future market trends.
  • Healthcare: Insights Designer can help healthcare providers analyze patient data, such as medical histories and treatment outcomes, to improve patient care and treatment protocols.
  • Marketing: Insights Designer can help marketing teams analyze customer data to develop more targeted and effective marketing campaigns.
  • Supply Chain Management: Insights Designer can analyze supply chain operations data, such as inventory levels and shipping times, to optimize supply chain efficiency and reduce costs.
  • Social Media: Insights Designer can help social media managers track and analyze user engagement metrics, such as likes and shares, to inform content creation and marketing strategies.
  • Education: Insights Designer can be used to analyze student data, such as test scores and attendance records, to inform curriculum development and improve student outcomes.

Insights Designer Components

The following are the components available in the Insights Designer:

  • Insight - A record in the Insights Designer module is called an Insight.
  • Widget - Each report inside an Insight is called a Widget.
    • Input - Data sources that are used to fetch the data.
    • Transform - Ability to write javascript code to alter the data if needed.
    • Output - Where you can set up the output details for data visualization.

For more details and information on how to use the Insights Designer refer this guide

Layout Designer

The Layout Designer in Vtiger CRM is a powerful tool that allows users to create custom UI layouts for each module, tailoring the CRM experience to specific business needs. Whether you’re tired of the default module layouts or struggling to find widgets that suit your workflow, the Layout Designer offers a solution by providing a flexible, user-friendly interface for designing personalized layouts.

The above image shows the Layout Editor.

Layout Editor Can be used for creating :

  • Customizable Column Layouts: Users can configure module layouts by selecting from one, two, or three-column basic blocks. For example, selecting a two-column layout divides the screen into two columns where widgets can be easily dragged and dropped.

  • Drag-and-Drop Interface: The intuitive drag-and-drop interface allows users to customize various elements such as fields, icons, and related lists. This makes it possible to design layouts that are perfectly aligned with users' roles and responsibilities.

  • Field Customization: In specific modules like Contacts, users can organize fields like Salutation, First Name, Last Name, Primary Email, Date of Birth, and more. This customization ensures that all relevant information is easily accessible within the layout.

  • Inclusion of Related Lists: Users can add related lists—such as Organizations, Deals, and Sales Orders—directly into the layout, making it easy to access all pertinent data within a single view.

  • Dynamic Element Integration: The Layout Designer supports the addition of dynamic elements like images and tables. Widgets can be deleted or replaced using the drag-and-drop method, offering flexibility in layout design.

  • Support for Dynamic Dependencies: Fields can be displayed, hidden, or made mandatory based on the values of other fields, providing a dynamic and responsive CRM experience. For instance, a field's visibility or required status is determined by its dependency settings in the Module Layouts and Fields.

  • Enhanced Usability and Effectiveness: By allowing users to create contextually relevant layouts, the Layout Designer improves data alignment, streamlines workflows, and enhances the overall user experience in Vtiger CRM.

Benefits of Using Layout Designer

  • Personalized User Experience: By allowing users to add custom fields and organize them according to their preferences, Layout Designer enhances the user experience and makes the CRM more intuitive.

  • Better Data Management: Layout Designer enables the customization of fields and their placement, improving data organization and management. It also allows for the hiding of sensitive information and the highlighting of relevant data.

  • UI Enhancements: The tool improves the visual appeal and usability of the CRM by providing options to arrange and customize widgets and fields, making user interaction more efficient and enjoyable.

  • Scalability and Flexibility: As business processes evolve, Layout Designer offers the flexibility to modify layouts to meet new requirements, ensuring that Vtiger CRM remains scalable and adaptable over time.

For more information on how to use the Layout Designer you can refer this guide

Server Scripts

Server Scripts and Jobs empower you to craft server-side scripts for executing diverse tasks. Often there arises a necessity for customized processing on the server, either triggered by record updates or scheduled intervals. This module empowers Vtiger users to author server-side scripts for executing diverse tasks. Through this capability, users can enact custom actions upon record saving or upon approval or rejection. Moreover, they can devise bespoke background tasks to execute at predetermined intervals.

Use Cases

  • Validate contact email addresses.
  • Enhance lead information.
  • Sync customer credit scores with CRM.
  • Execute business rule-based actions, like checking if it's a weekend.
  • Regularly import leads from external sources.
  • Update parent or child records based on business rules.
  • Merge CRM data with external data to create or update related records.
  • Send CRM data to a data warehouse.
  • Integrate AI for customer sentiment analysis in feedback. These are just a few examples of what you can do.

Server Scripts : Server Scripts are triggered by specific actions, such as when a record is saved, approved, or rejected. They're ideal for lighter tasks like updating fields based on business logic.

  • Triggered during record save, approval, or rejection.
  • Access both the record data and user information.
  • Useful for tasks like updating fields, creating records in other modules, or sending emails.
  • Changes made to the record data are updated in the CRM.
  • Execute immediately but can slow down record saves if too complex.

Server Jobs : Server Jobs run at scheduled intervals, making them perfect for heavier, periodic tasks.

  • Run in the background at set times (e.g., hourly, daily, weekly).
  • Do not use user or record data, but operate under the owner user's context.
  • Ideal for tasks like fetching or sending data to external services. They don't interfere with CRM operations as they run in the background.

For more information and details on how to use the Server Scripts refer this guide

Add-ons Publisher

The Add-On Publisher is a newly introduced feature in VTAP that empowers developers to create and publish custom packages or bundles directly to the Vtiger marketplace. This feature provides a unique opportunity for developers to build functionalities tailored to their specific instance needs, which can then be made available for all Vtiger users to download and utilize.

To get started, create an account through the VTAP Add-On Publisher portal. Once your account is set up, log in to your instance, navigate to the Modules section, and search for "Add-On Publisher" to begin building and publishing your custom add-ons.

Create

Steps to create a new addon.

  1. Click on create add-on.

  2. Select publish any modules - This will create a bundle of modules to be published and used in the main instance.

  3. Add-on name - The name of the Add-on.

  4. Select module - Select any module and click on add.

  5. Once you select module you will be able to see that module now click on the + icon and select any feature which is required.

    Features Available:

  • Fields - Any custom field which is needed can be selected here.
  • Other Settings - Settings such as closed states, duplicate prevention can be selected here.
  • Relationships - Custom relations ships.
  • Dependent Fields/Block - Field dependencies settings.
  • Picklists Values - All picklist values.
  • PickList Dependency - All picklist dependencies.
  • Lists - All the filters in that module.
  • Email Templates - All the templates created in that module.
  • Print Templates - All the templates created in that modules.
  • Reports - All the reports in the module.
  • Workflows - All the workflows in the module.
  • Approvals - Approvals in the module.
  • Process Designer - Processes in the module.
  • Sharing Rules - Rules added in the module.
  • API - Rest api's and webhooks created for the module.
  • TAP Components - module designer component.
  • TAP Scripts - module designer scripts.
  • TAP Styles - modules designer styles.
  • TAP Pages - modules designer pages.
  • TAP Connectors - connectors.
  • Labels - labels.
  • Module Designer Domains - Whitelisted domains.
  • Api Designer Domains - Whitelisted Api-designer domains.
  • UIActions Components - UI actions in the module.
  1. Once the required modules and features are selected submit it for review.
  2. Once it is submitted.
  3. The Vtiger team will review the changes.
  4. If any changes are required vtiger team will notify the owner.
  5. Once all the validations and checks are completed. It is added to addons/ marketplace.
  6. Now you can login to any account and install this add on.

Build your own

Build an add-on for the contacts module to add a new field, list.

Prerequisites

  1. Go to contacts module, modules&layouts add a new field say test and save it.
  2. Go to contacts module and add a new filter say AllRecords.

Steps to build a add on & Submit for review

Create a new addon.

  1. Select contacts as module.
  2. Now select fields, select the custom field which is needed.
  3. Select the filter which is required.
  4. Add the necessary features and click on submit.

vtapit

https://help.vtiger.com/article/156233487-VTAPIT---Command-line-tool

vtapit is a Command line tool that enables you to develop and manage VTAP Apps created from the App Creator (Platform > App Creator). It has sub-commands similar to the git which makes it easy to learn and get started.

Get Started

Create a new application (testapp) on Main Menu > Platform > App Creator

Download your OS compatible with executable - https://extend.vtiger.com/vtap/vtapit/dist

Sub Commands

cloneMakes a copy of the application created in the CRM to your local working directory.
runEnables runtime on the local working directory, which uses local files and CRM Apis for data.
addLets you add newly created local files to the CRM app.
statusLets you check status of newly added or modified local files.
pushUploads all the local changes (add or modify functions) made to the files to the CRM app.
publishWhen all local changes are pushed - use this sub-command to make it available.
pullDownloads all the changes from the CRM app back to a local working directory. A conflict marker file is created if a file has changed in the CRM but not in the local directory. You need to use the diff tool to compare changes and then use the Resolve sub-command.
resolveConfirm the conflict (diff) changes you have managed to review.

Clone

This will copy the files of the custom app to your local system.

vtapit clone --crmapp=CRMAPP --crmurl=CRMURL DIRECTORY
  • CRMAPP - App name that you have provided in the App Creator Module
  • CRMURL - URL to your CRM Account
  • DIRECTORY - The local directory where the cloned application files will be saved. Ensure this path is correct and writable

Example:

vtapit clone --crmapp=testapp --crmurl=https://your.odx.vtiger.crm local_dir/testapp

You will be prompted for CRM Username and Password for authentication

Run

This command will run the custom app on your local server. After running this command, you can access it http://localhost:8080/myapps/YOUR_APPNAME

cd local_dir/testapp
vtapit run

Add

This command will add a new file to your application.

cd local_dir/testapp
touch path_to_new_file
vtapit add path_to_new_file

Status

This command will show status of files in local directory of application.

cd local_dir/testapp
vtapit status

Push

This command will send the updated files and commit them to the app creator.

cd local_dir/testapp
vtapit push

Publish

Applies the changed files (saved throgh Push) to application. Users can view the new changes to the app.

cd local_dir/testapp
vtapit publish

Pull

This command will pull the changes done on the app creator by other developers to your local copy.

cd local_dir/testapp
vtapit pull

Resolve

In case of conflict resolution, this command will mark the file path to resolve so it can be published to the app creator.

cd local_dir/testapp
vtapit resolve path_to_file

Note: You can set up CRM environment variables to avoid repeating on the command line.

CRM_URL=https://crm.url.tld
CRM_USER=user@abc.com
CRM_PASSWORD=password
CRM_APP=appname

VSCode

You can use VSCode extension too.

REST API

Vtiger Cloud offers REST APIs which can be used for integration with external application.

Endpoint: https://CRM_URL/restapi/v1/vtiger/default

Authentication: HTTP Basic (CRM Username and Access Key). You can find Access Key under My Preferences in the CRM.

Response Format

StatusBodyRemarks
200JSON{ success: true, result: JSON_VAL }
4xx-Failure. Header line with error message
5xx-Failure. Header line with error message

Record ID

Webservice ID of the form (ModuleTypeIDxRecordID) is used to represent record references.

Described below are the details of API. You can use this postman collection to explore against your CRM instance.

/me - User Profile

This API helps you to get the logged in user profile or context (like UserID, Name).

GET Endpoint/me
{
    success: true,
    result: {
        id: user_record_id,
        user_name: string,
        fist_name: string,
        last_name: string,
        email1: string
    }
}

/listtypes - Modules List

Use this API to get details of the accessible list of modules and basic metadata of module.

GET Endpoint/listtypes?fieldTypeList=null
ParameterRemarks
fieldTypeListnull - for any. fieldTypeList[]=typename for specific type. (typename = email, grid etc...)
{
    success: true,
    result: {
        types: [
            module_name
        ],
        information: {
            module_name: {
                isEntity: boolean,
                label: string,
                singular: string
            }
        }
    }
}

/describe - Module Metadata

Use this API to get metadata of module comprising of fields, blocks, permissions.

GET Endpoint/describe?elementType=module_name
ParameterRemarks
elementTypeModule name
{
    success: true,
    result: {
        name: string,
        label: string,
        createable: boolean,
        updateable: boolean,
        deleteable: boolean,
        retrieveable: boolean,
        fields: [
            {
                name: string,
                label: string,
                mandatory: boolean,
                quickcreate: boolean,
                summaryfield: boolean,
                headerfield: boolean,
                default: value,
                type: {
                    name: string,
                    length: size,
                    refersTo: [ reference_module_name ],
                    picklistValues: [ {value: val, label: lbl} ],
                    defaultValue: picklist_value
                },
                isunique: boolean,
                nullable: boolean,
                editable: boolean,
                data: json // extended info
            }
        ],
        inactivefields: [ field_info ],
        idPrefix: module_type_id,
        isEntity: boolean,
        allowDuplicates: boolean,
        labelFields: string_or_arrayOfString
    }
}

/create

This API enables you to create a single entity record. You are expected to send all the mandatory field values along with the optional fields for successful record creation.

POST Endpoint/create?elementType=moduleName&element=convert_into_json_string ({field1:value1, field2:value2})
ParameterRemarks
elementTypeTarget Module Name
element({field1: value1, field2:value2})
{
    "success": true,
    "result": {
        "notes_title": "Sample_file_1",
        "document_source": "Vtiger",
        "assigned_user_id": "",
        "note_no": "DOC19",
        "folderid": "22x1",
        "createdtime": "2023-06-13 05:46:59",
        "modifiedtime": "2023-06-13 05:46:59",
        "modifiedby": "",
        "created_user_id": "",
        "source": "WEBSERVICE",
        "starred": "0",
        "tags": "",
        "record_currency_id": "",
        "record_conversion_rate": "",
        "filelocationtype": "I",
        "filestatus": "1",
        "filename": "",
        "filesize": "0",
        "filetype": "",
        "fileversion": "",
        "filedownloadcount": "",
        "share_count": "0",
        "email_open_count": "0",
        "open_count": "0",
        "shared_download_count": "0",
        "reshares_count": "0",
        "unique_open_count": "0",
        "deal_conversion_rate": "0",
        "avg_time_spent": "0",
        "total_time_spent": "0",
        "document_type": "Public",
        "notecontent": "",
        "id": "7x549",
        "isclosed": 0,
        "label": "Sample_file_1",
        "url": "https://{{your_domain}}/view/detail?module=Documents&id=549"
    }
}

/retrieve

You can pull a piece of specific record information using this API.

GET Endpoint/retrieve?id=record_Id
ParameterRemarks
idrecord_Id - Restapi uses a composite key to represent record id, a combination of (module-type-id and module-record-id) separated by (x).
{
    "success": true,
    "result": {
        "notes_title": "Sample_file_1",
        "document_source": "Vtiger",
        "assigned_user_id": "",
        "note_no": "DOC14",
        "folderid": "22x1",
        "createdtime": "2023-06-09 06:11:18",
        "modifiedtime": "2023-06-09 06:11:18",
        "modifiedby": "",
        "created_user_id": "",
        "source": "WEBSERVICE",
        "starred": "0",
        "tags": "",
        "record_currency_id": "",
        "record_conversion_rate": "",
        "filelocationtype": "I",
        "filestatus": "1",
        "filename": "Sample_1.odt",
        "filesize": "22728",
        "filetype": "application/vnd.oasis.opendocument.text",
        "fileversion": "",
        "filedownloadcount": "18",
        "share_count": "0",
        "email_open_count": "0",
        "open_count": "0",
        "shared_download_count": "0",
        "reshares_count": "0",
        "unique_open_count": "0",
        "deal_conversion_rate": "0",
        "avg_time_spent": "0",
        "total_time_spent": "0",
        "document_type": "Public",
        "notecontent": "",
        "id": "7x521",
        "isclosed": 0,
        "imageattachmentids": "7x522",
        "label": "Sample_file_1",
        "url": "https://{{your_domain}}/view/detail?module=Documents&id=521"
    }
}

/query

Retrieve one or more records matching filtering field conditions.

GET Endpoint/query?query=query_string;
ParameterRemarks
queryquery_string
{
    "success": true,
    "result": [
        {
            "notes_title": "testing 123",
            "document_source": "Vtiger",
            "assigned_user_id": "",
            "note_no": "DOC11",
            "folderid": "22x1",
            "createdtime": "2023-06-09 05:57:13",
            "modifiedtime": "2023-06-09 05:57:13",
            "modifiedby": "",
            "created_user_id": "",
            "source": "WEBSERVICE",
            "starred": "0",
            "tags": "",
            "record_currency_id": "",
            "record_conversion_rate": "",
            "filelocationtype": "",
            "filestatus": "1",
            "filename": "",
            "filesize": "0",
            "filetype": "",
            "fileversion": "",
            "filedownloadcount": "",
            "share_count": "0",
            "email_open_count": "0",
            "open_count": "0",
            "shared_download_count": "0",
            "reshares_count": "0",
            "unique_open_count": "0",
            "deal_conversion_rate": "0",
            "avg_time_spent": "0",
            "total_time_spent": "0",
            "document_type": "Public",
            "notecontent": "",
            "id": "7x517",
            "isclosed": "0",
            "record_currency_symbol": null
        }
    ]
}

/update

When you intend to update specific fields of existing records, you can use this or Revise API. Note: This API expects all the mandatory fields to be re-stated as part of the element parameter.

POST Endpoint/update?element={"id":"record_id", "field1":"revalue1", "field2":"value2"}
ParameterRemarks
element{"id":"record_id", "field1":"revalue1", "field2":"value2"}
{
    "success": true,
    "result": {
        "productname": "test1",
        "productcode": "",
        "discontinued": "1",
        "product_type": "Solo",
        "productcategory": "",
        "vendor_id": "",
        "manufacturer": "",
        "sales_start_date": "",
        "sales_end_date": "",
        "start_date": "",
        "expiry_date": "",
        "serial_no": "",
        "mfr_part_no": "",
        "vendor_part_no": "",
        "website": "",
        "glacct": "",
        "productsheet": "",
        "createdtime": "2023-06-08 09:35:49",
        "modifiedtime": "2023-06-13 05:47:40",
        "product_no": "PRO15",
        "modifiedby": "",
        "created_user_id": "",
        "source": "WEBSERVICE",
        "starred": "0",
        "tags": "",
        "record_currency_id": "21x2",
        "record_conversion_rate": "1.00000",
        "item_barcode": "",
        "hsn_code": "",
        "unit_price": "0.00000000",
        "commissionrate": "",
        "taxclass": "",
        "purchase_cost": "0.00000000",
        "purchase_cost_currency_value": "",
        "billing_type": "One time",
        "usageunit": "",
        "qty_per_unit": "",
        "qtyinstock": "0.000",
        "reorderlevel": "0",
        "assigned_user_id": "19x1",
        "qtyindemand": "",
        "defect_qtyinstock": "0.000",
        "reorder_qty": "0.000",
        "availablestock": "0.000",
        "committedstock": "0.000",
        "incomingstock": "0.000",
        "imagename": "",
        "description": "",
        "id": "6x502",
        "isclosed": 0,
        "label": "test1",
        "url": "https://{{your_domain}}/view/detail?module=Products&id=502",
        "currency1": 0,
        "currency2": 0,
        "currency_id": "21x2"
    }
}

/revise

This is similar to Update API but relaxes the constraint of re-stating the mandatory fields but expects target fields that need to be updated.

POST Endpoint/revise?element={"id":"record_id", "field2":"revalue2"}
ParameterRemarks
element{"id":"record_id", "field2":"revalue2"}
{
    "success": true,
    "result": {
        "productname": "sample",
        "productcode": "",
        "discontinued": "1",
        "product_type": "Solo",
        "productcategory": "",
        "vendor_id": "",
        "manufacturer": "",
        "sales_start_date": "",
        "sales_end_date": "",
        "start_date": "",
        "expiry_date": "",
        "serial_no": "",
        "mfr_part_no": "",
        "vendor_part_no": "",
        "website": "",
        "glacct": "",
        "productsheet": "",
        "createdtime": "2023-06-08 09:35:49",
        "modifiedtime": "2023-06-13 05:47:56",
        "product_no": "PRO15",
        "modifiedby": "",
        "created_user_id": "",
        "source": "WEBSERVICE",
        "starred": "0",
        "tags": "",
        "record_currency_id": "21x2",
        "record_conversion_rate": "1.00000",
        "item_barcode": "",
        "hsn_code": "",
        "unit_price": "0.00000000",
        "commissionrate": "",
        "taxclass": "",
        "purchase_cost": "0.00000000",
        "purchase_cost_currency_value": "",
        "billing_type": "One time",
        "usageunit": "",
        "qty_per_unit": "",
        "qtyinstock": "0.000",
        "reorderlevel": "0",
        "assigned_user_id": "",
        "qtyindemand": "",
        "defect_qtyinstock": "0.000",
        "reorder_qty": "0.000",
        "availablestock": "0.000",
        "committedstock": "0.000",
        "incomingstock": "0.000",
        "imagename": "",
        "description": "",
        "id": "6x502",
        "isclosed": 0,
        "label": "sample",
        "url": "https://{{your_domain}}/view/detail?module=Products&id=502",
        "currency1": 0,
        "currency2": 0,
        "currency_id": "21x2"
    }
}

/sync

When you need to fetch records that changed their state from the last-known time, you can use this API.

GET Endpoint/sync?modifiedTime=timestamp&elementType=moduleName&syncType=sync_type
ParameterRemarks
modifiedTimeLast known modified time from where you expect state changes of records should be in UNIX timestamp. For example 1561718898
elementTypeTarget module name.
syncTypeuser: fetch records restricted to the assigned owner of the record.
userandgroup: fetch records restricted to the assigned owner of own’s group.
application: fetch records without restriction on the assigned owner.
{
    "success": true,
    "result": {
        "updated": [
            {
                "salutationtype": "",
                "firstname": "",
                "lastname": "Ram",
                "email": "",
                "phone": "",
                "mobile": "",
                "homephone": "",
                "birthday": "",
                "fax": "",
                "account_id": "",
                "title": "",
                "department": "",
                "contact_id": "",
                "leadsource": "",
                "secondaryemail": "",
                "assistant": "",
                "assigned_user_id": "",
                "assistantphone": "",
                "donotcall": "0",
                "notify_owner": "0",
                "emailoptout": "0",
                "createdtime": "2023-06-09 04:36:47",
                "modifiedtime": "2023-06-12 07:20:52",
                "contact_no": "CON24",
                "modifiedby": "",
                "isconvertedfromlead": "0",
                "created_user_id": "",
                "primary_twitter": "",
                "source": "WEBSERVICE",
                "engagement_score": "0",
                "last_contacted_on": "",
                "last_contacted_via": "",
                "slaid": "",
                "contacttype": "Lead",
                "contactstatus": "Cold",
                "happiness_rating": "",
                "record_currency_id": "",
                "record_conversion_rate": "",
                "profile_score": "0",
                "profile_rating": "0",
                "referred_by": "",
                "emailoptin": "singleoptinuser",
                "emailoptin_requestcount": "0",
                "emailoptin_lastrequestedon": "",
                "smsoptin": "singleoptinuser",
                "language": "",
                "primary_phone_field": "",
                "primary_email_field": "",
                "isclosed": "0",
                "source_campaign": "",
                "otherphone": "",
                "portal": "0",
                "support_start_date": "",
                "support_end_date": "",
                "mailingcountry": "",
                "othercountry": "",
                "mailingstreet": "",
                "otherstreet": "",
                "mailingpobox": "",
                "otherpobox": "",
                "mailingcity": "",
                "othercity": "",
                "mailingstate": "",
                "otherstate": "",
                "mailingzip": "",
                "otherzip": "",
                "mailing_gps_lat": "-360.0000000",
                "mailing_gps_lng": "-360.0000000",
                "description": "",
                "imagename": "",
                "primary_linkedin": "",
                "followers_linkedin": "",
                "primary_facebook": "",
                "followers_facebook": "",
                "facebookid": "0",
                "instagramid": "",
                "twitterid": "",
                "id": "4x506",
                "_module": "Contacts"
            },
            {
                "salutationtype": "",
                "firstname": "Shri",
                "lastname": "Ram",
                "email": "ram12@gmail.com",
                "phone": "+911234567890",
                "mobile": "",
                "homephone": "",
                "birthday": "",
                "fax": "",
                "account_id": "",
                "title": "",
                "department": "",
                "contact_id": "",
                "leadsource": "",
                "secondaryemail": "",
                "assistant": "",
                "assigned_user_id": "",
                "assistantphone": "",
                "donotcall": "0",
                "notify_owner": "0",
                "emailoptout": "0",
                "createdtime": "2023-06-09 05:31:14",
                "modifiedtime": "2023-06-12 12:45:36",
                "contact_no": "CON25",
                "modifiedby": "",
                "isconvertedfromlead": "0",
                "created_user_id": "",
                "primary_twitter": "",
                "source": "WEBSERVICE",
                "engagement_score": "0",
                "last_contacted_on": "",
                "last_contacted_via": "",
                "slaid": "",
                "contacttype": "Lead",
                "contactstatus": "Cold",
                "happiness_rating": "",
                "record_currency_id": "",
                "record_conversion_rate": "",
                "profile_score": "0",
                "profile_rating": "0",
                "referred_by": "",
                "emailoptin": "none",
                "emailoptin_requestcount": "0",
                "emailoptin_lastrequestedon": "0000-00-00 00:00:00",
                "smsoptin": "singleoptinuser",
                "language": "",
                "primary_phone_field": "",
                "primary_email_field": "",
                "isclosed": "0",
                "source_campaign": "",
                "otherphone": "",
                "portal": "0",
                "support_start_date": "",
                "support_end_date": "",
                "mailingcountry": "",
                "othercountry": "",
                "mailingstreet": "",
                "otherstreet": "",
                "mailingpobox": "",
                "otherpobox": "",
                "mailingcity": "",
                "othercity": "",
                "mailingstate": "",
                "otherstate": "",
                "mailingzip": "",
                "otherzip": "",
                "mailing_gps_lat": "-360.0000000",
                "mailing_gps_lng": "-360.0000000",
                "description": "",
                "imagename": "",
                "primary_linkedin": "",
                "followers_linkedin": "",
                "primary_facebook": "",
                "followers_facebook": "",
                "facebookid": "0",
                "instagramid": "",
                "twitterid": "",
                "id": "4x515",
                "_module": "Contacts"
            }
        ],
        "deleted": [
            "4x503"
        ],
        "more": false,
        "lastModifiedTime": 1686573936
    }
}

/delete

Delete existing records through this API.

POST Endpoint/delete?id=record_Id
ParameterRemarks
idrecord_Id
{
    "success": true,
    "result": {
        "status": "successful"
    }
}

/relatedtypes

What relationship a module has with others can be obtained through this API.

GET Endpoint/relatedtypes?elementType=moduleName
ParameterRemarks
elementTypeTarget module name.
{
    "success": true,
    "result": {
        "types": [
            "Potentials",
            "Calendar",
            "Emails",
            "Quotes",
            "PurchaseOrder",
            "SalesOrder",
            "Products",
            "Calendar",
            "Documents",
            "Campaigns",
            "Invoice",
            "ServiceContracts",
            "Services",
            "Project",
            "Assets",
            "EmailCampaigns",
            "Vendors",
            "ModComments",
            "Cases",
            "Olark",
            "WorkOrders",
            "PhoneCalls",
            "Esign",
            "Webchat",
            "SMSNotifier"
        ],
        "information": {
            "18": {
                "name": "Potentials",
                "label": "Potentials",
                "translated_label": "Deals",
                "isEntity": "1",
                "relation_id": "18",
                "actions": "add"
            },
            "19": {
                "name": "Calendar",
                "label": "Activities",
                "translated_label": "Activities",
                "isEntity": "1",
                "relation_id": "19",
                "actions": "add"
            },
            "20": {
                "name": "Emails",
                "label": "Emails",
                "translated_label": "Emails",
                "isEntity": "1",
                "relation_id": "20",
                "actions": "add"
            },
            "22": {
                "name": "Quotes",
                "label": "Quotes",
                "translated_label": "Quotes",
                "isEntity": "1",
                "relation_id": "22",
                "actions": "add"
            },
            "23": {
                "name": "PurchaseOrder",
                "label": "Purchase Order",
                "translated_label": "Purchase Orders",
                "isEntity": "1",
                "relation_id": "23",
                "actions": "add"
            },
            "24": {
                "name": "SalesOrder",
                "label": "Sales Order",
                "translated_label": "Sales Orders",
                "isEntity": "1",
                "relation_id": "24",
                "actions": "add"
            },
            "25": {
                "name": "Products",
                "label": "Products",
                "translated_label": "Products",
                "isEntity": "1",
                "relation_id": "25",
                "actions": "select"
            },
            "26": {
                "name": "Calendar",
                "label": "Activity History",
                "translated_label": "Activity History",
                "isEntity": "1",
                "relation_id": "26",
                "actions": "add"
            },
            "27": {
                "name": "Documents",
                "label": "Documents",
                "translated_label": "Documents",
                "isEntity": "1",
                "relation_id": "27",
                "actions": "add,select"
            },
            "28": {
                "name": "Campaigns",
                "label": "Campaigns",
                "translated_label": "Campaigns",
                "isEntity": "1",
                "relation_id": "28",
                "actions": "select"
            },
            "29": {
                "name": "Invoice",
                "label": "Invoice",
                "translated_label": "Invoices",
                "isEntity": "1",
                "relation_id": "29",
                "actions": "add"
            },
            "91": {
                "name": "ServiceContracts",
                "label": "Service Contracts",
                "translated_label": "Service Contracts",
                "isEntity": "1",
                "relation_id": "91",
                "actions": "ADD"
            },
            "106": {
                "name": "Services",
                "label": "Services",
                "translated_label": "Services",
                "isEntity": "1",
                "relation_id": "106",
                "actions": "SELECT"
            },
            "123": {
                "name": "Project",
                "label": "Projects",
                "translated_label": "Projects",
                "isEntity": "1",
                "relation_id": "123",
                "actions": "add"
            },
            "373": {
                "name": "Assets",
                "label": "Assets",
                "translated_label": "Assets",
                "isEntity": "1",
                "relation_id": "373",
                "actions": "ADD"
            },
            "404": {
                "name": "EmailCampaigns",
                "label": "List and Campaigns",
                "translated_label": "List and Campaigns",
                "isEntity": "1",
                "relation_id": "404",
                "actions": "ADD"
            },
            "474": {
                "name": "Vendors",
                "label": "Vendors",
                "translated_label": "Vendors",
                "isEntity": "1",
                "relation_id": "474",
                "actions": "SELECT"
            },
            "565": {
                "name": "ModComments",
                "label": "ModComments",
                "translated_label": "Comments",
                "isEntity": "1",
                "relation_id": "565",
                "actions": "ADD"
            },
            "584": {
                "name": "Cases",
                "label": "Cases",
                "translated_label": "Cases",
                "isEntity": "1",
                "relation_id": "584",
                "actions": "ADD"
            },
            "593": {
                "name": "Olark",
                "label": "Olark",
                "translated_label": "Olark Chats",
                "isEntity": "1",
                "relation_id": "593",
                "actions": ""
            },
            "1021": {
                "name": "WorkOrders",
                "label": "WorkOrders",
                "translated_label": "Work Orders",
                "isEntity": "1",
                "relation_id": "1021",
                "actions": "ADD"
            },
            "2420": {
                "name": "PhoneCalls",
                "label": "Phone Calls",
                "translated_label": "Phone Calls",
                "isEntity": "1",
                "relation_id": "2420",
                "actions": ""
            },
            "2882": {
                "name": "Esign",
                "label": "Esign",
                "translated_label": "Esign Documents",
                "isEntity": "1",
                "relation_id": "2882",
                "actions": "ADD,SELECT"
            },
            "4370": {
                "name": "Webchat",
                "label": "Webchat",
                "translated_label": "Live Chats",
                "isEntity": "1",
                "relation_id": "4370",
                "actions": ""
            },
            "6569": {
                "name": "SMSNotifier",
                "label": "SMSNotifier",
                "translated_label": "SMS Messages",
                "isEntity": "1",
                "relation_id": "6569",
                "actions": " "
            }
        }
    }
}

/reopen

Reopen closed record if permitted.

POST Endpoint/reopen
Body - urlencodedRemarks
idrecord_Id
{
    "success": true,
    "result": {
        "message": "Record reopened successfully."
    }
}

When you are looking to break the existing relationship between two records, you can use this API.

POST Endpoint/delete_related
Body - urlencodedRemarks
sourceRecordIdrecord_id
relatedRecordIdtarget_record_id
{
    "success": true,
    "result": {
        "message": "successful"
    }
}

/tags_add

Add tags to the target record.

POST Endpoint/tags_add
Body - urlencodedRemarks
idrecord_Id
tags["tag1", "tag2"]
{
    "success": true,
    "result": {
        "message": "tags added"
    }
}

/tags_retrieve

Fetch tags applied on target record.

GET Endpoint/tags_retrieve?id=record_Id
ParameterRemarks
idrecord_Id
{
    "success": true,
    "result": {
        "tags": []
    }
}

/tags_delete

Drop tag(s) applied on the target record or across all records.

POST Endpoint/tags_delete
Body - urlencodedRemarks
idrecord_Id
tags["tag"]
delete_allboolean
{
    "success": true,
    "result": {
        "message": "tags deleted"
    }
}

/get_account_hierarchy

Accounts can be linked to parent Accounts and hence form a hierarchy.

GET Endpoint/get_account_hierarchy?id=record_Id
ParameterRemarks
idrecord_Id
{
    "success": true,
    "result": [
        {
            "id": "3x539",
            "name": "TATA",
            "level": 1,
            "label": "Parent Org",
            "current": "false"
        },
        {
            "id": "3x540",
            "name": "tcs",
            "level": 2,
            "label": "Org",
            "current": "true"
        }
    ]
}

/lookup

This API enables you to search records with a phone or email id in different modules and fields.

GET Endpoint/lookup?type=phone/email&value=xxx&searchIn=["moduleName"]
ParameterRemarks
typephone/email
valuexxx
searchIn["moduleName"]
{
    "success": true,
    "result": [
        {
            "salutationtype": "",
            "firstname": "Shri",
            "lastname": "Ram",
            "email": "ram12@gmail.com",
            "phone": "+911234567890",
            "mobile": "",
            "homephone": "",
            "birthday": "",
            "fax": "",
            "account_id": "",
            "title": "",
            "department": "",
            "contact_id": "",
            "leadsource": "",
            "secondaryemail": "",
            "assistant": "",
            "assigned_user_id": "",
            "assistantphone": "",
            "donotcall": "0",
            "notify_owner": "0",
            "emailoptout": "0",
            "createdtime": "2023-06-09 05:31:14",
            "modifiedtime": "2023-06-12 12:45:36",
            "contact_no": "CON25",
            "modifiedby": "",
            "isconvertedfromlead": "0",
            "created_user_id": "",
            "primary_twitter": "",
            "source": "WEBSERVICE",
            "engagement_score": "0",
            "last_contacted_on": "",
            "last_contacted_via": "",
            "slaid": "",
            "starred": "0",
            "tags": "",
            "contacttype": "Lead",
            "contactstatus": "Cold",
            "happiness_rating": "",
            "record_currency_id": "",
            "record_conversion_rate": "",
            "profile_score": "0",
            "profile_rating": "0",
            "referred_by": "",
            "emailoptin": "none",
            "emailoptin_requestcount": "0",
            "emailoptin_lastrequestedon": "0000-00-00 00:00:00",
            "smsoptin": "singleoptinuser",
            "language": "",
            "primary_phone_field": "",
            "primary_email_field": "",
            "isclosed": 0,
            "source_campaign": "",
            "otherphone": "",
            "portal": "0",
            "support_start_date": "",
            "support_end_date": "",
            "mailingcountry": "",
            "othercountry": "",
            "mailingstreet": "",
            "otherstreet": "",
            "mailingpobox": "",
            "otherpobox": "",
            "mailingcity": "",
            "othercity": "",
            "mailingstate": "",
            "otherstate": "",
            "mailingzip": "",
            "otherzip": "",
            "mailing_gps_lat": "-360.0000000",
            "mailing_gps_lng": "-360.0000000",
            "description": "",
            "imagename": "",
            "primary_linkedin": "",
            "followers_linkedin": "",
            "primary_facebook": "",
            "followers_facebook": "",
            "facebookid": "0",
            "instagramid": "",
            "twitterid": "",
            "id": "4x515",
            "label": "Shri Ram",
            "url": "https://{{your_domain}}/view/detail?module=Contacts&id=515"
        }
    ]
}

/picklist_dependency

You can get dependency between two picklist fields.

GET Endpoint/picklist_dependency?module=moduleName&sourcefield=sourceFieldName&targetfield=targetFieldName
ParameterRemarks
modulemoduleName
sourcefieldsourceFieldName
targetfieldtargetFieldName
{
    "success": true,
    "result": {
        "sourcefield": "contacttype",
        "targetfield": "contactstatus",
        "valuemapping": [
            {
                "sourcevalue": "Lead",
                "targetvalues": [
                    "Cold",
                    "Warm",
                    "Hot",
                    "NLWC",
                    "Inactive"
                ]
            },
            {
                "sourcevalue": "Sales Qualified Lead",
                "targetvalues": [
                    "Accepted",
                    "Lost",
                    "Rejected"
                ]
            },
            {
                "sourcevalue": "Customer",
                "targetvalues": [
                    "Active",
                    "NLWC",
                    "Inactive"
                ]
            },
            {
                "sourcevalue": "Partner",
                "targetvalues": [
                    "Active",
                    "NLWC",
                    "Inactive"
                ]
            },
            {
                "sourcevalue": "Analyst",
                "targetvalues": [
                    "Positive",
                    "Neutral",
                    "Negative",
                    "NLWC",
                    "Inactive"
                ]
            },
            {
                "sourcevalue": "Competitor",
                "targetvalues": [
                    ""
                ]
            },
            {
                "sourcevalue": "Vendor",
                "targetvalues": [
                    "Active",
                    "NLWC",
                    "Inactive"
                ]
            },
            {
                "sourcevalue": "Marketing Qualified Lead",
                "targetvalues": [
                    "Not reached",
                    "Reached",
                    "Interested"
                ]
            }
        ]
    }
}

/convertlead

Use this API to achieve lead conversion.

POST Endpoint/convertlead
Body - urlencodedRemarks
element{"leadId":"2x3072","entities":{"Contacts":{"create":true},"Accounts":{"create":true},"Potentials":{"create":true}}}

Establish a relationship between the two records.

POST Endpoint/add_related
Body - urlencodedRemarks
sourceRecordIdrecord_id
relatedRecordIdtarget_record_id
relationIdLabeltarget_relation_label
{
    "success": true,
    "result": {
        "message": "successful"
    }
}

When you need related records of a target, record this API to go with.

GET Endpoint/retrieve_related?id=record_Id&relatedLabel=target_relationship_label&relatedType=target_moduleName
ParameterRemarks
idrecord_Id
relatedLabeltarget_relationship_label
relatedTypetarget_moduleName
{
    "success": true,
    "result": [
        {
            "productname": "sample",
            "productcode": "",
            "discontinued": "1",
            "product_type": "Solo",
            "productcategory": "",
            "vendor_id": "",
            "manufacturer": "",
            "sales_start_date": "",
            "sales_end_date": "",
            "start_date": "",
            "expiry_date": "",
            "serial_no": "",
            "mfr_part_no": "",
            "vendor_part_no": "",
            "website": "",
            "glacct": "",
            "productsheet": "",
            "createdtime": "2023-06-08 09:35:49",
            "modifiedtime": "2023-06-13 05:48:06",
            "product_no": "PRO15",
            "modifiedby": "",
            "created_user_id": "",
            "source": "WEBSERVICE",
            "starred": "0",
            "tags": "",
            "record_currency_id": "21x2",
            "record_conversion_rate": "1.00000",
            "item_barcode": "",
            "hsn_code": "",
            "unit_price": "0.00000000",
            "commissionrate": "",
            "taxclass": "",
            "purchase_cost": "0.00000000",
            "purchase_cost_currency_value": "",
            "billing_type": "One time",
            "usageunit": "",
            "qty_per_unit": "",
            "qtyinstock": "0.000",
            "reorderlevel": "0",
            "assigned_user_id": "",
            "qtyindemand": "",
            "defect_qtyinstock": "0.000",
            "reorder_qty": "0.000",
            "availablestock": "0.000",
            "committedstock": "0.000",
            "incomingstock": "0.000",
            "imagename": "",
            "description": "",
            "id": "6x502",
            "isclosed": "0",
            "record_currency_symbol": "₹"
        }
    ]
}

Fetch related records matching a search criteria using this API.

GET Endpoint/query_related?query=query_string&id=record_id&relatedLabel=target_moduleName
ParameterRemarks
queryquery_string
idrecord_id
relatedLabeltarget_moduleName
{
    "success": true,
    "result": [
        {
            "notes_title": "file upload",
            "document_source": "Vtiger",
            "assigned_user_id": "",
            "note_no": "DOC13",
            "folderid": "22x1",
            "createdtime": "2023-06-09 05:59:50",
            "modifiedtime": "2023-06-09 05:59:50",
            "modifiedby": "",
            "created_user_id": "",
            "source": "WEBSERVICE",
            "starred": "0",
            "tags": "",
            "record_currency_id": "",
            "record_conversion_rate": "",
            "filelocationtype": "I",
            "filestatus": "1",
            "filename": "Test1.odt",
            "filesize": "13104",
            "filetype": "application/vnd.oasis.opendocument.text",
            "fileversion": "",
            "filedownloadcount": "18",
            "share_count": "0",
            "email_open_count": "0",
            "open_count": "0",
            "shared_download_count": "0",
            "reshares_count": "0",
            "unique_open_count": "0",
            "deal_conversion_rate": "0",
            "avg_time_spent": "0",
            "total_time_spent": "0",
            "document_type": "Public",
            "notecontent": "",
            "id": "7x519",
            "isclosed": "0",
            "imageattachmentids": "7x520",
            "record_currency_symbol": null
        },
        {
            "notes_title": "Sample_file_1",
            "document_source": "Vtiger",
            "assigned_user_id": "",
            "note_no": "DOC14",
            "folderid": "22x1",
            "createdtime": "2023-06-09 06:11:18",
            "modifiedtime": "2023-06-09 06:11:18",
            "modifiedby": "",
            "created_user_id": "",
            "source": "WEBSERVICE",
            "starred": "0",
            "tags": "",
            "record_currency_id": "",
            "record_conversion_rate": "",
            "filelocationtype": "I",
            "filestatus": "1",
            "filename": "Sample_1.odt",
            "filesize": "22728",
            "filetype": "application/vnd.oasis.opendocument.text",
            "fileversion": "",
            "filedownloadcount": "18",
            "share_count": "0",
            "email_open_count": "0",
            "open_count": "0",
            "shared_download_count": "0",
            "reshares_count": "0",
            "unique_open_count": "0",
            "deal_conversion_rate": "0",
            "avg_time_spent": "0",
            "total_time_spent": "0",
            "document_type": "Public",
            "notecontent": "",
            "id": "7x521",
            "isclosed": "0",
            "imageattachmentids": "7x522",
            "record_currency_symbol": null
        }
    ]
}

/files_retrieve

This special API lets you pull the content of the linked image (Contacts, Products) that are not embedded as part of the record.

GET Endpoint/files_retrieve?id=resource_Id
ParameterRemarks
idresource_Id - You obtain this value through record retrieve (example: imageattachmentids field value of Contacts module record).
{
    "success": true,
    "result": [
        {
            "fileid": "522",
            "filename": "Sample_1.odt",
            "filetype": "application/vnd.oasis.opendocument.text",
            "filesize": 22728,
            "filecontents": "UEsDBBQAAAgAAC0xyVZexjIMJwAAACcAAAAIAAAAbWltZXR5cGVhcHBsaWNhdGlvbi92bmQub2FzaX ........."
        }
    ]
}

Customize

With VTAP tool you can customize CRM to your business need either through in-app customization or developing niche application tailored to your needs. Navigate ahead to explore the step-by-step examples.

CRM Look & Feel

You can change the look and feel of the CRM, change the color of the buttons, increase/decrease the font size, change the size of default icons etc. To change these styles in Vtiger you need to be familiar with css.

(i) Go to Module Designer and create new TAP Style by providing it with a name and optional module. In the example below lets build a new theme for Vtiger, let me call it a BrownTheme and apply for all the module.

(ii) Add the below styles for changing the primary button colors, we use bootstrap framework for layout. Save and publish to reflect the changes, go to Contacts page and refresh it see it in action.

.btn-primary, .btn-primary:active {
    background-color : #d2a07b !important;
    border : 1px solid #a99e90 !important;
}

(iii) To change the top header, body header and body content changing the background colors, like below. Save and Publish and refresh to see the changes.

#topbar {
    background-color : #c0a793 !important;
}
.vds-body, .vds-bodyfixedheader {
    background-color : #f5e6df !important;
}
.vds-bodycontents {
    background-color : #c0a793 !important;
}

(iv) Like wise we have added more changes in to reflect brown theme are different parts of the product. Below have added css content of the BrownTheme styles. Though this is not 100% ready but it is still give you an idea on how you can build more such themes.

.btn-primary, .btn-primary:active {
    background-color : #d2a07b !important;
    border : 1px solid #a99e90 !important;
}
button.btn-primary:hover, .bg-primary-active, 
    .filterTypes .filterTypesContainer:hover,
    .btn-primary:not(:disabled):not(.disabled).active, 
    .btn-primary:not(:disabled):not(.disabled):active {
        background-color : #c1a692 !important;
        border : 1px solid #a99e90 !important;
}
.border-primary {
    border-color : #c1a692 !important;
}
.active-shadow {
    box-shadow : 0 0 0 0.08rem rgb(210,160,123) !important;
}
#topbar {
    background-color : #c0a793 !important;
}
.vds-body, .vds-bodyfixedheader {
    background-color : #f5e6df !important;
}
.vds-bodycontents {
    background-color : #c0a793 !important;
}
#listview-table .resize-body tr:hover:not(.selectedRow) td, .selectedRow td {
    background-color : #ffd9c4 !important;   
}
#detailHeader {
    background-color : #e0dbd7 !important;
}
.detailBody, .bg-grey-hue-7 {
    background-color : #d6bcae !important;
}
.vt-checkbox-container input:checked ~.roundedCheckmark:not(.filter-checkmark), 
    .select2-container--default .select2-results__option--highlighted[aria-selected],
    .bg-blue-5 {
    background-color : #d2a07b !important;
}
.activeApp, .blockSideLabel.active {
    border-left : 4px solid #c1a692;
}
.form-control:focus {
    border-color : #c1a692 !important;
    box-shadow : 0 0 0 0.08rem rgb(210,160,123) !important;
}
.blockSideLabel.active, 
.dropdown-item:focus, 
.dropdown-item:hover, 
.listViewFilters .dropdown-item:hover{
    background-color : #f5e6df !important;
}

Final screens would like:

UI Component

Module Designer allows you to build 3 different types of components, Custom, List View, and Detail View components.You can navigate to Module Designer > Components to add the component you want.

Types Of Component :

  • Custom Components: These are independent of the existing components in Vtiger CRM and can be reused across different custom pages and other custom components. If you are unfamiliar with what a component is, its importance, and its structure, please review the this document.
  • List View Components: This component extends the main ListView component of Vtiger CRM, enabling you to add extra features to the existing ListView, according to your specific needs.
  • Detail View Components: This component extends the main DetailView component of Vtiger CRM, enabling you to add extra features to the existing DetailView, according to your specific needs.

Note : While custom components are designed for reuse across various custom pages and other components, these changes won't automatically apply to the CRM interface. To see your customizations reflected in the CRM, you must add the custom component to Module Designer > TAP Scripts and then save and publish your changes. This step ensures that your modifications are integrated into the CRM's workflow and are visible in the interface.

Custom Component

To add custom component, go to Module Designer and select any module and add Component under resources.

In the below example we will define a custom component that will show an icon inside the button. We will name it ButtonWithIcon and create it in Contacts module, you will see the IDE with default component name.

Example

Lets start writing the component, first is the name of the component with "ButtonWithIcon". Since we want to add this in Contacts module, the final name would be Contacts_Component_ButtonWithIcon.

Note: Please be sure to follow the syntax for naming the component i.e ModuleName_Componet_Componentname, where Component is a standard keyword and ComponentName is the one that you provided while creating the component.

We want to give label to the button and pass an icon name to display before the label. These will be provided by the parent component as attributes so we need to add them in props properties. For icons we will make use of font awesome classes.

props : ["label", "icon"] 

Finally we will add html which will be used to display in DOM to the template property. Notice the use of props in templates, if used to display as text then place them under curly braces, if passed to attribute then activate reactivity by binding with colon(:).

var Contacts_Component_ButtonWithIcon = VTAP.Component.Core.extend({ 

    //add props to get icon name and button label from parent component
    props : ['icon', 'label', 'varient'],

    //dynamic property defines button varient
    computed : {
        buttonClass() {
            if(this.varient == '') this.varient = 'primary';
            return 'btn btn-'+this.varient;
        }  
    },

    //button html
    template : 
        `<button :class="buttonClass">
            <i :class="icon"></i>
                {{label}}
            </button>`
});

Add_AdvancedSettings_ListPage.js

var Contacts_Component_AdvancedSettings = VTAP.Component.Core.extend({ 

    created() {
        VTAP.Component.Register('LIST_ADVANCED_SETTING',
                                {name:'Custom Settings',
                                clickHandler:() => { 
                                    alert('redirect to settings page'); 
                                }
                            });
    }
});

Add_Global_Action.js


var Contacts_Component_GlobalAction = VTAP.Component.Core.extend({ 
    created() {
        VTAP.Component.Register('GLOBAL_ACTION', 
                                {},
                                VTAP.Component.Load('CustomGlobalIcon','Contacts'));
    }
});

var Contacts_Component_CustomGlobalIcon = VTAP.Component.Core.extend({
    methods : {
            click(){
                    alert('click on global action icon');
            }
    },
    template:`<ispan class='btn p-2' @click=click()><ii class='fa fa-cloud'><i/i><i/span>`
});

Save and Publish the component and it will be available for any custom page to include.

See here how custom components are used in custom pages.

List View Component

Consider a scenario where you have developed a custom page for the Contacts module, you’ll have to type the entire url every time you want to access the custom page, but using the list view component you can create a LIST_BASIC_BUTTON which on click will redirect you to the custompage.

Navigate Module Designer -> Select Module -> Component -> List View

Add the code for creating the button which redirects to the custom page on clicking

var custom_Contacts_Component_ListView = Contacts_Component_ListView.extend({ 
     created() {
        VTAP.Component.Register('LIST_BASIC_BUTTON', {
            label: 'Custom Page',       
            clickHandler: () => {   
                window.location.href = "https://vti-shrravya-a-20240721120901.od1.vtiger.ws/view/custompage?module=Contacts";
            },
            icon: 'fa-check',        
            variant: 'primary'       
        });
    }
});

Save And Publish. Now go back and refresh you should be able to see, the custom page button will be added to your List View of Contacts Module which onclick will redirect you to your custom page.

Detail View Component

Consider an example where you want to create a record for Deals in the Detail View of Contacts Module. We can add a button to the Detail View which on click will open the quick create of the Deals Module. Navigate Module Designer -> Select Module -> Component -> Detail View

Add the code for creating a deals quick create button

var custom_Contacts_Component_DetailView = Vtiger_Component_DetailView.extend({ 
   created() {
        VTAP.Component.Register('DETAIL_BASIC_BUTTON', 
            {
                label: 'Deals', 
                icon: 'fa-pencil', 
                variant: 'primary', 
                clickHandler:() => { 
                    VTAP.Utility.ShowRecordCreate(this.record,'Potentials',()=>{});
                }
            }
        );
    }
});

Save And Publish. Now go back and refresh you should be able to see your Deals Quick Create Button in the detail view which on click will show a quick create of Deals.

Pages

VTAP gives you the ability to build custom pages from scratch, where you can display forms, lists, charts, widgets etc for any existing modules or custom modules. To create a new page go to Platform > Module Designer > Select module > Pages

Example : We will create a custom page and include custom component ButtonWithIcon we added in the previous section.

In Vtiger, components are the building blocks for any pages, widgets, charts, lists etc. So when a new page is created, by default page component is added and shown in IDE.

Lets create one page with name "Custompage" and save it. You will see a checkbox "Is admin page?", tick this if you want to create page for admin users only, this is generally used when you want to admins to configure for a feature or store login credentials to connect to any external apps.

To add a custom component you will use components property of the vue instance and load it by passing the name and module of the component where its defined. Then use the component button_icon in the template and pass appropriate details like icon and label as attributes.


var Contacts_Component_CustompageView = VTAP.Component.Core.extend({ 

    components : {
        'button_icon' : VTAP.Component.Load('ButtonWithIcon','Contacts')
    },

    template : 
        `<div class="vds-body">
                <div class="vds-bodyfixedheader container-fluid py-2 px-0 bg-grey-hue-8 px-4">
                    <h3>Example for custom components in Vtiger</h3>
                </div>
                <div class="vds-bodycontents px-4 m-5">
                    <div class='row'>
                        <div class='d-flex flex-column col-6 p-5'>
                            <div class='d-flex justify-content-center'>
                                <h3>Primary Button with Edit icon<h3>
                            </div>
                            <div class="d-flex justify-content-center">
                                <button_icon icon="fa fa-pencil" label="Edit" varient="primary"></button_icon>
                            </div>
                        </div>
                    </div>
                </div>
            </div>`
});

Lets add more buttons with different varient and icons.

var Contacts_Component_CustompageView = VTAP.Component.Core.extend({ 

    components : {
        'button_icon' : VTAP.Component.Load('ButtonWithIcon','Contacts')
    },

    template : 
            `<div class="vds-body">
                    <div class="vds-bodyfixedheader container-fluid py-2 px-0 bg-grey-hue-8 px-4">
                        <h3>Example for custom components in Vtiger</h3>
                    </div>
                    <div class="vds-bodycontents px-4 m-5">
                        <div class='row'>
                            <div class='d-flex flex-column col-6 p-5'>
                                <div class='d-flex justify-content-center'>
                                    <h3>Primary Button with Edit icon</h3>
                                </div>
                                <div class='d-flex justify-content-center'>
                                    <button_icon icon="fa fa-pencil" label="Edit" varient="primary"></button_icon>
                                </div>
                            </div>

                            <div class='d-flex flex-column col-6 p-5'>
                                <div class='d-flex justify-content-center'>
                                    <h3>Success Button with Download icon</h3>
                                </div>
                                <div class='d-flex justify-content-center'>
                                    <button_icon icon="fa fa-thumbs-up" label="Download" varient="success"></button_icon>
                                </div>
                            </div>
                        </div>

                        <div class='row'>
                            <div class='d-flex flex-column col-6 p-5'>
                                <div class='d-flex justify-content-center'>
                                    <h3>Secondary Button with Edit icon</h3>
                                </div>
                                <div class='d-flex justify-content-center'>
                                    <button_icon icon="fa fa-download" label="Edit" varient="secondary"></button_icon>
                                </div>
                            </div>

                            <div class='d-flex flex-column col-6 p-5'>
                                <div class='d-flex justify-content-center'>
                                    <h3>Danger Button with Delete icon</h3>
                                </div>
                                <div class='d-flex justify-content-center'>
                                    <button_icon icon="fa fa-archive" label="Delete" varient="danger"></button_icon>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>` 
});

The final page would look like below screen shot.

How to access Custom Page?

Any Custom page can be accessed with a URL. URL format is like below

$YOUR_INSTANCE_URL/view/$PAGE_NAME?module=$MODULE_NAME

$PAGE_NAME = Name of the page given while adding the page (all small letters) $MODULE_NAME = Name of the module in which page is added

Example : https://acme.odx.vtiger.com/view/Custompage?module=Contacts

If you are building admin page then the URL will change to https://acme.odx.vtiger.com/admin/view/Custompage?module=Contacts

UI Actions

The ui components feature enables us to modify or add custom buttons or icons. Which on click will perform actions.

Adding a component

  • VTAP has enabled new low code platform where you can just perform actions by writing onclick functions.
  • Click on the green box on top of the page. ref
  • Now you can see a popup. By adding the following details we can get started on ui components.
  • component name -> the name of the component which you want to use for this custom action.
  • component type ->What type if component it is. (Where this icon will sart showing in the UI).
  • module -> to whcih module it should show and work.
  • on clcik -> what is the action to be performed when this button is clicked.
  • button label -> name of the button to show
  • component icon -> the icon can be selected from the pre defined drop down.
  • color -> color of the button or the icon.

Different Component Types :

  • Detail View More Action Item
  • Detail View Basic Button
  • Detail View Action Icon
  • List View Add Record Type
  • List View Settings Action
  • List View Row Basic Action Icon
  • List View Row Secondary Action Icon
  • List View Mass Action Icon
  • Global Action Icon
  • List View Button

Examples

Detail View Basic Button - Add a button to delete the current record from header view.

VTAP.Api.Delete('records', {id:VTAP.Detail.Id(), module:VTAP.Detail.Module()}, (err, res) => {
}); 
  • First go to ui actions and create a new record.
  • Add module as contacts.
  • choose the name of the button and icon.
  • now go to on click block.
  • Add the above code. Here we will take the record id by using VTAP.Detail.Id() and similarliy for module.
  • You can add a notification on sucess to show the record is deleted.

List View Basic Button - Add a button to redirect to custom page.

window.location.href = "https://xxxxx.od2.vtiger.com/view/Page1?module=Potentials";
  • Go to Ui actions create a new actions.
  • Add any component name which suits your action.
  • Select the modules.
  • Select the button label and the icon.
  • Select color.
  • Now add the above code in onclick block.
  • On clciking that button from list view you will be redirected to the custom page or any website which is added.

Example 2 - Create a POPUP Modal using module designer and UI actions.

  1. First lets add the trigger which will open the popup, for example add a button in List page. You need to go to UI Actions inside Platforms > Module Designer.
  2. Add UI Component and select Component Type as "List View Button".
  3. Give a Label, icon, color if needed and then in On click you need to add below code.
// Popup is the name of the popup component defined in Contacts module
VTAP.Utility.ShowPopup({component:VTAP.Component.Load('Popup','Contacts')});
  1. Go to module designer select Contacts module to create a popup component.
  2. Click on components and create a component by selecting custom in the options with name. ex- Popup.
  3. Add the below code in the module designer.
  4. Here MYMODULE should be replaced by the current module name(eg:Contacts) where the tap script is created.
  5. Save and publish the script to see the popup in action.

    var MYMODULE_Component_Popup = VTAP.Component.Core.extend({
        data() {
        //store popup data to make use of them to display and to send it to server
        return {
            name:"",
            email:""
        }
    },
    methods: {
        onok(event) {
            //prevent popup to close
            event.preventDefault();

             //add post action on click on ok button
            VTAP.Api.Post("records", {module:'Contacts',"lastname":this.name,"email":this.email}, (error,response) => {
                    if(response) {
                        VTAP.Utility.ShowSuccessNotification("Saved successfully");
                        this.hidePopup();
                    } else {
                        VTAP.Utility.ShowErrorNotification(error);
                    }
             });
        },
        hidePopup() {
            //bv::hide::modal is the event, modal-1 is the modal popup id which needs to be closed.
            this.$root.$emit('bv::hide::modal', 'modal-1')
        }
    },
    //:no-close-on-backdrop="true" is to ensure the popup is not closed when clicked outside the modal
    template: `<b-modal  id="modal-1" title="Save Contact" @ok=onok  :ok-title="Save" :no-close-on-backdrop="true">
                    <p class="my-4"> 
                        <div class="container" >
                             <input v-model="name" class="form-control w-50 m-2" placeholder="Enter the name">
                             <input v-model="email" class="form-control w-50 m-2" placeholder="Enter the email address">
                        </div>    
                    </p> 
                </b-modal>`
});

CRM Hooks

CRM hooks in Vtiger are customizable points in the user interface where you can add custom UI actions like buttons, icons, links, widgets in Vtiger list and detail pages. Using these you can make Vtiger UI more interactive, customizable and cater to specific user and business needs.

Following are the placeholder hooks available in Vtiger UI

List Page hooks

List View Button
List Mass Action Button
List Settings (only for Admin users)
List Custom Views (Kanban view, Calendar view, or List of records view)
List Row Basic Action
List Row Secondary Action
Global Action

Detail Page hooks

Detail One View Widget
Detail View Button
Detail Header Widget
Detail Summary Widget
Detail More Action Icon
Detail More Action Activity Item
Detail Related Record Action

How to add custom button in List page?

Go to Menu, under Platform App select Module Designer. You will see the below page. Once there, you will want to select the module you wish to add the button to from the dropdown menu in the top left-hand corner.

Next, a pop-up window will appear. Add a name for the Tap Script, example "ListExample" and, once again, select the module. Here you can also select module where you want the button to appear,or leave the target module blank to let button appear in all modules.

Click ‘Save’ and you should now see a screen where you can begin coding.

var Contacts_Component_ListExample = VTAP.Component.Core.extend({ 
    //created function is called 
    created() {
        VTAP.Component.Register('LIST_BASIC_BUTTON', 
                                {}, 
                                VTAP.Component.Load("BasicButton","Contacts"),
                                {module : 'Contacts'}
                        );
    }
});

var Contacts_Component_BasicButton = VTAP.Component.Core.extend({
    methods : {
        click() {
            alert('Hooray!! my first button');
        }
    },
    template : `Custom button`
});

Please note that the name of script given and the name of the component should match, which in this example is "ListExample". Every button is wrapped in a Vtiger Component, to understand more about component and its life cycle click here.

Please check here for more examples on other hooks.

Custom API

API Designer allows application developers to build custom web service APIs. You can define your own custom service endpoints and query parameters. These APIs can be used to interact with CRM data, integrate with external apps and also expose these data to VTAP Javascript APIs to be used within custom pages or widgets in Vtiger UI.

API Designer lets you add two types of APIs.

  • REST APIs
  • Incoming Webhook

REST APIs provides you the ability to access CRM data through HTTP/HTTPS protocol. You can perform create, update, retrieve or delete operations on any CRM record information. Apart from this it will also allows you to store extension specific data, user specific configuration data, or connect to external applications using the stored extension specific data. All these data can be accessed using basic authorization passing username and accesskey.

Incoming Webhook allows any external applications to post data to CRM, these are subsets of REST APIs but they do come with additional security. Each incoming webhook needs to be accessed using unique security token which can be configured when create webhook api. It also provides you to control accesses to webhook coming from specific IP addresses.

APIs can be created using VADL, this is loosely based on XML syntax to enable administrators or developers to be able to define API without needing deep programming skills. This is essential to create any custom APIs, good understanding on it will help in creating APIs quickly.

REST URI format

REST APIs follows standard REST API protocol, and each customer can define their endpoints.

For REST API endpoints, it follows the below format: https://instance.odx.vtiger.com/restapi/vtap/api/YOUR_CUSTOM_API_PATH

For Incoming webhook end point, the format is: https://instance.odx.vtiger.com/restapi/vtap/webhook/YOUR_CUSTOM_WEBHOOK_PATH

Supported HTTP request method

For REST API end points, below HTTP request methods are supported:

  • GET
  • POST
  • PUT
  • DELETE

For Incoming Webhook, below HTTP request methods are supported:

  • GET
  • POST

How to create REST APIs?

From the main menu, access Platform > API Designer module, you will see Add API button, clicking on gives two options.

##Build REST APIs to access CRM data

To access CRM data we have enabled read, write, update and delete operations to CRM records. Every API is identified with its name and it has to be unique since it is used in the endpoints.

In the below screen, to create API you need to select module, unique name(only characters and numbers) and checkmark it to enable.

Read data from CRM

After you add API, you will see in-built IDE where you need to use VADL to define APIs. For retrieving CRM data we will use select xml node, below is an example with api name "top_organizations" to show top 5 Organizations(internally called as "Accounts") with highest annual revenue. You will see select node used here with module attribute

<?xml version="1.0"?>
<api method="get">
    <select module="Accounts">
        <record>
            <field name="accountname"></field>
            <field name="annual_revenue"></field>
        </record>
        <sort>
            <field name="annual_revenue" order="descending"></field>
        </sort>
        <limit max="5" page="@page"></limit>
    </select>
</api>

Below is the list of VADL nodes that can be used for select.

NameDescriptionExample
selectThis will retrieve data from the crm identified with module attribute. This will define other how the data will be read, conditions, sorting and number of records.<select module='Contacts'></select>
recordThis will have all the fields which needs to be retrieved and is placed inside select node.<record><field name="firstname"></field><field name="lastname"></field></record>
whereUse this when you want to filer the records, you can pass multiple fields. Use glue attribute(OR, AND) to decide if all or any fields should match the conditions.<where glue="OR"><field name="firstname" value="@first"></field><field name="lastname" value="@last" condition="like"></field></record>
sortThis will have fields that will sort the data, with order attribute defining descending or ascending.<sort><field name="annual_revenue" order="descending"></field></sort>
limitBy default we return 100 records, but you can pass max attribute to limit records. If there are more records, you can pass page parameter in the request get specific page records.<limit max="5" page="@page"></limit>

Conditions

Where clause supports different type of conditions and their details are:

ConditionDescription
eqEquals (=), defaults if not passed
neqNot Equals (!=)
gtGreater Than (>)
ltLess Than (<)
gteGreater Than OR Equal To (>=)
lteLess Than OR Equal To (>=)
inSelect in Multiple VALUES (IN)
likeLike (%VALUE%)

You can access this API from external applications through exposed endpoints, in the above example you can access it below endpoint. https://instance.odx.vtiger.com/restapi/vtap/api/top_organizations.

Each of these endpoints expects username and accesskey to be passed in the request headers as basic authorization. Postman screenshot of the above API is shown below.

All custom REST API's built in API Designer can also be accessed using VTAP Javascript APIs, these data can be used to show as a list in custom page or use it for a bar chart etc.

VTAP.CustomAPI.Get("get_organization", {}, (error, response) => {
    if(response) {
        //use response to show in the list or in the widget.
    }
}); 

Read data using value-binding

Consider a case when you want to expose an API which wants to know Contact's first name and last name based on email address. Here email address is dynamic value, identified with '@' in the value attribute which will be passed as part of request parameter. To achieve I have created an API with name "search_contacts_with_email" this we will write xml as below.

<?xml version="1.0"?>
<api method="get">
    <select module="Contacts">
        <record> 
            <field name="firstname"> </field>
            <field name="lastname"> </field>
            <field name="id"> </field>
            <field name="department"> </field>
        </record>
        <where glue="OR">
            <field name="email" condition="eq" value="@emailaddress"> </field>   
            <field name="phone" condition="eq" value="@phone"> </field>   
        </where>
    </select>
</api>

Note : notice the glue attribute in where node, it is used to check if one of the condition in the where clause will match.

Accessing the same API via postman by passing emailaddress in the request parameter.

Create records in CRM

To create record you need to use create node, with method attribute as "post" in api node. Lets create a Contact record and link it to an existing organization record. For account_id field you can see it will first search company and if not found then it will create it.

<?xml version="1.0" ?>
<api method="post">
    <create module="Contacts">
        <record>
            <field name="firstname" value="@firstname"> </field>
            <field name="lastname" value="@lastname"> </field>
            <field name="email" value="@email"> </field>
            <field name="contacttype" value="@type" default="Lead" presence="optional"> </field>
            <field name="account_id" module="Accounts">
                <select>
                    <where>
                        <field name="accountname" value="@company"></field>
                    </where>
                </select>
                <create>
                    <record>
                        <field name="accountname" value="@company"></field>
                    </record>
                </create>
            </field>
        </record>
        <return>
            <field name="id"> </field>
        </return>
    </create>
</api>

Get Deals or Multiple Field Conditions in the Where Clause

Retrieve all deals (Potentials) associated with a specific contact or account, based on either the contact's mobile number or the account's phone number, ensuring that records are returned if either condition is met.

<?xml version="1.0"?>
<api method="get">
    <select module="Potentials">
        <record>
            <field name="potentialname"></field>
            <field name="amount"></field>
            <field name="assigned_user_id"></field>
            <field name="createdtime"></field>
        </record>
        <where glue="OR">
               <field name="contact_id" module="Contacts">
                   <select>
                       <where>
                           <field name="mobile" condition="eq" value="@mobile" presence="optional"></field>
                       </where>
                    </select>
               </field>
               <field name="related_to" module="Accounts">
                    <select>
                         <where>
                             <field name="phone" condition="eq" value="@phone" presence="optional"></field>
                         </where>
                    </select>
                </field>
            </where>
    </select>
</api>

Fetches case records along with the contact's first name, last name, and mobile number, as well as the assigned user's full name and the group's name. This reduces the number of API calls needed to gather related information, streamlining your workflow.

<?xml version="1.0"?>
<api method="get">
    <select module="Cases">
        <record>
            <field name="title"></field>
            <field name="casestatus"></field>
            <field name="assigned_user_id"></field>
            <field name="createdtime"></field>
            <field name="case_no"></field>
            <field name="assigned_user_id" module="Users">
                <select>
                    <field name="first_name"></field>
                    <field name="last_name"></field>
                    <field name="userlabel"></field>
                </select>
            </field>
            <field name="contact_id" module="Contacts">
                <select>
                    <field name="firstname"></field>
                    <field name="lastname"></field>
                    <field name="mobile"></field>
                </select>
            </field>
            <field name="group_id" module="Groups">
                <select>
                    <field name="groupname"></field>
                </select>
            </field>
        </record>
        <where>
            <field name="contact_id" module="Contacts">
                <select>
                    <where>
                        <field name="lastname" value="@name"></field>
                    </where>
                </select>
            </field>
        </where>
    </select>
</api>

Bulk Create

Bulk create can be done using the key bulk.

<?xml version="1.0"?> 
<api method="post"> 
    <create module="Potentials" bulk="true" data='@data'>
        <record> 
            <field name="potentialname" value="@dealName"></field> 
            <field name="closingdate" value="22-06-2024"></field>
        </record>
    </create>
</api>

You can create a record and also link it parent record using link node. In the below example you will see label in link node, this is the relationship name(label you see in related list in summary page) between newly created and parent record. If you are not sure then you can leave it skip mentioning it.

Here we are creating Documents record and relating to a Contact with an email address.

<?xml version="1.0"?>
<api method="post">
    <create module="Documents">
        <record>
            <field name="notes_title" value="@notes_title"></field>
            <field name="notecontent" value="@notecontent" presence="optional"></field>
            <field name="filelocationtype" value="I"></field>
            <field name="fileversion" value="@fileversion" presence="optional"></field> 
            <field name="filestatus" value="1"></field>
            <field name="folderid" value="22x1"></field>
            <field name="document_source" value="Vtiger"></field>
            <field name="document_type" value="Public"></field>
        </record>

        <link label="Documents">
            <select module="Contacts">
                <where>
                    <field name="email" value="@emailaddress"></field>     
                </where>
            </select>
        </link>

        <return>
            <field name="id"></field>
        </return>
    </create>    
</api>

Update Record

To replace email field value with incoming parameter (new_email) on all Contacts record matching incoming parameter (old_email) and respond back with record ID of changed records.

<?xml version="1.0" ?>
<api method="put">
    <update module="Contacts">
        <record>
            <field name="email" value="@new_email"></field>
        </record>
        <where>
            <field name="email" condition="eq" value="@old_email"></field>
        </where>
        <return>
            <field name="id"></field>
        </return>
    </update>
</api>

Bulk Update

Bulk update can be done using the key bulk.

<?xml version="1.0" ?>
<api method="put">
    <update module="Contacts" bulk="true" data="@data">
        <record>
            <field name="firstname" value="@firstname"></field>
        </record>
        <where>
            <field name="lastname" condition="eq" value="@lastname"></field>
        </where>
        <return>
            <field name="url"></field>
        </return>
    </update>
</api>

Update if exists else create a record, upsert

You can update and create in one single api call using upsert node. It has 2 parts one is update and other create node inside upsert node. They follow the same conventions to create and update record, just like in the previous examples.

<?xml version="1.0"?>
<api method="post" module="Contacts">
    <upsert>
        <where>
            <field name="id" condition="eq" value="@CRMID"></field>  
        </where>
        <sort>
            <field name="id" order="desc"></field>
        </sort>
        <limit max="1"></limit>
        <update>
            <record>
                <field name="email" value="@email" presence="optional"></field>
                <field name="cardnumber" value="@card" presence="optional"></field>
            </record>
        </update>
        <create>
            <record>
                <field name="firstname" value="@firstname" presence="optional"></field>
                <field name="lastname" value="@lastname" presence="optional"></field>
            <field name="email" value="@email" presence="optional"></field>
                <field name="account_id" module="Accounts" presence="optional">
                    <select>
                        <where>
                            <field name="website" value="@website"></field>
                        </where>
                        <sort>
                            <field name="domain" order="descending"></field>
                        </sort>
                    </select>
                </field>
            </record>
        </create>
        <return>
            <field name="id"></field>
            <field name="lastname"></field>
            <field name="account_id"></field>
        </return>
    </upsert>
</api>

Bulk Upsert

Bulk is supported in upsert. It can be used by key word bulk="true" and data="@data"

<?xml version="1.0"?>
   <api method="post" module="Contacts">
        <upsert bulk="true">
             <where>
                <field name="mobile" condition="eq" value="@mobile"></field>  
            </where>
            <update>
               <record>
                   <field name="mobile" value="@mobile"></field>
                   <field name="email" value="@email" presence="optional"></field>
                   <field name="lastname" value="@lastname" presence="optional"></field>
               </record>
            </update>
            <create module="Contacts" bulk="true">
                <record>
                   <field name="mobile" value="@mobile"></field>
                   <field name="email" value="@email" presence="optional"></field>
                   <field name="lastname" value="@lastname" presence="optional"></field>
               </record>
            </create>
            <return>
                <field name="mobile"></field>
                <field name="email"></field>
                <field name="lastname"></field>
            </return>
        </upsert>
    </api>

Update a record and add a comment/mention

You can add a mention when you perform create, update and upsert operations. Lets see an example to add comment/mention to Contacts Module.

<?xml version="1.0" ?>
<api method="POST">
    <update>
        <record>
            <field name="firstname" value="@firstname" presence="optional"></field>
        </record>      
        <where>
            <field name="email" value="@email"></field>
        </where>
        
        <mention>
            <template>
                <![CDATA[
                    <br>
                    <table>
                        <tr>
                            <td>Last name: %s</td></tr>
                        <tr>
                            <td>Email ID: %s</td>
                            </tr>
                    </table>
                ]]>                
            </template>
            <values>
                <value name="lastname"></value>
                <value name="email"></value>
            </values>
            <!--<text>-->
            <!--    //either use template or text, one of them -->
            <!--</text> -->
        </mention>
         <return>
            <field name="id"></field>
            <field name="assigned_user_id"></field>
            <field name="lastname"></field>
        </return>
        </update>
</api>

List below different mention node definitions:

NameDescriptionExample
mentionwrapper node to define the syntax for mentions/comments<mention></mention>
textthis contains text message<mention><text>testing comments</text></mention>
templatethis contains template message where placeholder %s is to be used to merge values<mention><template>A Lead is created with email address: %s</template></mention>
valuesif template is used then %s are replaced with names present in values node<values><value name="email"></value></values>

Delete records

You can delete multiple records at once, by defining the filter conditions.

<?xml version="1.0"?>
<api method="delete">
    <delete module="Contacts">
        <where>
                <field name="contacttype" value="@type"></field>
        </where>
    </delete>
</api>

Create multiple records in one API call

You can combine and create mulitple records in one API call, for example you can create a Deal(Potential) record along with Contact and Organization(Accounts). If Contact and Organization record exists then link it to Deal record. Below is an example

<?xml version="1.0" ?>
<api method="post">
    <create module="Potentials">
        <record>
            <field name="potentialname" value="@potentialname"></field>
            <field name="closingdate" value="@closingdate"></field>
            <field name="pipeline" value="@pipeline"></field>
            <field name="sales_stage" value="@sales_stage"></field>
            <field name="amount" value="@amount"></field>
            <field name="assigned_user_id" value="19x1"></field>
            <field name="related_to" module="Accounts">
                <select>
                    <where>
                        <field name="accountname" value="@company"></field>
                    </where>
                </select>
                <create>
                    <record>
                        <field name="accountname" value="@company"></field>
                        <field name="assigned_user_id" value="19x1"></field>
                    </record>
                </create>
            </field>
            <field name="contact_id" module="Contacts">
                <select>
                    <where>
                        <field name="email" value="@email"></field>
                    </where>
                </select>
                <create>
                    <record>
                        <field name="firstname" value="@firstname"></field>
                        <field name="lastname" value="@lastname"></field>
                        <field name="email" value="@email"></field>
                        <field name="contacttype" value="@contacttype"></field>
                        <field name="assigned_user_id" value="19x1"></field>
                        <field name="related_to" module="Accounts">
                            <select>
                                <where>
                                    <field name="accountname" value="@company"></field>
                                </where>
                            </select>
                            <create>
                                <record>
                                    <field name="accountname" value="@company"></field>
                                    <field name="assigned_user_id" value="19x1"></field>
                                </record>
                            </create>
                        </field>
                    </record>
                </create>
            </field>
        </record>
        <return>
            <field name="id"> </field>
        </return>
    </create>
</api>

Create_Potentials_With_Account_And_Contacts


<?xml version="1.0"?>
<api method="post">
    <create module="Potentials">
        <record>
            <field name="potentialname" value="@potentialname"></field>
            <field name="closingdate" value="@closingdate"></field>
            <field name="pipeline" value="Standard"></field>
            <field name="related_to" module="Accounts">
                <select>
                    <where>
                        <field name="accountname" value="@company"></field>
                    </where>
                </select>
                <create>
                    <record>
                        <field name="accountname" value="@company"></field>
                    </record>
                </create>
            </field>
            <field name="contact_id" module="Contacts">
                <select>
                    <where>
                        <field name="email" value="@email"></field>
                    </where>
                </select>
                <create>
                    <record>
                        <field name="lastname" value="@lastname"></field>
                        <field name="contacttype" value="Lead"></field>
                        <field name="account_id" module="Accounts">
                            <select>
                                <where>
                                    <field name="accountname" value="@company"></field>
                                </where>
                            </select>
                        </field>
                    </record>
                </create>
            </field>
        </record>
    </create>
</api>

You can create a deal record with related records to accounts and conatcts in the same api. Using select and where nodes.

  • select will help in selecting the record where the given condition is satisfied.
  • if the record is not found a record is created using create node and then linked to the deal or potential

NOTE - All bulk operations have a limit of 50 records. Each record will be considered as a single request and seperate api call when calculating minute and daily limit.

Integration

In the REST API topic you saw how to build APIs to access CRM data, but in the real world an organization uses hundreds of applications for their employees and data is spread across them. Very often there is need to either sync the data with other applications or bring/send some parts of data to/from CRM.

Connect applications having REST APIs

Connecting to other applications that has REST APIs exposed has become easy with VTAP platform now, you can use API Designer, build an API, connect to them using Javascript APIs and display them on a widget inside Vtiger or post data to the other application. These outbound REST APIs allow you to create, update, read or delete data on an application that supports REST architecture. To build any REST APIs you need to use VADL which provides xml nodes, we have used them to build custom CRM data APIs.

Use cases for REST APIs

  • Connect to Slack and post a message to channels from CRM.
  • Perform email or phone validation when the entering the data in CRM.
  • Get latest exchange currency rates and apply them when creating Invoice or Quotes etc.
  • Show weather details of a Contact or Lead real time.
  • Enrich customer data using email address or company domain and many more.

Parts of REST API

Rest API compromises of below parts:

NameDescriptionVADL equivalentExample
EndpointThis is used to connect the application where the data resides. For dynamic parts of the url pass them as attributes in the url xml as shown in example 2.url
      1. <url>https://example.com/api/v1/get_contacts</url>
      2.<url SHEETID="@sheetid" OPTIONS="values/Sheet1!A1:append?valueInputOption=RAW">https://sheets.googleapis.com/v4/spreadsheets/$SHEETID/$OPTIONS</url>
MethodThis tells what is the type of request, like get, post, put or delete.Default is get type.method
Header parametersThere can be many headers which notifies the target app about the details of the request.headers<headers><header name="content-type" value="application/json"></header></headers>
Authentication parametersThis will tell which type of authentication are used, like basic auth or bearer token, or oauth.auth<auth><basic username="" password=""></basic></auth>
Query parametersAdditional data needed for the request endpoint to perform required action.parameters<parameters><parameter name="id" value="@id"></parameter></parameters>
Raw jsonsend parameters as jsonraw-post-data<parameters raw-post-data="true"><parameter name="id" value="@id"></parameter></parameters>

How to add REST API?

  • Go to main menu, under Platform you need to select API Designer.
  • Click on Add Api button, you will given two option to select, one is Rest Api and other Webhook.
  • Select Rest api, provide the name of the api which will be used later to make calls, link to the module and enable the status to active.

General structure of REST API

<?xml version="1.0"?>
<api method="post">
    <rest method="post">
        <url id="@id"> https://example/api/v1/get/$id </url>
        <headers>
            < header> name="" value=" "</headers>
        </headers>
        <auth>
            <basic username="" password=""> </basic>
        </auth>
        <parameters>
            <parameter name="id" value="@id"> </parameter>
        </parameters>
    </rest>
</api>

Examples of REST APIs

Send json POST request

<?xml version="1.0"?>
<api method="post">

    <rest method="post">
        <url>https://httpbin.org/anything</url>

        <headers>
            <header name="Content-Type" value="application/json"></header>
        </headers>

        <parameters raw-post-data="true">
            <parameter name='identifier' value='@identifier'></parameter>
            <parameter name='managerId' value='@managerId'></parameter>
            <parameter name='managerName' value='@managerName'></parameter>
        </parameters>
    </rest>
</api>

Build a weather app api to get the weather data.

<?xml version="1.0"?>
<api method="get">
    <rest method="get">
        <url>https://api.openweathermap.org/data/2.5/weather</url>
        <parameters>
            <parameter name="appid" value="@apiKey"></parameter>
            <parameter name="q" value="@city"></parameter>
            <parameter name="units" value="@unit"></parameter>
            <parameter name="lang" value="@lang"></parameter>
        </parameters>
    </rest>
</api>

Connect applications having OAuth support

We will add documentation on this soon, in the mean while you can watch this webinar video on our youtube channel.

Connect application having SOAP APIs

TBD

FAQs

I am getting error "unauthorized domain! Please whitelist https://xxx.xxx.xxx

To connect to any external application the adminstrator has to whitelist the domain, before you can start calling the API. For this you need to go to Apps > Platform > Api Designer, on the top right side we have provided Settings icon. Click on it to see Api Settings, here you can select the module for which you are building the API and add the external domain which can be used in building API.

Is it possible to make get/post type of request?

Yes, you can build GET, POST, PUT or DELETE type of request. When building api using VADL, inside api xml node you need to pass type attribute with one of the above values.

Webhooks

Incoming webhooks allows other applications to post the data into CRM when any event takes place in their system. These are special urls or endpoints created to allow easy integration with externals application. Each webhook consists of a unique endpoint, to which external applications can post parameters to create/update/delete data in the CRM.

Note : Having knowledge on how to build REST APIs and VADL is important.

When to use Incoming Webhooks?

  • You want to inform CRM about an event occurred in your application and want to log them in CRM.
  • For example, when a customer submits his contact data to Google forms. Instead of accessing the excel file to see the list you want to be available in CRM so your sales team can start following up with them.
  • You can let CRM know if the customer's bank confirms the payment or customer has raised a charge dispute in your stripe account.
  • You want to capture the survey details when the customer submits from Survey to your CRM.

Are these Incoming Webhooks safe?

The data posted using this mechanism follows secure and safe means to connect using a secret token that is provided during the webhook creation. These tokens are unique for each webhook that you create, and can be regenerated at any time to keep them more secure.

To increase the security we have also provided ways to restrict these webhooks from particular IP Addresses only. You can pass one or more IP Addresses to control the access to these webhooks.

How to create Incoming Webhooks?

Webhooks are created using VADL xml nodes, same format is used to create REST APIs as well.

Go to menu, under Platform app select the API Designer tool. Click on Add API button to see 2 options, one to create REST API and other to create Webhooks.

Here are steps to create webhooks.

  • Provide a name for the webhook, which will be used to idenfity and access them.
  • Select module for which you want to associate this webhook, and enable the status.

  • You will see a editor where you need to add VADL definition to define the webhook.
  • After defining the webhook, save and publish to make it available for external applications.
  • Before publishing, you will need to create secure token. This token can be created from the Security section.

  • You can enable this token either to be passed in header or part of the url.

  • If needed you can restrict access to the webhook from particular IP Addresses.

Structure of Incoming Webhook

<?xml version="1.0"?>
<api>
    <webhook>
        create or update or upsert records 
    </webhook>    
</api>

Examples

Integrate with Google forms

Assume you want to capture the google form data, where customer submits his contact details like phone number, email address and of course his name. We will create each submission as a Lead Contact record in CRM. Lets go through the steps that will help us achieve this integration with CRM.

  1. Create a Google form to capture Contact details, name, phone and other required information.
  2. Create a Incoming Webhook request in Vtiger using API Designer.
  3. Add webhook definition using VADL to create Contact.
  4. Generate secure token as header or parameter.
  5. Save and publish the Webhook.
  6. Enable Google forms to notify Vtiger when form is submitted using app script.
  7. Copy the webhook endpoint generated in Vtiger to Google forms script to post the data.

Lets go through each of these steps in detail:

First lets create a simple Google form having name, email address and phone number.

Now lets create webhook from API Designer and add the below definition. We will create a Contact record, map Vtiger fields with google form fields under record.

<?xml version="1.0"?>
<api>
    <webhook>
        <create module="Contacts">
            <record>
                <field name="lastname" value="@lastname"></field>
                <field name="firstname" value="@firstname"></field>
                <field name="email" value="@address"></field>
                <field name="phone" value="@phone"></field>
            </record>
            <return>
                <field name="id"></field>
            </return>
        </create>
    </webhook>    
</api>

Let me explain the content of the API below.

NameDescription
apiAll the definition are wrapped under this node.
webhookThis start the definition of the webhook.
CreateThis will create a record in vtiger, we need to pass module attribute with the name of the module which needs to be created.
recordThis is placeholder to define all the fields.
fieldThis defines the field with name attribute and with set the value of the field. If the value starts with @ means the value will be part of request parameters. You can define multiple fields under record.
returnThis defines what data has to be returned back to the webhook, this is optional and generally not needed for webhook as they are one direction only.

Before saving and publishing, we need to generate a secure token and decide where to pass it, for now I will select as parameter. It means the token has to be sent as part of webhook endpoint. Once done, it will generate endpoint which you can see under Documentation section on the right panel.

Now lets go back to Google form and configure app script which will be responsible to send the data to this webhook endpoint we generated.

Google form has to now submit data to our end point, this is done by adding a script that will invovke webhook endpoint. You can refer this article for more details on how to deploy and enable form submit triggerer.

That's it!! you are good to go and test it. Submit one Google form and enable listening in Playground section of the API on the right side panel. We have provided option to see the response of the form submission, IP address of the request, and other details.

You can click on start listening to see the sample response in the logs when form is submitted.

Here is a sample where you can see the response from the google form is captured below. And Vtiger is creating the record, and displaying the response if the api definition.

More webhook examples

Suppose you want to change the stage of a Deal based on some external trigger, like customer paid Invoice which is tracked in other system. In such case you can emit a webhook from that external system using the below api definition.


<?xml version="1.0"?>
<api method="post">
    <webhook>
        <update module="Potentials">
            <record>
                <field name="sales_stage" value="@stage"></field>
            </record>
            <where>
                <field name="contact_id" module="Contacts">
                    <select>
                        <where>
                            <field name="email" value="@email"></field>
                        </where>
                    </select>
                </field>
            </where>
            <return>
                <field name="id"></field>
            </return>
        </update>
    </webhook>
</api>

You have an external system that takes care of electronic signatures, once the customer signs it you want to keep a log of the signed document in the crm. In such case we can create a custom webhook and let external system call it with required details. In the below definition we will create a Document record with the signed document and link to a Contact record using the email address.

Note : You have to send a multipart/form-data request with signed document passed in the filename parameter.


<?xml version="1.0"?>
<api method="post">
    <webhook>
    <create module="Documents">
        <record>
            <field name="notes_title" value="@notes_title"></field>
            <field name="notecontent" value="@notecontent" presence="optional"></field>
            <field name="filelocationtype" value="I"></field>
            <field name="fileversion" value="@fileversion" presence="optional"></field> 
            <field name="filestatus" value="1"></field>
            <field name="folderid" value="22x1"></field>
            <field name="document_source" value="Vtiger"></field>
            <field name="document_type" value="Public"></field>
        </record>

        <link label="Documents">
            <select module="Contacts">
                <where>
                    <field name="email" value="@email"></field>     
                </where>
            </select>
        </link>

        <return>
            <field name="id"></field>
        </return>
    </create> 
    </webhook>
</api>

Details about some of the VADL xml nodes are defined below:

NameDescription
linkThis will related the record created with the parent record. Here label attribute is important to identify to which relation will be used.

Custom Apps

Platform provides App Creator tool which allows you to quickly build niche web application.

VCAP JS provides helper APIs to exchange data with CRM.

Published apps will be on Web and Mobile Apps for allowed users.

Read further to know way you can build such apps and publish it.

Flipbook

Lets us build a simple flipbook using App Creator, where you can flip the pages.

Create App

Navigate Main Menu > Platform > App Creator > Add App

Fill the Form And Save

Clicking on Flipbook App will open an editor where you can customise your App

views/index.html

Copy-and-paste the HTML code below which uses jQuery, turn.js library, which is used to create the flipping page effect, with VCAP and index.js runtime.

<!DOCTYPE html>
<html>
    <head>
        <title>VTAP Flipbook</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
    	<link rel="stylesheet" href="resources/index.css">
		<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js"></script>
		<script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/9729/turn.min.js"></script>
    </head>
    <body>
		<div><h1>Flipbook Example Using App Creator</h1></div>
        <div id="flipbook" class="sample-flipbook">
			<div class="hard">Vtiger Platform</div>
			<div class="hard"></div>
			<div> Builds Creative Apps </div>
			<div> to business much easily </div>
			<div> than ever before. </div>
			<div> Thank you! </div>
			<div class="hard"></div>
			<div class="hard"></div>
		</div>
		
		<script src="resources/vcap.js"> </script>
        <script src="resources/index.js"> </script>
		
    </body>
</html>

Click Ctrl+S to Save the changes

resources/index.js

Copy-and-paste index.js handler code which initializes the flipbook once the VTAP platform has fully loaded.

VCAP.event.on('app.loaded', function(){
    $("#flipbook").turn({
        width: 400,
        height: 300,
        autoCenter: true
    });
});

Click Ctrl+S to Save the changes

resources/index.css

Copy-and-paste index.css code which provides the styles for the flipbook.

.sample-flipbook{
	width:400px;
	height:200px;
	-webkit-transition:margin-left 0.2s;
	-moz-transition:margin-left 0.2s;
	-ms-transition:margin-left 0.2s;
	-o-transition:margin-left 0.2s;
	transition:margin-left 0.2s;
}

.sample-flipbook .page{
	width:200px;
	height:200px;
	background-color:white;
	line-height:300px;
	font-size:20px;
	text-align:center;
}

.sample-flipbook .page-wrapper{
	-webkit-perspective:2000px;
	-moz-perspective:2000px;
	-ms-perspective:2000px;
	-o-perspective:2000px;
	perspective:2000px;
}

.sample-flipbook .hard{
	background:#ccc !important;
	color:#333;
	-webkit-box-shadow:inset 0 0 5px #666;
	-moz-box-shadow:inset 0 0 5px #666;
	-o-box-shadow:inset 0 0 5px #666;
	-ms-box-shadow:inset 0 0 5px #666;
	box-shadow:inset 0 0 5px #666;
	font-weight:bold;
}

.sample-flipbook .odd{
	background:-webkit-gradient(linear, right top, left top, color-stop(0.95, #FFF), color-stop(1, #DADADA));
	background-image:-webkit-linear-gradient(right, #FFF 95%, #C4C4C4 100%);
	background-image:-moz-linear-gradient(right, #FFF 95%, #C4C4C4 100%);
	background-image:-ms-linear-gradient(right, #FFF 95%, #C4C4C4 100%);
	background-image:-o-linear-gradient(right, #FFF 95%, #C4C4C4 100%);
	background-image:linear-gradient(right, #FFF 95%, #C4C4C4 100%);
	-webkit-box-shadow:inset 0 0 5px #666;
	-moz-box-shadow:inset 0 0 5px #666;
	-o-box-shadow:inset 0 0 5px #666;
	-ms-box-shadow:inset 0 0 5px #666;
	box-shadow:inset 0 0 5px #666;
	
}

.sample-flipbook .even{
	background:-webkit-gradient(linear, left top, right top, color-stop(0.95, #fff), color-stop(1, #dadada));
	background-image:-webkit-linear-gradient(left, #fff 95%, #dadada 100%);
	background-image:-moz-linear-gradient(left, #fff 95%, #dadada 100%);
	background-image:-ms-linear-gradient(left, #fff 95%, #dadada 100%);
	background-image:-o-linear-gradient(left, #fff 95%, #dadada 100%);
	background-image:linear-gradient(left, #fff 95%, #dadada 100%);
	-webkit-box-shadow:inset 0 0 5px #666;
	-moz-box-shadow:inset 0 0 5px #666;
	-o-box-shadow:inset 0 0 5px #666;
	-ms-box-shadow:inset 0 0 5px #666;
	box-shadow:inset 0 0 5px #666;
}

Click Ctrl+S to Save the changes

Publish & Launch App

After saving file changes you need to Publish app for reflecting them in the others.

You can Launch app (or reload app if launched previously)

We have successfully created a Flipbook App and now you easily through the pages!

Traffic Clock

Here is a prototype to represent time in a traffic signal theme. We will implement this in VTAP using step-by-step to understand designing of components and re-use the same.

Before implementing the application, identifying the data and visual scope make its easy to structure the application. We have time (hour, minute, second) to be displayed as text (left-side) and boxes (right-side).

The boxes further should highlight the elapsed value and remaining value.

So our data structure would be like

elapsed: {
    hour: A,
    minute: B,
    second: C,
}

Box UI can be created using span element:

.box {
    display: inline-block; /* to make it work without content */
    width: 16px;
    height: 16px;
    margin: 2px;
    border: 1px solid black;
}

We will be using VueJS to enable binding the Visual and Data rendering.

Now lets create traffic_clock application in VTAP. Navigate Main Menu > Platform > App Creator > Add App

Fill the Form And Save

Clicking on TrafficClock App will open an editor where you can customise your App

Iteration - 1

Let us first get the timer display on HTML that changes every second.

views/index.html

<!DOCTYPE html>
<html>
    <head>
        <title>Traffic Clock</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="stylesheet" href="resources/index.css">
    </head>
    <body>
        <div id="app" v-cloak>
            <div class="hour">{{elapsed.hour}}</div>
            <div class="minute">{{elapsed.minute}}</div>
            <div class="second">{{elapsed.second}}</div>
        </div>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
         <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.7.14/vue.min.js"></script>
        <script src="resources/index.js"></script>
    </body>
</html>

Click Ctrl+S to Save the changes

resources/index.js

window.addEventListener('DOMContentLoaded', function() {
	new Vue({
		el: "#app",
		data: function() {
                                        /* Initialization */
			var d = new Date();
			return {
				elapsed: {
					hour: d.getHours(),
					minute: d.getMinutes(),
					second: d.getSeconds()
				}
			}
		},
		mounted: function() {
			setInterval( () => {
				var d = new Date();
				this.elapsed.hour = d.getHours();
				this.elapsed.minute = d.getMinutes();
				this.elapsed.second = d.getSeconds();
			}, 1000);
		}
	});
});

Click Ctrl+S to Save the changes

Publish the changes. Launch the application.

Iteration - 2

Let us now get the boxes displayed representing elapsed and remaining value of hour, minute, second.

views/index.html

<!DOCTYPE html>
<html>
    <head>
        <title>Traffic Clock</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
    	<link rel="stylesheet" href="resources/index.css">
    </head>
    <body>
        <div id="app" v-cloak>
            <div class="hour">
               {{elapsed.hour}}
               <span class="box hour elapsed" v-for="h in elapsed.hour"></span>
               <span class="box hour" v-for="h in (24 - elapsed.hour)"></span>
            </div>
            <div class="minute">
               {{elapsed.minute}}
               <span class="box minute elapsed" v-for="m in elapsed.minute"></span>
               <span class="box minute" v-for="m in (60 - elapsed.minute)"></span>
            </div>
            <div class="second">
               {{elapsed.second}}
               <span class="box second elapsed" v-for="s in elapsed.second"></span>
               <span class="box second" v-for="s in (60 - elapsed.second)"></span>
             </div>
        </div>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.7.14/vue.min.js"></script>
        <script src="resources/index.js"></script>
    </body>
</html>

Click Ctrl+S to Save the changes

resources/index.css

.box {
	display: inline-block;
	width: 16px;
	height: 16px;
	margin: 2px;
	border: 1px solid black;
}

.hour.elapsed {
	background: red;
}

.minute.elapsed {
	background: orange;
}

.second.elapsed {
	background: green;
}

Click Ctrl+S to Save the changes

Publish the changes. Launch the application.

Iteration - 3

Let us now build a custom VueJS boxes component which takes care of representing elapsed and remaining value. This will help us reduce HTML repetition as the component can be reused for hour, minute, second by varying the inputs to the component through attribute (type, value and max).

views/index.html

<!DOCTYPE html>
<html>
    <head>
        <title>Traffic Clock</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
    	<link rel="stylesheet" href="resources/index.css">
    </head>
    <body>
        <div id="app" v-cloak>
            <div class="hour">
               {{elapsed.hour}}
               <boxes type="hour" :value="elapsed.hour" :max="24"></boxes>
            </div>
            <div class="minute">
               {{elapsed.minute}}
               <boxes type="minute" :value="elapsed.minute" :max="60"></boxes>
            </div>
            <div class="second">
               {{elapsed.second}}
               <boxes type="second" :value="elapsed.second" :max="60"></boxes>
             </div>
        </div>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.7.14/vue.min.js"></script>
        <script src="resources/index.js"></script>
    </body>
</html>

Click Ctrl+S to Save the changes

resources/index.js

window.addEventListener('DOMContentLoaded', function() {

    Vue.component("boxes", {
        props: ["type", "value", "max"],
        template: `
            <div style="display:inline-block">
                <span :class="type" class="box elapsed" v-for="v in value"></span>
                <span :class="type" class="box" v-for="v in (max - value)"></span>
            </div>
        `
    });

    new Vue({
        el: "#app",
        data: function() {
            /* Initialization */
            var d = new Date();
            return {
                elapsed: {
                    hour: d.getHours(),
                    minute: d.getMinutes(),
                    second: d.getSeconds()
                }
            }
        },
        mounted: function() {
            setInterval( () => {
                var d = new Date();
                this.elapsed.hour = d.getHours();
                this.elapsed.minute = d.getMinutes();
                this.elapsed.second = d.getSeconds();
            }, 1000);
        }
    });
});

Click Ctrl+S to Save the changes

Publish the changes. Launch the application.

Iteration - 4

Continue the journey to match the expected representation.

Contacts Card

Let us now create an application that interacts with CRM data. It should fetch the Contacts records and represent them as cards.

Iteration - 1

Create application in VTAP (contact_cards). Navigate Main Menu > Platform > App Creator > Add App

Fill the Form And Save

Clicking on ContactCards App will open an editor where you can customise your App

resources/index.js

We will be using VueJS library along with VCAP (Vtiger Custom Application) runtime that provides userapi that helps you work with CRM records easily.

window.addEventListener('DOMContentLoaded', function() {
    new Vue({
        el: "#app",
        data: {
            contacts: []
        },
        mounted: function() {
            VCAP.userapi.records.get({module: "Contacts"}, (e, contacts) => {
                this.contacts = contacts;
            });
        }
    });
});

Click Ctrl+S to Save the changes

views/index.html

<!DOCTYPE html>
<html>
    <head>
        <title>Contacts</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css">
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css">
        <link rel="stylesheet" href="resources/index.css">
    </head>
    <body>
        <div id="app" class="container-fluid bg-dark" style="height: 100%" v-cloak>
            <div v-if="contacts" class="row">
                <div class="card m-4" style="width: 350px" v-for="c in contacts">
                    <div class="card-body">
                        <h5 class="card-title">{{c.firstname}} {{c.lastname}}</h5>
                        <p class="card-text">{{c.title}}</p>
                    </div>
                    <ul class="list-group list-group-flush">
                        <li class="list-group-item"><i class="fa-solid fa-envelope"></i> {{c.email}}</li>
                        <li class="list-group-item"><i class="fa-solid fa-phone"></i> {{c.phone ? c.phone : '-'}}</li>
                    </ul>
                </div>
            </div>
        </div>

        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.7.14/vue.min.js"></script>
        <script src="resources/vcap.js"> </script>
        <script src="resources/index.js"> </script>
    </body>
</html>

Click Ctrl+S to Save the changes

Publish the changes. Launch the application.

Iteration - 2

Lets try to refactor HTML to move the card representation as a VueJS component.

resources/index.js

Define a crm-contact component that renders information as a card.

window.addEventListener('DOMContentLoaded', function() {

    Vue.component('crm-contact', {
        props: ['info'],
        template: `
            <div class="card m-4" style="width: 350px">
                <div class="card-body">
                   <h5 class="card-title">{{info.firstname}} {{info.lastname}}</h5>
                   <p class="card-text">{{info.title}}</p>
                </div>
                <ul class="list-group list-group-flush">
                    <li class="list-group-item"><i class="fa-solid fa-envelope"></i> {{info.email}}</li>
                    <li class="list-group-item"><i class="fa-solid fa-phone"></i> {{info.phone ? info.phone : '-'}}</li>
                </ul>
            </div>
        `
    });

    new Vue({
        el: "#app",
        data: {
            contacts: []
        },
        mounted: function() {
            VCAP.userapi.records.get({module: "Contacts"}, (e, contacts) => {
                this.contacts = contacts;
            });
        }
    });
});

Click Ctrl+S to Save the changes

views/index.html

Use the crm-contact card tag in the HTML now.

<!DOCTYPE html>
<html>
    <head>
        <title>Contacts</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css">
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css">
        <link rel="stylesheet" href="resources/index.css">
    </head>
    <body>
        <div id="app" class="container-fluid bg-dark" style="height: 100%" v-cloak>
            <div v-if="contacts" class="row">
                <crm-contact v-for="c in contacts" :info="c"></crm-contact>
            </div>
        </div>

        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.7.14/vue.min.js"></script>
        <script src="resources/vcap.js"> </script>
        <script src="resources/index.js"> </script>
    </body>
</html>

Click Ctrl+S to Save the changes

Publish the changes. Launch the application.

Tasks Mind

We are now going to build an application that represents Tasks as Mindmap. In order to filter tasks we accept the project name from the URL. This enables us to trigger the app from a specific Project from CRM.

Create application in VTAP (tasks_mind). Navigate Main Menu > Platform > App Creator > Add App

Fill the Form And Save

Clicking on TasksMind App will open an editor where you can customise your App.

views/index.html

To build the Tasks MindMap we shall use jsMind library which provides various options to control the visualization of data (refer).

<!DOCTYPE html>
<html>
   <head>
       <title>Tasks Map</title>
       <meta charset="UTF-8">
       <meta name="viewport" content="width=device-width, initial-scale=1.0">
       <link type="text/css" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jsmind@0.5/style/jsmind.css"/>
       <link rel="stylesheet" href="resources/index.css">
   </head>
   <body>
       <div id="app">
           <div id="progress"></div>
           <div id="jsmind_container"></div>
       </div>
      
       <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
       <script src="https://cdn.jsdelivr.net/npm/jsmind@0.5/es6/jsmind.js"></script>
       <script src="https://cdn.jsdelivr.net/npm/jsmind@0.5/es6/jsmind.draggable-node.js"></script>
       <script src="resources/vcap.js"> </script>
       <script src="resources/index.js"> </script>
   </body>
</html>

Click Ctrl+s to Save the changes

resources/index.js

window.addEventListener('DOMContentLoaded', function() {

      // Collect parameters that comes in through URL
	var req_params = {};
	location.hash.substr(1).split("&").forEach(
		function(item) {
                req_params[item.split("=")[0]] = 
                   decodeURIComponent(item.split("=")[1])
            }
      );

	if (!req_params["name"]) {
		// No project selected.
		return;
	}

	var all_tasks_filterid = 19; /* "All Tasks" Filter ID   */
      var task_skip_closed = true; /* Turn off closed records */
	
      var project_name = req_params["name"];
	
	// Additional fields that are required if Filter is missing it.
      var extrafields = ["parent_task", "dependent_on"];

	// Filter custom condition
	var q = [];
	q.push(["related_project", "equal", project_name]);
	
	if (task_skip_closed) {
          q.push(["isclosed", "equal", 0]);
      }
      var record_options = {
          module: "Tasks", 
          filterid: all_tasks_filterid, 
          q: [q], 
          extrafields: extrafields
      };

	// Helper functions
	function determine_task_color(task) {
		return task.isclosed == 1? "#5cb85c" : "#428bca";
	}

      var mind = {
		"meta": {
			"name": project_name
		},
		"format": "node_array",
		data: [
			{id: "project", isroot:true, topic: project_name }
		]
	};

	jQuery("#progress").html("Loading...");

	VCAP.userapi.records.get(record_options, (e, tasks) => {
		jQuery("#progress").hide();

		var taskids = {};

		// Main tasks
		for (var i = 0, len = tasks.length; i < len; i++) {
                  /* avoid duplicate parsing */
			if (taskids[tasks[i].id]) continue;
 
			if (tasks[i].parent_task && tasks[i].parent_task.id > 0) 
                       continue;

			mind.data.push({ 
                     id: tasks[i]["id"], 
                     parentid: "project", 
                     topic: tasks[i]["label"], 
                     "background-color":determine_task_color(tasks[i]) 
                  });

			taskids[tasks[i]["id"]] = true;
		}


		// Sub tasks
		for (var i = 0, len = tasks.length; i < len; i++) {
                  /* avoid duplicate parsing */
			if (taskids[tasks[i].id]) continue; 

			if (tasks[i].parent_task && tasks[i].parent_task.id > 0){
				mind.data.push({ 
                          id: tasks[i]["id"], 
                          parentid: tasks[i].parent_task["id"], 
                          topic: tasks[i]["label"], 
                          "background-color":determine_task_color(tasks[i])
                        });
			}
			taskids[tasks[i]["id"]] = true;
		}
		
		var options = {
			container: 'jsmind_container', 
			theme: 'primary',
			editable: false
		};
		
		var jm = new jsMind(options);
		jm.show(mind);
	
            // Navigate to task on click of node
		jm.add_event_listener((type, data) => {
                if (data.evt == "select_node" && data.node != "project") {
                    var taskUrl = "/view/detai
l”;
                    taskUrl += "?module=Tasks&id="+ data.node;
			  
                    window.open(taskUrl);
                }
		});		
	});
});

Click Ctrl+s to Save the changes

resources/index.css

#jsmind_container {
	height: 100vh;
	width: 100%;
}

Click Ctrl+s to Save the changes

Publish the changes.

The application will be available at (/myapps/tasks_mind) but won't be of much use if the parameter (name) is not sent through the URL. We shall achieve this linking in the next step.

Linking app from CRM

Navigate VTAP > Module Designer > TAP Script provides us capability to build In-app custom components for the CRM. Using this we can add custom action on every Project Row of the List View.

Choose Projects and Create TAP Script - which can work across module.

NOTE: Target Module can be selected as Project to make it available only for Projects ListView. For demonstration we will achieve this through code.

var Project_Component_TasksMap = VTAP.Component.Core.extend({
   created: function() {
       var viewModule = App.module();
       if (viewModule == "Project") {
           VTAP.Component.Register( 'LIST_ROW_BASIC_ACTION', {}, 
               VTAP.Component.Load('TasksMapAction', 'Project'));
       }
   }
});

var Project_Component_TasksMapAction = VTAP.Component.Core.extend({
   props: ['record'],
   methods: {
       navigateToTasksMap: function() {
           var url = "/myapps/tasksmind";
           if (this.record._moduleName == "Project") {
               url += "#name=" + this.record.label;
           }
           window.open(url);
       }
   },
   template: `
       <li title="Tasks Map" class="list-inline-item c-pointer px-2 m-0" 
           @click="navigateToTasksMap()"><i class='fas fa-tasks'></i>
       </li>`
});

Save and Publish to make the component functional.

Open Projects ListView and hover on the record to see the new button added.

NOTE: If you don’t see it - try clearing your browser cache or use a private window.

Now clicking on the button would open /myapps/tasks_mind#name=project_name parameter which makes its work.

ReactJS App

VTAP supports deploying front-end apps built using ReactJS framework. We will cover the changes required to build scripts and deployment using the vtapit tool to make it seamless for your development.

Step 1: Create application (hello_react_vtap). Navigate Main Menu > Platform > App Creator > Add App

Fill the Form And Save

Step 2: Clone application to working directory using vtapit.

vtapit clone --crmapp hello_react_vtap hello_react_vtap_workdir
CRM URL: https://yourinstance.odx.vtiger.com
Username: me@name.here
Password: ********

Step 3: For this example, we will create a ReactJS app within the cloned VTAP working folder. This can be managed in other directory too as it would not be deployed.

cd hello_react_vtap_workdir
npx create-react-app react-src-local

Step 4: Edit react-src-local/package.json

We will customize the default build step to copy the compiled javascript to the VTAP resource folder which will be loaded by the HTML page.

"scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build && cp build/static/js/main*.js ../resources/index.js",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },

Step 5: Edit views/index.html

Define the react-app default mount point. Make sure the dependent resources and runtime libraries are loaded.

<!DOCTYPE html>
<html>
    <head>
        <title>React App - Contacts List</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="stylesheet" href="resources/index.css">
    </head>
    <body>
        <div id="root"></div>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js"></script>
        <script src="resources/vcap.js"> </script>
        <script src="resources/index.js"> </script>
    </body>
</html>

Step 6: You can continue building ReactJS app and build when ready.

Edit react-src-local/src/App.js

import './App.css';
import React, {Component} from 'react';

/* global VCAP */
/* This declaration will avoid eslint errors during build time */

class App extends Component {
  constructor(props) {
      super(props);
      this.state = {
          contacts: []
      }
  }

  componentDidMount() {
    /* We are loading the contacts records here… which gets rendered due to reactivity. */
    VCAP.userapi.records.get({module: "Contacts"}, (e, contacts) => {
        if (!e) this.setState({contacts: contacts});
    });
  }
  render() {
    return (
        <div className="App">
            <ul>
            {this.state.contacts.map((contact, index) => (
                <li>{contact.label}</li>
            ))}
            </ul>
        </div>
    );
  }
}

export default App;

Step 7:

Build ReactJS app - on success it copies the runtime javascript to VTAP app resource folder.

cd react-src-local 
npm run build
cd ..

Step 8: You can do a dry-run check of the VTAP app.

vtapit run
http://localhost:8080/myapps/

Step 9:

Deploy final changes to CRM with vtapit push and publish the app to Go-live.

vtapit add views/index.html 
vtapit add resources/index.js
vtapit push
vtapit publish

You can launch the app in browser now:

https://yourinstance.odx.vtiger.com/myapps/hello_react_vtap

Congratulations! The ReactJS app is now running on VTAP.

Connectors

SMS Connector

Vtiger has an SMS Messages module that supports different SMS providers which will help in sending text messages from CRM. Right now, it is limited to some providers like Twilio, ClickATell, IndiaSms, Gupshup etc. But, we have a lot more SMS providers which are not yet available in Vtiger. SMS Connectors helps you enable any customer provider we want. With a simple xml code we can introduce a new SMS Provider in the SMS Messages module.

Build your own

Let's dig in to know how to enable the SMS Connector for the provider you like.

  1. Navigate to Menu > Platform > Module Designer

  1. Click Add New Module on the top right corner and create an Extension Module. Give a name to the module and ignore selecting an App Category.

  1. VTAP has introduced a new Resource type in Module Designer called Connectors, select Connectors on the right side panel and select SMS in the drop down and giving a name to the connector. For example if you are integrating with Twilio.com service then use Twilio which will appear in the SMS Configuration page when setting it up.

Now you will see an editor where you need to define the SMS connector parameters, headers, end points and configuration parameters. On adding the connector, a default XML template will be loaded which needs to be updated with the provider details which we want to enabled.

4 .Let’s go through each XML component that needs our attention.

Config

This is where we need to update the fields that will be shown in configuration page to connect. This is in Settings > SMS Messages > Provider configuration. We support text, password, url fields in here.

<config>
   <fields>
       <field name="username" label="User Name" type="text" />
       <field name="password" label="Password" type="password" />
       <field name="type" label="Connection Type" type="picklist" />
           <options>
               <option> Type1 </options>
               <option> Type2 </options>     
           </options>
       </field>
    </fields>
</config>

Provider

We should configure SMS provider endpoint and authentication details here. This is what we use for connecting to the SMS provider service to do SMS actions.

Any XML component in the connector will accept values from config fields. We have supported tags to access config fields. For instance if we want to access the username field from config then we can use $config.$fields.username

<provider>
    <url>https://provider_url</url>
    <auth>
        <basic username="$config.$fields.username" password="$config.$fields.password"/>
    </auth>
</provider>

Message_Send

So far we enabled the fields needed and configured the Provider details. Now, it is time to do the action part, configuration that is needed for sending SMS. This has sub components

message_send.request

request.urlsms provider endpoint for sending sms. We can leave this empty if provider url is good enough.
request.headerif provider is expecting any headers while sending sms we can enable those headers from here.
request.parametersparameters that need to be sent while sending sms. We can use config fields if there are any provider account specific parameters. We will have some dynamic parameters that are controlled from Vtiger UI (message, recipients) for which we can use @message, @recipients.

message_send.response

Response has a format attribute. This helps in identifying the format of response from the SMS provider after sending SMS. For now, we only support xml or json format

We need to configure the identifiers of response. What key should be considered as message id, status etc. We support nested values as well(for ex:- result.content.message_id)

response.messagesIf sent message is in any nested property of response then this should be configured. Ignore this, if returned response itself is the details.
response.message_idthis is used to identify message id from response
response.message_statusthis is used to identity message status from response
response.errorthis is used to identify error if sms is failed

message_send.callback

We can remove this component if provider doesn’t support callback

Some providers support callback. When SMS is sent from the CRM, the provider will keep it in queue, once it is delivered then it hits the callback url

We auto generate callback urls for custom SMS providers. We can use runtime parameter @callback if we want to include in request parameters

Same as response we have format attribute and identifiers for callback.

  • callback.message_id
  • callback.message_status

message_send.statuses

Each provider can have their own status values for SMS. We need to configure what status from the response should be used as Processing or Delivered or Failed

This is how message_send going to look after configuring every thing

<message_send>
     <request method="post">
         <url> /send </url>
         <headers>
             <header name="content-type" value="application/json" />
         </headers>
         <parameters>
             <parameter name="From" value="$config.fields.from" />
             <parameter name="To" value="@recipients" />
             <parameter name="Body" value="@message" />
             <parameter name="Callback" value="@callback" />
         </parameters>
     </request>

     <response format="json"> 
         <message_id use="messageId" />
         <message_status use="messageStatus" />
         <message_to use="messageTo" />
         <error_message use="errorMessage" />
     </response>

     <callback format="json">
         <message_id use="messageId" />
         <message_status use="messageStatus" />
     </callback>

     <statuses>
         <status Processing="queued,sending" />
         <status Delivered="delivered" />
         <status Failed="failed" />
     </statuses>
</message_send>

Message_Status

In each SMS notifier record in the CRM we have the option to get status of the message, this part is going to be used for that request. Like, message_send we have request, response, statuses as sub components in this component as well. This is how it is going to look

<message_status>
        <request method="get">
            <url> /@messageid</url>
                   <parameters>
                              <parameter name=”name” value=”value”> </parameter>
                        </parameters>
        </request>

        <response format="json">
            <message_status use="message_state" />
            <message_to use="to_number" />
        </response>

        <statuses>
            <status Processing="queued,sent" />
            <status Delivered="delivered" />
            <status Failed="failed,rejected,undelivered" />
        </statuses>
</message_status>
  1. Once we update the XML with all required details. We are good to go with publishing the connector. Once we Save and Publish the Connector. We can see a new Provider added in SMS Notifier settings (Settings > Extensions > SMS Provider Configuration)

Configure the Provider specific details and activate it.

To test if it's working properly, try sending sms to a Contact. Go to the Contacts module, and select the SMS option. Select the phone number and send sms and see if it works.

Examples

Twilio

<?xml version='1.0'?>
<connector for="SMSNotifier">

   <config>
     <fields>
        <field name="AccountSID" label="Account SID" type="text" required="true">                
              </field>
        <field name="AuthToken" label="Auth Token" type="text" required="true">      
              </field>
        <field name="From" label="From" type="text" required="true"> 
              </field>
     </fields>
   </config>

   <provider>
      <url>      
        https://api.twilio.com/2010-04-01/Accounts/$config.$fields.AccountSID/Messages 
      </url>
      <auth> 
         <basic username="$config.$fields.AccountSID"   
                  password="$config.$fields.AuthToken" />
      </auth>
    </provider>

    <message_send>
        <request method="post">
            <headers> 
                <header name="content-type" value="application/json" />
            </headers>
            <parameters>
                <parameter name="From" value="$config.$fields.From" />
                <parameter name="To" value="@recipients" />
                <parameter name="Body" value="@message" />
                <parameter name="StatusCallback" value="@callback" />
            </parameters>
        </request>

        <response format="xml">
            <message_id use="Message.Sid" />
            <message_status use="Message.Status" />
            <message_to use="Message.To" />
            <error_message use="RestException.Message" />
        </response>

        <callback>
            <message_id use="MessageSid" />
            <message_status use="MessageStatus" />
        </callback>

        <statuses>
            <!-- CONFIGURE : What is the Processing/Delivered/Failed status value -->
            <status Processing="queued,sending" />
            <status Delivered="delivered" />
            <status Failed="failed" />
        </statuses>
    </message_send>

    <message_status>    
        <request method="get">
            <url> /@messageid </url>
        </request>

        <response format="xml">
            <message_id use="Message.Sid" />
            <message_status use="Message.Status" /> 
        </response>

        <statuses>
            <status Processing="queued,sending" />
            <status Delivered="delivered" />
            <status Failed="failed" />
        </statuses>
    </message_status>

</connector>

BulkSMS

<?xml version='1.0'?>
<connector for="SMSNotifier">

    <config>
        <fields>
            <field name="username" label="User Name" type="text" required="true" />
            <field name="password" label="Password" type="password" required="true" /> 
        </fields>
    </config>

    <provider>
        <url> https://api.bulksms.com/v1 </url>
        <auth> 
            <basic username="$config.$fields.username" 
                  password="$config.$fields.password" />
        </auth>
    </provider>

    <message_send>
        <request method="post">
            <url> /messages </url>

            <headers>
                <header name="content-type" value="application/json" />
            </headers>

            <parameters>
                <parameter name="to" value="@recipients" />
                <parameter name="body" value="@message" />
            </parameters>

        </request>

        <response format="json"> 
            <message_id use="id" />
            <message_status use="status.type" />
            <message_to use="to" />
            <error_message use="title" />
        </response>

        <statuses>
            <status Processing="ACCEPTED,SCHEDULED" />
            <status Delivered="DELIVERED" />
            <status Failed="FAILED" />
        </statuses>

    </message_send>

    <message_status>

        <request method="get">
            <url> /messages/@messageid</url>
            <headers>
                <header name="content-type" value="application/json" />
            </headers>
        </request>

        <response format="json">
            <message_status use="status.type" />
        </response>

        <statuses> 
            <status Processing="ACCEPTED,SCHEDULED" />
            <status Delivered="DELIVERED" />
            <status Failed="FAILED" />
        </statuses>

    </message_status>

</connector>

Plivo

<?xml version='1.0'?>
<connector for="SMSNotifier">

    <config>
        <fields>
            <field name="AuthID" label="Auth ID" type="text" required="true"> </field>
            <field name="AuthToken" label="Auth Token" type="text" required="true"> 
                  </field>
            <field name="From" label="From" type="text" required="true"> </field>
        </fields>
    </config>

    <provider>
        <url> https://api.plivo.com/v1/Account/$config.$fields.AuthID/Message/ </url>
        <auth> 
            <basic username="$config.$fields.AuthID" 
                password="$config.$fields.AuthToken" />
        </auth>
    </provider>

    <message_send>
        <request method="post">
            <url> </url>
            <headers>
                <header name="Content-Type" value="application/json"> </header>
            </headers>
            <parameters>
                <parameter name="src" value="$config.$fields.From" />
                <parameter name="dst" value="@recipients" />
                <parameter name="text" value="@message" />
                <parameter name="method" value="POST" />
                <parameter name="url" value="@callback" />
            </parameters>

        </request>

        <response format="json">
            <message_responses use="message_uuid" />
            <error_message use="error" />   
        </response>

        <statuses>
            <status Processing="queued,sent" />
            <status Delivered="delivered" />
            <status Failed="failed,rejected,undelivered" />
        </statuses>

        <callback> 
            <message_id use="MessageUUID" />
            <message_status use="Status" />
        </callback>
    </message_send>

    <message_status>

<request method="get">
            <url> /@messageid</url>
        </request>

        <response format="json">
            <!-- CONFIGURE : What is the status identifier in response -->
            <message_status use="message_state" />
            <message_to use="to_number" />
        </response>

        <statuses>
            <status Processing="queued,sent" />
            <status Delivered="delivered" />
            <status Failed="failed,rejected,undelivered" />
        </statuses>
    </message_status>

</connector>

WhatsApp Connector

VTAP allows you to build connectors to whatsapp service providers. There are different types of whatsapp APIs by various whatsapp partners. To let CRM connect to these and send/receive messages VTAP now supports Whatsapp connector which gives a framework to build extensions and provide all these capabilities.

Build your own

VTAP introduced a new Resource type in Module Designer called Connectors. Add a new connector and select Whatsapp type of Connector and give a name to it.

Define the XML and the save and publish to be available for the admin to configure from Whatsapp Configuration page.

XML configuration

On adding the connector, a default XML template will be loaded which needs to be updated with the service details which we want to enable sending/receiving whatsapp messages. Let’s go through each XML node from the top level.

Connector - This is the root node for all the configuration and for attribute will tell the type of connector.

Config - We need to take input from administrators to connect to external services like apikey, username, passwords etc. These configuration fields are added here under config > fields node with type attribute defining text type of fields.

These details will be used either in request as a header or part of a parameter or request body. To access these in other parts of xml we can use them with $config.$fields.appid.

The fields given here will appear in the Whatsapp > Settings > Add configuration page.

<config>
    <fields>
        <field name=”appid” type=”text” />
        <field name=”apikey” type=”text” />
    </fields>
</config>

Provider - To connect to any services you need to set an endpoint and other authentication details. These can be set under the provider node and we can input values from the administrator added in the config node here.

<provider>
    <url> https://provider_url </url>
    <auth> 
        <basic username="$config.$fields.username" password="$config.$fields.password"/>
    </auth>
</provider>
UrlThis is where you will put in your endpoint of whatsapp service.
AuthIf the service provider expects the secret details to be sent as basic authorization or bearer token then it can be done using the Auth node.
Auth
BasicThis is used to perform basic authorization.
BearerUse this if the service provider expects the key to be passed as bearer token.
ApikeyUse this if a token is to be passed in Authorization header.
JWTUse this for JSON web token

message_send

After setting endpoints and configuration details we need to send a request to send a whatsapp message. For this we have sub components described below

message_send.request

request.urlurl part for sending whatsapp messages. If provider.url already has the end point then you can ignore this.
request.headersadd headers like content-type and others under this
request.templateuse this to send template messages. This contains below sub components
template.urlset this if the url part is different for template type of messages.
template.headersthese can be used to override the request.headers
template.parametersthis will have parameters that will be used to send whatsapp messages. If the request expects the data to be sent as raw data then use raw-post-data to true.
<template>
    <url>/messaging</url>
    <headers>
        <header name=”content-type” value=”application/json”></header>
        <header name=”apikey” value=”$config.$fields.apikey”></header>
    </headers>
    <parameters raw-post-data="true">
        <parameter name="to" value="@to" />
        <parameter name="body" value="@message" />
        <parameter name="notifyurl" value="@callback" />
    </parameters>
</template>
request.textuse this when you want to send text message(within 24 hour session). These messages are send once you customer has responded back to the template messages or have initiated the conversation with the businesses.

text.url, text.headers and text.parameters follow the same conventions as template components.

<text>
    <url>/messaging</url>
    <parameters raw-post-data="true">
        <parameter name="appid" value="$config.$fields.appid" />
        <parameter name="deliverychannel" value="whatsapp" />
        <parameter name="notifyurl" value="@callback"></parameter>
    </parameters>
</text>
request.mediause this when you want to add an attachment with the message. Rest of the sub components follow the same conventions like template components. We should use this when all attachment types(pdf, image, video etc) are to be sent using the same api.
request.documentuse this when api expects to send pdf attachments.
request.imageuse this when api expects to send image attachments.
request.videouse this when api expects to send video attachments.
request.template_headerfooteruse this node when you have a header and footer with the text template message.
request.template_mediaif the template message has an attachment, then use this xml node to define the endpoints and parameters.
request.template_ctaIf the template message has Call-to-action buttons, then use this xml node. These message will have website and phone number buttons in them, including file.
request.template_cta_textIf the Call-to-action template message does not have attachment then use this.
request.template_qrIf the template message have quick reply buttons, then use this xml node. Use this when there is attachment along with the quick reply buttons.
request.template_qr_textUse this when quick reply buttons does not have attachments with them.

Note : All the above xml nodes are used to define sending whatsapp messages and are defined inside message_send xml node.

message_send.response

We need to use the response of the message sent to update details like setting the status, unique id of message for later lookup on delivery status etc. Following nodes are useful to set:

response.messagesuse this if the message is wrapped around with some parameter.
response.message_idthis is used to identify the message's unique id.
response.message_statusthis will identify the message’s status.
response.message_toset this if the response has the receiver's number of messages.
<response format="json">
    <error use="error" />
    <messages use="results" />

    <message_id use="messageId" />
    <message_status use="messageStatus" />
    <message_to use="messageTo" />
</response>

message_send.callback

To get delivery callbacks of the message like delivered, read etc, we need to configure the status and message_id in the callback node.

<callback format="json">
    <message_id use="messageid" />
    <message_status use="messageStatus" />
</callback>

message_send.statuses

Use this to map the status value of the provider with Vtiger. Here “Submitted” value from the service provider is mapped to the Send status of Vtiger. Likewise the other statuses.

<statuses>
    <!-- CONFIGURE : What is the success/failure status -->
    <status Sent="Submitted" />
    <status Queued="Queued" />
    <status Delivered="delivered" />
    <status Undelivered="failed" />
    <status Sent="sent" />
    <status Received="received" />
    <status Read="Read" />
</statuses>

message_receive

This node lets you configure the incoming messages from the customer to the CRM.

message_receive.responseWe need to capture information from the Incoming messages to assign it to associate to proper contact in CRM. Also incoming messages can have attachments which need to be created and linked.

Following nodes needs to be mapped from response:

response.messagemap the incoming message.
response.from_numberphone number from which the Contact sent a message.
response.message_idunique message id
response.attachmentsif the attachment is sent in an array with file_url and file_mime then map it to attachments.
response.file_contentif attachment is sent as base64 encoded data then map it to file_content, other values like file type can be mapped to file_mime, and name to file_name
response.file_urlif a public file url is sent then use file_url with file_mime and file_name to map to type and name of the file.
<message_receive>
    <response>
        <message use="message" />
        <from_number use="waid" />
        <message_id use="tid" />

        <attachments use="attachments" />
        <file_content use=”file.content” />
        <file_url use="url" />
        <file_mime use="mime" />
    </response>
</message_receive>

Data variables

VTAP gives several dynamic data variable which can be used in the xml, see below list:

@messagethis stores the message to be sent to the whatsapp number.
@tothis is the whatsapp number to which message is to be sent.
@file_typethis represents the mime type of the file to be sent out.
@file_urlthis is the public url of the file to be sent out to the customer.
@file_namethis is the name of the file.
@file_mime_typethis is the type of the file.
@file_contentif api expects the file data to be sent as base64 encoded value.
@template_idif template message is sent and the api requires whatsapp Template Id.
@template_paramsin template message, dynamic parameters with their name and value for example {“OTP”: 1111, “time” : “10 mins”}
@template_valuesif only dynamic values is needed in the api, for example [111, “10 mins”]
@template_namename of the whatsapp template name.
@callbackcallback url to check for delivery of the messages.
@incoming_callbackincoming callback url of the provider.
@headeruse this dynamic variable when you want selected template's header to be fillled in the xml.
@footertemplate's footer value will be replaced with this footer.
@website_button_textwebsite button text value from the selected template will be replaced for this variable in call to action template message.
@website_button_urlthis will be replaced with actual website url from your in call to action template message.
@phone_button_textthis will be replaced with phone button's text value in call to action template message.
@phone_numberthis will replace actual phone number button in call to action template message.
@button_text_1for quick reply template message, first button is represented by this. Likewise @button_text_2 and @button_text_3 represents 2nd and 3rd button.
@filefor uploading file directly in messages.

Template Sync

To sync all the templates created in the service provider into vtiger crm. The skeleton to build the template sync is shown below.

<templates>
    <request>
        <url>url</url>
        <auth>

        </auth>
        <parameters>
            <parameter name="pageSize" value="100"></parameter>
            <parameter name="pageNumber" value="1"></parameter>
        </parameters>
    </request>
    <response format="json">
        <templates use="messageTemplates" />
        <templatename use="elementName" />
        <templateid use="id" />
        <category use="category" />
        <description use="bodyOriginal" />
        <status use="status" />
        <media_type use="header.typeString" />
        <header use="header.text" />
        <media_url use="header.link" />
        <footer use="footer" />

        <button_text_1 use="buttons|search_multi_array:{'check':'type','value':'quick_reply','return':'parameter.text','index':'0'}" />
        <button_text_2 use="buttons|search_multi_array:{'check':'type','value':'quick_reply','return':'parameter.text','index':'1'}" />
        <button_text_3 use="buttons|search_multi_array:{'check':'type','value':'quick_reply','return':'parameter.text','index':'2'}" />

        <website_button_text use="buttons|search_multi_array:{'check':'type','value':'url','return':'parameter.text'}" />
        <website_button_url use="buttons|search_multi_array:{'check':'type','value':'url','return':'parameter.url'}" />
        <phone_button_text use="buttons|search_multi_array:{'check':'type','value':'call','return':'parameter.text'}" />
        <phone_number use="buttons|search_multi_array:{'check':'type','value':'call','return':'parameter.phoneNumber'}" />

    </response>

    <statuses>
        <status Approved="APPROVED"></status>
        <status Rejected="REJECTED"></status>
        <status Not_Approved="DRAFT"></status>
    </statuses>

    <media_types>
        <media_type document="document"></media_type>
        <media_type video="VIDEO"></media_type>
        <media_type image="IMAGE"></media_type>
        <media_type text="TEXT"></media_type>
    </media_types>
</templates>
  • Only providers which support get all templates can be used to sync.
  • The starting node is the template where the template sync starts.
  • Request has a url and auth which the service provider requires.
  • paramerts are what the service provider requires.
  • response is used for mapping the template name,templateid,headers,urls,media type,body,buttons.
  • Media types is to know what all types of media is allowed in this provider.

Examples

Clickatell

Connecting to Clickatell whatsapp account, and here is the link to their api documentation.

<?xml version='1.0'?>
<connector for="Whatsapp">
    <config>
        <fields>
            <!-- CONFIGURE : Data fields that need to be captured from Whatsapp settings -->
            <field name="apikey" label="API Key" type="text" required="true"/>
        </fields>
    </config>
    <provider>
        <!-- CONFIGURE : Provider URL -->
        <url>https://platform.clickatell.com/v1</url>
    </provider>
    <message_send>
        <request method="post">
            <!-- CONFIGURE : Whatsapp send request end point -->
            <url> /message </url>
            <headers>
                <header name="content-type" value="application/json" />
                <header name="authorization" value="$config.$fields.apikey" />
            </headers>
            <text>
                <url>/message</url>
                <parameters raw-post-data="true">
                    <!-- CONFIGURE : name of From parameter and value -->
                    <parameter name="messages">
                        <values>
                            <parameter name="channel" value="whatsapp"></parameter>
                            <parameter name="to" value="@to"></parameter>
                            <parameter name="content" value="@message"></parameter>
                        </values>
                    </parameter>
                </parameters>    
            </text>

            <template>
                <url> /message </url>
                <parameters raw-post-data="true">
                    <!-- CONFIGURE : name of From parameter and value -->
                    <parameter name="messages">
                        <values>
                            <parameter name="channel" value="whatsapp"></parameter>
                            <parameter name="to" value="@to"></parameter>
                            <parameter name="template">
                                <parameter name="templateName" value="@template_name"></parameter>
                                <parameter name="body">
                                    <parameter name="parameters" value="@template_params"></parameter>
                                </parameter>
                            </parameter>
                        </values>
                    </parameter>
                </parameters> 
            </template>

            <template_headerfooter>
                <url> /message </url>
                <parameters raw-post-data="true">
                    <!-- CONFIGURE : name of From parameter and value -->
                    <parameter name="messages">
                        <values>
                            <parameter name="channel" value="whatsapp"></parameter>
                            <parameter name="to" value="@to"></parameter>
                            <parameter name="template">
                                <parameter name="templateName" value="@template_name"></parameter>
                                <parameter name="body">
                                    <parameter name="parameters" value="@template_params"></parameter>
                                </parameter>
                            </parameter>
                        </values>
                    </parameter>
                </parameters> 
            </template_headerfooter>

            <template_media>
                <url> /message </url>
                <parameters raw-post-data="true">
                    <!-- CONFIGURE : name of From parameter and value -->
                    <parameter name="messages">
                        <values>
                            <parameter name="channel" value="whatsapp"></parameter>
                            <parameter name="to" value="@to"></parameter>
                            <parameter name="template">
                                <parameter name="templateName" value="@template_name"></parameter>
                                <parameter name="header">
                                    <parameter name="type" value="media"></parameter>
                                    <parameter name="media">
                                        <parameter name="fileId" value="@file_url"></parameter>
                                    </parameter>
                                </parameter>
                            </parameter>
                        </values>
                    </parameter>
                </parameters> 
            </template_media>

            <template_cta>
                <url> /message </url>
                <parameters raw-post-data="true">
                    <!-- CONFIGURE : name of From parameter and value -->
                    <parameter name="messages">
                        <values>
                            <parameter name="channel" value="whatsapp"></parameter>
                            <parameter name="to" value="@to"></parameter>
                            <parameter name="template">
                                <parameter name="templateName" value="@template_name"></parameter>
                                <parameter name="header">
                                    <parameter name="type" value="media"></parameter>
                                    <parameter name="media">
                                        <parameter name="fileId" value="$file_url"></parameter>
                                    </parameter>
                                </parameter>
                                <parameter name="body">
                                    <parameter name="parameters" value="@template_params"></parameter>
                                </parameter>
                                <parameter name="buttons">
                                    <parameter name="websiteUrl">
                                        <values>
                                            <parameter name="listPosition" value="1"></parameter>
                                            <parameter name="parameters">
                                                <parameter name="1" value="@website_button_url"></parameter>
                                            </parameter>
                                        </values>
                                    </parameter>
                                </parameter>
                            </parameter>
                        </values>
                    </parameter>
                </parameters> 
            </template_cta>

            <template_cta_text>
                <url> /message </url>
                <parameters raw-post-data="true">
                    <!-- CONFIGURE : name of From parameter and value -->
                    <parameter name="messages">
                        <values>
                            <parameter name="channel" value="whatsapp"></parameter>
                            <parameter name="to" value="@to"></parameter>
                            <parameter name="template">
                                <parameter name="templateName" value="@template_name"></parameter>
                                <parameter name="body">
                                    <parameter name="parameters" value="@template_params"></parameter>
                                </parameter>
                                <parameter name="buttons">
                                    <parameter name="websiteUrl">
                                        <values>
                                            <parameter name="listPosition" value="1"></parameter>
                                            <parameter name="parameters">
                                                <parameter name="1" value="@website_button_url"></parameter>
                                            </parameter>
                                            <parameter name="listPosition" value="2"></parameter>
                                            <parameter name="parameters">
                                                <parameter name="1" value="@phone_number"></parameter>
                                            </parameter>
                                        </values>
                                    </parameter>
                                </parameter>
                            </parameter>
                        </values>
                    </parameter>
                </parameters> 
            </template_cta_text>

            <template_qr>
                <url> /message </url>
                <parameters raw-post-data="true">
                    <!-- CONFIGURE : name of From parameter and value -->
                    <parameter name="messages">
                        <values>
                            <parameter name="channel" value="whatsapp"></parameter>
                            <parameter name="to" value="@to"></parameter>
                            <parameter name="template">
                                <parameter name="templateName" value="@template_name"></parameter>
                                <parameter name="header">
                                    <parameter name="type" value="media"></parameter>
                                    <parameter name="media">
                                        <parameter name="fileId" value="@file_url"></parameter>
                                    </parameter>
                                </parameter>
                                <parameter name="body">
                                    <parameter name="parameters" value="@template_params"></parameter>
                                </parameter>
                                <parameter name="items">
                                    <values>
                                        <parameter name="title" value="@button_text_1"></parameter>
                                        <parameter name="postbackData" value="@button_text_1"></parameter>
                                    </values>
                                    <values>
                                        <parameter name="title" value="@button_text_2"></parameter>
                                        <parameter name="postbackData" value="@button_text_2"></parameter>
                                    </values>
                                    <values>
                                        <parameter name="title" value="@button_text_3"></parameter>
                                        <parameter name="postbackData" value="@button_text_3"></parameter>
                                    </values>
                                </parameter>
                            </parameter>
                        </values>
                    </parameter>
                </parameters> 
            </template_qr>

            <template_qr_text>
                <url> /message </url>
                <parameters raw-post-data="true">
                    <!-- CONFIGURE : name of From parameter and value -->
                    <parameter name="messages">
                        <values>
                            <parameter name="channel" value="whatsapp"></parameter>
                            <parameter name="to" value="@to"></parameter>
                            <parameter name="template">
                                <parameter name="templateName" value="@template_name"></parameter>
                                <parameter name="body">
                                    <parameter name="parameters" value="@template_params"></parameter>
                                </parameter>
                                <parameter name="items">
                                    <values>
                                        <parameter name="title" value="@button_text_1"></parameter>
                                        <parameter name="postbackData" value="@button_text_1"></parameter>
                                    </values>
                                    <values>
                                        <parameter name="title" value="@button_text_2"></parameter>
                                        <parameter name="postbackData" value="@button_text_2"></parameter>
                                    </values>
                                    <values>
                                        <parameter name="title" value="@button_text_3"></parameter>
                                        <parameter name="postbackData" value="@button_text_3"></parameter>
                                    </values>
                                </parameter>
                            </parameter>
                        </values>
                    </parameter>
                </parameters> 
            </template_qr_text>

            <media>
                <url>/message</url>
                <parameters  raw-post-data="true">
                    <parameter name="messages">
                        <values>
                            <parameter name="channel" value="whatsapp"></parameter>
                            <parameter name="to" value="@to"></parameter>
                            <parameter name="content" value="@file_content"></parameter>
                            <parameter name="media">
                                <parameter name="contentType" value="@file_mime_type"></parameter>
                                <parameter name="caption" value="@file_name"></parameter>
                            </parameter>
                        </values>
                    </parameter>
                </parameters> 
            </media>

        </request>

        <!-- CONFIGURE : type of message response json or xml -->
        <response format="json">
            <!-- CONFIGURE : response identifiers -->
            <error use="messages.0.error.description" /> <!-- What is the error message identifier if request fails from service -->

            <message_id use="messages.0.apiMessageId" />
            <message_status use="messages.0.accepted" />
            <message_to use="messages.0.to" />
        </response>

        <!-- INFO : Callback will be https://instance_url/modules/Whatsapp/callbacks/Custom.php -->
        <callback format="json">
            <!-- CONFIGURE : What is the message/status identifier in callback response -->
            <message use="event" />
            <message_id use="event.messageStatusUpdate.0.messageId" />
            <message_status use="event.messageStatusUpdate.0.status" />
        </callback>

        <statuses>
            <!-- CONFIGURE : What is the success/failure status -->
            <status Sent="SENT_TO_SUPPLIER" />
            <status Queued="1" />
            <status Delivered="DEVICE_ACK" />
            <status Undelivered="failed" />
            <status Received="received" />
            <status Read="READ" />
        </statuses>
    </message_send>

   <message_receive>
        <response>
            <message use="event.moMedia.0.caption || event.moText.0.content" />
            <to_number use="event.moMedia.0.to || event.moText.0.to || event.moLocation.0.to" />
            <message_id use="event.moMedia.0.messageId || event.moText.0.messageId || event.moLocation.0.messageId" />

            <from_number use="event.moMedia.0.from || event.moText.0.from || event.moLocation.0.from" />
            <from_name use="event.moMedia.0.whatsapp.profileName || event.moText.0.whatsapp.profileName || event.moLocation.0,whatsapp.profileName" />

            <file_mime use="event.moMedia.0.contentType" />
            <file_name use="event.moMedia.0.caption" />
            <file_content use="event.moMedia.0.content" />
        </response>
    </message_receive>

</connector>

Gupshup

Gupshup Enterprise Whatsapp Connector with template sync

<?xml version='1.0'?>
<connector for="Whatsapp">
    <config>
        <fields>
            <field name="userid_2way" label="User ID(Two way)" type="text" />
            <field name="password_2way" label="Password(Two way)" type="password" />
            <field name="userid" label="User ID(Notification)" type="text" />
            <field name="password" label="Password(Notification)" type="password" />
        </fields>
    </config>
    <provider>

        <url>https://media.smsgupshup.com/GatewayAPI/rest</url>
    </provider>
    <message_send>
        <request method="get" support-text="true">
            <!-- CONFIGURE : Whatsapp send request end point -->
            <url>https://media.smsgupshup.com/GatewayAPI/rest</url>
            <headers>
                <header name="content-type" value="application/x-www-form-urlencoded" />
            </headers>

            <text>
                <url></url> 
                <parameters>
                    <parameter name="userid" value="$config.$fields.userid_2way" />
                    <parameter name="password" value="$config.$fields.password_2way" />
                    <parameter name="method" value="SendMessage" />
                    <parameter name="auth_scheme" value="plain" />
                    <parameter name="v" value="1.1" />
                    <parameter name="send_to" value="@to" />
                    <parameter name="msg" value="@message" />
                    <parameter name="format" value="json" />
                    <parameter name="msg_type" value="DATA_TEXT" />
                    <parameter name="data_encoding" value="Unicode_text" />
                </parameters>
            </text>

            <template>
                <url></url> 
                <parameters>
                    <parameter name="userid" value="$config.$fields.userid" />
                    <parameter name="password" value="$config.$fields.password" />
                    <parameter name="method" value="SendMessage" />
                    <parameter name="auth_scheme" value="plain" />
                    <parameter name="v" value="1.1" />
                    <parameter name="send_to" value="@to" />
                    <parameter name="msg" value="@message" />

                    <parameter name="isHSM" value="true"></parameter>
                    <parameter name="format" value="json" />
                    <parameter name="msg_type" value="HSM" />
                </parameters>
            </template>

            <template_headerfooter>
                <url></url> 
                <parameters>
                    <parameter name="userid" value="$config.$fields.userid" />
                    <parameter name="password" value="$config.$fields.password" />
                    <parameter name="method" value="SendMessage" />
                    <parameter name="auth_scheme" value="plain" />
                    <parameter name="v" value="1.1" />
                    <parameter name="send_to" value="@to" />
                    <parameter name="msg" value="@message" />

                    <parameter name="isHSM" value="true" />
                    <parameter name="format" value="json" />
                    <parameter name="msg_type" value="HSM" />

                    <parameter name="footer" value="@footer" />
                    <parameter name="header" value="@header" />
                    <parameter name="isTemplate" value="true"/>
                </parameters>
            </template_headerfooter>

            <template_media>
                <url></url>
                <parameters>
                    <parameter name="userid" value="$config.$fields.userid" />
                    <parameter name="password" value="$config.$fields.password" />
                    <parameter name="method" value="SendMediaMessage" />
                    <parameter name="auth_scheme" value="plain" />
                    <parameter name="v" value="1.1" />
                    <parameter name="send_to" value="@to" />
                    <parameter name="msg" value="@message" />
                    <parameter name="isTemplate" value="true" />
                    <parameter name="isHSM" value="true" />
                    <parameter name="caption" value="@message" />
                    <parameter name="format" value="json" />
                    <parameter name="data_encoding" value="Unicode_text" />
                    <parameter name="footer" value="@footer" />
                    <parameter name="msg_type" value="@file_type" />
                    <parameter name="media_url" value="@file_url" />
                    <parameter name="filename" value="@file_name" />
                </parameters>
            </template_media>

            <template_cta_text>
                <url></url>
                <parameters>
                    <parameter name="userid" value="$config.$fields.userid" />
                    <parameter name="password" value="$config.$fields.password" />
                    <parameter name="method" value="SendMessage" />
                    <parameter name="auth_scheme" value="plain" />
                    <parameter name="v" value="1.1" />
                    <parameter name="send_to" value="@to" />
                    <parameter name="msg" value="@message" />
                    <parameter name="isTemplate" value="true" />
                    <parameter name="isHSM" value="true"></parameter>
                    <parameter name="caption" value="@message" />
                    <parameter name="format" value="json" />
                    <parameter name="data_encoding" value="Unicode_text" />
                    <parameter name="footer" value="@footer" />
                    <parameter name="msg_type" value="text" />
                    <parameter name="header" value="@header" />
                </parameters>
            </template_cta_text>

            <template_cta>
                <url></url>
                <parameters>
                    <parameter name="userid" value="$config.$fields.userid" />
                    <parameter name="password" value="$config.$fields.password" />
                    <parameter name="method" value="SendMediaMessage" />
                    <parameter name="auth_scheme" value="plain" />
                    <parameter name="v" value="1.1" />
                    <parameter name="send_to" value="@to" />
                    <parameter name="msg" value="@message" />
                    <parameter name="isTemplate" value="true" />
                    <parameter name="isHSM" value="true"></parameter>
                    <parameter name="caption" value="@message" />
                    <parameter name="format" value="json" />
                    <parameter name="data_encoding" value="Unicode_text" />
                    <parameter name="footer" value="@footer" />
                    <parameter name="msg_type" value="@file_type" />
                    <parameter name="media_url" value="@file_url" />
                    <!--parameter name="buttonUrlParam" value="@buttonurl" /-->
                    <parameter name="filename" value="@file_name" />
                </parameters>
            </template_cta>

            <template_qr>
                <url></url>
                <parameters>
                    <parameter name="userid" value="$config.$fields.userid" />
                    <parameter name="password" value="$config.$fields.password" />
                    <parameter name="method" value="SendMediaMessage" />
                    <parameter name="auth_scheme" value="plain" />
                    <parameter name="v" value="1.1" />
                    <parameter name="send_to" value="@to" />
                    <parameter name="msg" value="@message" />
                    <parameter name="isTemplate" value="true" />
                    <parameter name="isHSM" value="true"></parameter>
                    <parameter name="caption" value="@message" />
                    <parameter name="format" value="json" />
                    <parameter name="data_encoding" value="Unicode_text" />
                    <parameter name="footer" value="@footer" />
                    <parameter name="msg_type" value="@file_type" />
                    <parameter name="media_url" value="@file_url" />
                    <parameter name="filename" value="@file_name" />
                </parameters>
            </template_qr>

            <template_qr_text>
                <url></url>
                <parameters>
                    <parameter name="userid" value="$config.$fields.userid" />
                    <parameter name="password" value="$config.$fields.password" />
                    <parameter name="method" value="SendMessage" />
                    <parameter name="auth_scheme" value="plain" />
                    <parameter name="v" value="1.1" />
                    <parameter name="send_to" value="@to" />
                    <parameter name="msg" value="@message" />
                    <parameter name="isTemplate" value="true" />
                    <parameter name="isHSM" value="true"></parameter>
                    <parameter name="caption" value="@message" />
                    <parameter name="format" value="json" />
                    <parameter name="data_encoding" value="Unicode_text" />
                    <parameter name="footer" value="@footer" />
                    <parameter name="msg_type" value="HSM" />
                    <parameter name="header" value="@header" />
                </parameters>
            </template_qr_text>

            <media>
                <url></url>
                <parameters>
                    <parameter name="method" value="SendMediaMessage" />
                    <parameter name="auth_scheme" value="plain" />
                    <parameter name="v" value="1.1" />
                    <parameter name="send_to" value="@to" />
                    <parameter name="msg" value="@message" />
                    <parameter name="userid" value="$config.$fields.userid_2way" />
                    <parameter name="password" value="$config.$fields.password_2way" />
                    <parameter name="isTemplate" value="false" />
                    <parameter name="isHSM" value="false"></parameter>
                    <parameter name="msg_type" value="@file_type" />
                    <parameter name="caption" value="@message" />
                    <parameter name="format" value="json" />
                    <parameter name="media_url" value="@file_url" />
                    <parameter name="filename" value="@file_name" />
                </parameters>
            </media>
        </request>

        <response format="json">
            <!-- CONFIGURE : response identifiers -->
            <error use="response.details" /> <!-- What is the error message identifier if request fails from service -->
            <messages use="response" />

            <message_id use="id" />
            <message_status use="status" />
            <message_to use="phone" />
            <from_number use="$config.$fields.from_number" />

        </response>

        <callback format="json" >
            <!-- CONFIGURE : What is the message/status identifier in callback response -->
            <messages use="response" multiple="true"/>
            <message_id use="externalId" />
            <message_status use="eventType" />
            <error use="cause" />
            <message_receive use="type" />
        </callback>

        <statuses>
            <!-- CONFIGURE : What is the success/failure status -->
            <status Sent="SENT" />
            <status Sent="success" />
            <status Delivered="DELIVERED" />
            <status Undelivered="FAILED" />
            <status Received="Received" />
            <status Read="READ" />
        </statuses>
    </message_send>

    <message_receive>

        <response>
            <message use="text||image.caption||button.text" />
            <from_number use="mobile" />
            <from_name use="name" />
            <message_id use="timestamp" />
            <to_number use="waNumber" />
            <!-- if file contents are inside attachments node-->
            <attachments use="document||image||voice||audio||video" />
            <file_url use="url" download="true" append="signature"/>
            <file_mime use="mime_type" />
            <file_name use="caption" />
        </response>
    </message_receive>

    <templates>
        <request>
            <url>https://wamedia.smsgupshup.com/GatewayAPI/rest</url>
            <parameters>
                <parameter name="method" value="get_whatsapp_hsm"></parameter>
                <parameter name="userid" value="$config.$fields.userid"></parameter>
                <parameter name="password" value="$config.$fields.password"></parameter>
                <parameter name="fields" value="['buttons']"></parameter>
                <parameter name="limit" value="500"></parameter>
                <parameter name="offset" value="0"></parameter>
            </parameters>
        </request>
        <response format="json">
            <templates use="data" />
            <templatename use="name" />
            <templateid use="id" />
            <category use="category" />
            <description use="body" />
            <status use="status" />
            <media_type use="type" />
            <header use="header" />
            <media_url use="header.mediaHeaderId" />
            <header use="header" />
            <footer use="footer" />

            <button_text_1 use="buttons|search_multi_array:{'check':'type','value':'QUICK_REPLY','return':'text','index':'0'}" />
            <button_text_2 use="buttons|search_multi_array:{'check':'type','value':'QUICK_REPLY','return':'text','index':'1'}" />
            <button_text_3 use="buttons|search_multi_array:{'check':'type','value':'QUICK_REPLY','return':'text','index':'2'}" />

            <website_button_text use="buttons|search_multi_array:{'check':'type','value':'URL','return':'text'}" />
            <website_button_url use="buttons|search_multi_array:{'check':'type','value':'URL','return':'url'}" />
            <phone_button_text use="buttons|search_multi_array:{'check':'type','value':'PHONE_NUMBER','return':'text'}" />
            <phone_number use="buttons|search_multi_array:{'check':'type','value':'PHONE_NUMBER','return':'phone_number'}" />

        </response>

        <statuses>
            <status Approved="ENABLED"></status>
            <status Rejected="REJECTED"></status>
        </statuses>

        <media_types>
            <media_type document="DOCUMENT"></media_type>
            <media_type video="VIDEO"></media_type>
            <media_type image="IMAGE"></media_type>
            <media_type text="TEXT"></media_type>
        </media_types>
    </templates>
</connector>

Phonecall Connector

Vtiger has an PhoneCalls module that supports different PhoneCall providers which will help in making outbound and inbound calls from CRM. Right now, it is limited to some providers like Twilio, Plivo, Exotel, Telzio etc. But, we have a lot more Phone Call providers which are not yet available in Vtiger.

Phone Connectors helps you enable below functionality.

  • Click to call
  • Listening incoming calls and show poup inside Vtiger

With a simple xml code we can introduce a new Phone Provider in the PhoneCalls module and connect with telephony providers.

Requirements

Telephony provider should have REST API's support. That's it.

Build your own

Let's dig in to know how to enable the Phone Connector for the Provider you like.

  1. First you need to access Module Designer, go to the menu check Platform app and under that you can see Module Designer.

Here you get an option to add an extension module on the top right corner. Give a name to the module and ignore selecting an App Category.

Adding Connector : VTAP introduced a new Resource type in Module Designer called Connectors - see Adding Connector will prompt for Connector Type and Name where we need to fill details - see

Adding XML configuration : On adding the connector, a default XML template will be loaded which needs to be updated with the provider details which we want to enable - see.

Let’s go through each XML component that needs our attention

Config

This is where we need to update the fields we need to enable in Settings > Phone Configuration > Provider configuration. We support text, password, url fields in here These are configuration required to make conenction with telephony api's like username, password, or connection keys.

<config>
   <fields>
       <field name="username" label="User Name" type="text" >
       <field name="password" label="Password" type="password" >
    <fields>
<config>

Provider

We should configure Phone provider endpoint and authentication details here. This is what we use for connecting to the Phone provider service to do call actions.

Any XML component in the connector will accept values from config fields. We have supported tags to access config fields. For instance if we want to access the username field from config then we can use $config.$fields.username We store the connection details using config xml node, then we can use those stored config info using $config.$fields.CONFIG_NAME.

<provider>
   <url> https://provider_url <url>
   <auth> 
      <basic username="$config.$fields.username" password="$config.$fields.password">
   <auth>
<provider>

outgoing_call

So far we enabled the fields needed and configured the Provider details. Now, it is time to do the action part, configuration that is needed for placing outbound call. This has sub components

outgoing_call.request

  • request.url - call provider endpoint for making outbound calls. We can leave this empty if provider url is good enough
  • request.header - if provider is expecting any headers while making a call we can enable those headers here
  • request.parameters - parameters that need to be sent while making a call. We can use config fields if there are any provider account specific parameters. We will have some dynamic parameters that are controlled from Vtiger UI (to, from) for which we can use @to_number, @from_number

outgoing_call.response

  • Response has a format attribute. This helps in identifying the format of response from the Phone provider after placing a call. For now, we only support xml or json format
  • We need to configure the identifiers of response. What key should be considered as call id, status etc. We support nested values as well(for ex:- result.content.call_id)
  • response.call_id - this is used to identify call id from response
  • response.call_status - this is used to identity call status from response
  • response.error - this is used to identify error if call is failed

outgoing_call.callbacks

  • We can remove this component if provider doesn’t support callbacks.
  • Some providers support callback. When call is placed from the CRM, the provider will ring it, once it is answered then it hits the callback url
  • We auto generate callback urls for custom Phone providers. We can use runtime parameter @answer_status_callback if we want to include in request parameters
  • answer_callback:
    • This will be triggered when an outgoing call is answered.
  • answer_callback.response
    • This is used to create phonecall record from the callback request
  • answer_callback.send_response
    • This is used to send response to the callback. Usually phone providers expect some xml to be emitted.
<answer_callback>
    <response format="json">
        <call_id use="CallSid"><call_id>
        <to_number use="DialTo"><to_number>
        <from_number use="From"><from_number>
        <user_number use="To"><user_number>
        <call_status use="CallStatus"><call_status>
    <response>

    <send_response format="xml" agentdialing="Number">
        <headers>
            <header name="Content-Type" value="text/xml"><header>
        <headers>
        <parameters>
            <parameter name="Dial" >
                <attrs>
                    <attr name="action" value="@status_callback"><attr>
                <attrs>
                <parameter name="Number" value="@to_number"><parameter>
            <parameter>
        <parameters>
    <send_response>
<answer_callback>
  • status_callback
    • This will be triggered when there is change of status for a call
    • status_callback.status
      • Specify for attribute as call statuses that has to be checked before triggering this part.
    • status_callback.status.response
      • This is used to update phone record
<status_callback use="CallStatus">
    <status for="completed,hanup">
        <response>
            <call_id use="CallSid"><call_id>
            <call_status use="CallStatus"><call_status>
            <duration use="duration"><duration>
            <starttime use="starttime"><starttime>
        <response>
    <status>

    <status for="in-progress,ringing">
        <response format="json">
            <call_id use="CallSid"><call_id>
            <status use="CallStatus"><to_number>
        <response>
    <status>
<status_callback>

This is how outgoing_call going to look after configuring every thing

<outgoing_call>
    <request method="get">
        <url>AgentManualDial.php?<url>

        <headers><headers>

        <parameters>
            <parameter name="username" value="$config.$fields.username"><parameter>
            <parameter name="api_key" value="$config.$fields.apikey"><parameter>
            <parameter name="agentID" value="@email"><parameter>
            <parameter name="campaignName" value="@user_phone_home"><parameter>
            <parameter name="customerNumber" value="@to_number"><parameter>
            <parameter name="format" value="json"><parameter>
        <parameters>
    <request>

    <response>
        <error use="message"><error>
        <success use="status"><success>
    <response>

    <callbacks>
        <status_callback use="data.Status">

            <status for="Answered">
                <response>
                    <call_id use="data.monitorUCID"><call_id>
                    <call_status use="completed" default="completed"><call_status>
                    <duration use="data.Duration"><duration>
                    <recordingurl use="data.AudioFile"><recordingurl>
                    <starttime use="data.StartTime"><starttime>
                    <endtime use="data.EndTime"><endtime>
                    <to_number use="data.Did"><to_number>
                    <from_number use="data.CallerID"><from_number>
                    <user_number use="data.AgentID"><user_number>
                    <gateway use="service"><gateway>
                    <direction use="direction" default="outbound"><direction>
                <response>
            <status>

            <status for="NotAnswered">
                <response>
                    <call_id use="data.monitorUCID"><call_id>
                    <call_status use="no-answer" default="no-answer"><call_status>
                    <duration use="data.Duration"><duration>
                    <recordingurl use="data.AudioFile"><recordingurl>
                    <starttime use="data.StartTime"><starttime>
                    <endtime use="data.EndTime"><endtime>
                    <to_number use="data.Did"><to_number>
                    <from_number use="data.CallerID"><from_number>
                    <user_number use="data.AgentID"><user_number>
                    <gateway use="service"><gateway>
                    <direction use="direction" default="outbound"><direction>
                <response>
            <status>
        <status_callback>
    <callbacks>
<outgoing_call>

Incoming_call

  • incoming_call.response
    • Response has a format attribute. This helps in identifying the format of response from the Phone provider after placing a call. For now, we only support xml or json format
    • We need to configure the identifiers of response. What key should be considered as call id, status etc. We support nested values as well(for ex:- result.content.call_id)
      • response.call_id - this is used to identify call id from response
      • response.call_status - this is used to identity call status from response
      • response.error - this is used to identify error if call is failed
  • incoming_call.send_reponse
    • This is used to send response to the callback. Usually phone providers expect some xml to be emitted
    • Send Response has a format attribute. This helps in identifying the format of response from the Phone provider after placing a call. For now, we only support xml or json format.
incoming_call>
    <response format="json">
        <call_id use="CallSid"><call_id>
        <to_number use="To"><to_number>
        <from_number use="From"><from_number>
        <gateway use="service"><gateway>
        <direction use="direction" default="inbound"><direction>
    <response>

    <!-- sending response to callback -->
    <send_response format="xml" agentdialing="Number">
        <headers>
            <header name="Content-Type" value="text/xml"><header>
        <headers>
        <parameters>
            <parameter name="Dial" >
                <attrs>
                    <attr name="statuscallbackurl" value="@answer_status_callback"><attr>
                    <attr name="from_number" value="@from_number"><attr>
                <attrs>
                <parameter name="Number" value="@to_number"><parameter>
            <parameter>
        <parameters>
    <send_response>
<incoming_call>

Publish

Once we update the XML with all required details. We are good to go with publishing the connector.

Once we Save and Publish the Connector. We can see a new Provider added in Phone Configuration settings (Settings > Extensions > Phone Configuration) - see

Configure the Provider specific details and activate it.

To test if it's working properly, try Calling to a Contact. Go to the Contacts module, and select the Call option.

Twilio

Twilio extension we have developed to give you an idea on how the xml configuration would work, with using the above details: Voice API details for Twilio can be accessed from here

Explanation

  1. Under config we need details that will help us connect to the APIs using basic authorization. Here authid and authtoken input fields are used to store basic auth details.
  2. In provider we configure the endpoint, auth details which will be picked up from the Settings > Phone Call Settings > Add Provider > Twilio.
  3. In outgoing_call and incoming_call xml will define how the calls will be triggered and show phone call popup.
  4. In outgoing_call request xml node define making POST call to https://api.twilio.com/2010-04-01/Accounts/$TWILIO_ACCOUNT_SID/Calls.json and passing required parameters
  5. In outgoing_call request will make the actual call to the endpoint, and the response of the request is mapped under response xml node. Assuming the response is json and their keys are mapped to vtiger fields
  6. callbacks are webhook endpoints enabled in the platform to listen on the phone events like answer, call status, hangup event and incoming calls. All these callback response needs to mapped to vtiger relevant fields.
  7. Finally incoming_call listens to inbound calls and sends response back to acknowledge.
   
<?xml version='1.0'?>
<connector for="PhoneCalls">
    <config>
        <fields>
            <!-- CONFIGURE : Data fields that need to be captured from SMS Notifier settings -->
            <field name="authid" label="" type="text">
            <field name="authtoken" label="" type="text">
            <field name="callerid" label="" type="text" presence="mandatory">
            <field name="answer_callback" label="" type="text" presence="mandatory">
            <field name="status_callback" label="" type="text" presence="mandatory">
            <field name="incoming_callback" label="" type="text" presense="mandatory">    
        <fields>
    <config>
    <provider>
        <!-- CONFIGURE : Provider URL -->
        <url> <![CDATA[https://api.twilio.com/2010-04-01/Accounts/$config.$fields.authid]]><url>
        <auth> 
            <!-- CONFIGURE : Credentials -->
            <basic username="$config.$fields.authid" password="$config.$fields.authtoken" >
        <auth>
    <provider>

    <outgoing_call>
        <request>
            <url>Calls.json<url>

            <headers><headers>

            <parameters>
                <parameter name="To" value="@extension"><parameter>
                <parameter name="From" value="@callerid"><parameter>
                <parameter name="Url" value="@answer_callback|urlappend:{'DialTo': '@to_number'}"><parameter>
            <parameters>
        <request>

        <response>

            <callid use="id">
            <status use="callStatus">

            <!-- optional / use from request -->
            <to_number use="To">
            <from_number use="From">

        <response>

        <callbacks>

            <answer_callback>

                <response format="json">
                    <call_id use="CallSid"><call_id>
                    <to_number use="DialTo"><to_number>
                    <from_number use="From"><from_number>
                    <user_number use="To"><user_number>
                    <call_status use="CallStatus"><call_status>
                    <gateway use="service"><gateway>
                <response>

                //sending response to callback
                <send_response format="xml">
                    <headers>
                        <header name="Content-Type" value="text/xml"><header>
                    <headers>
                    <parameters>
                        <parameter name="Dial" >
                            <attrs>
                                <attr name="action" value="@status_callback"><attr>
                            <attrs>
                            <parameter name="Number" value="@to_number"><parameter>
                        <parameter>
                    <parameters>
                <send_response>

            <answer_callback>

            <status_callback use="CallStatus">

                <status for="completed,hangup">
                    <response>
                        <call_id use="CallSid"><call_id>
                        <call_status use="CallStatus"><call_status>
                        <duration use="duration"><duration>
                        <billrate use="billrate"><billrate>
                        <starttime use="starttime"><starttime>
                    <response>
                <status>

                <status for="in-progress,ringing">
                    <response format="json">
                        <call_id use="ParentCallSid"><call_id>
                        <call_status use="CallStatus"><call_status>
                    <response>
                <status>

            <status_callback>

        <callbacks>

    <outgoing_call>

    <incoming_call>
        <response format="json">
            <call_id use="CallSid"><call_id>
            <to_number use="To"><to_number>
            <from_number use="From"><from_number>
            <gateway use="service"><gateway>
            <call_status use="CallStatus"><call_status>
            <direction use="direction" default="inbound"><direction>
        <response>

        //sending response to callback
        <send_response format="xml" agentdialing="Number">
            <headers>
                <header name="Content-Type" value="text/xml"><header>
            <headers>
            <parameters>
                <parameter name="Dial" >
                    <attrs>
                        <attr name="action" value="@status_callback"><attr>
                    <attrs>
                    <parameter name="Number" value="@extension">
                        <attrs>
                            <attr name="action" value="@status_callback"><attr>
                        <attrs>
                    <parameter>
                <parameter>
            <parameters>
        <send_response>

    <incoming_call>
<connector>

Ozonetel

Another example developed for Ozonetel provider.


<?xml version='1.0'?>
<connector for="PhoneCalls">
    <config>
        <fields>
            <!-- CONFIGURE : Data fields that need to be captured from Phone Configuration settings -->
            <field name="apikey" label="" type="text">
            <field name="username" label="" type="text">
            <field name="callerid" label="" type="text" presence="mandatory">
            <field name="status_callback" label="" type="text" presence="mandatory">
            <field name="incoming_callback" label="" type="text" presence="mandatory">
        <fields>
    <config>
    <provider>
        <!-- CONFIGURE : Provider URL -->
        <url>https://api1.cloudagent.in/CAServices<url>

    <provider>

    <outgoing_call>
        <request method="get">
            <url>AgentManualDial.php?<url>

            <headers><headers>

            <parameters>
                <parameter name="username" value="$config.$fields.username"><parameter>
                <parameter name="api_key" value="$config.$fields.apikey"><parameter>
                <parameter name="agentID" value="@email"><parameter>
                <parameter name="campaignName" value="@user_phone_home"><parameter>
                <parameter name="customerNumber" value="@to_number"><parameter>
                <parameter name="format" value="json"><parameter>
            <parameters>
        <request>

        <response>
            <error use="message"><error>
            <success use="status"><success>
        <response>

        <callbacks>
            <status_callback use="data.Status">

                <status for="Answered">
                    <response>
                        <call_id use="data.monitorUCID"><call_id>
                        <call_status use="completed" default="completed"><call_status>
                        <duration use="data.Duration"><duration>
                        <recordingurl use="data.AudioFile"><recordingurl>
                        <starttime use="data.StartTime"><starttime>
                        <endtime use="data.EndTime"><endtime>
                        <to_number use="data.Did"><to_number>
                        <from_number use="data.CallerID"><from_number>
                        <user_number use="data.AgentID"><user_number>
                        <gateway use="service"><gateway>
                        <direction use="data.Type" default="outbound"><direction>
                        <uui use="data.UUI"><uui>
                        <campaign_name use="data.CampaignName"><campaign_name>
                    <response>
                <status>

                <status for="NotAnswered">
                    <response>
                        <call_id use="data.monitorUCID"><call_id>
                        <call_status use="no-answer" default="no-answer"><call_status>
                        <duration use="data.Duration"><duration>
                        <recordingurl use="data.AudioFile"><recordingurl>
                        <starttime use="data.StartTime"><starttime>
                        <endtime use="data.EndTime"><endtime>
                        <to_number use="data.Did"><to_number>
                        <from_number use="data.CallerID"><from_number>
                        <user_number use="data.AgentID"><user_number>
                        <gateway use="service"><gateway>
                        <direction use="data.Type" default="outbound"><direction>
                        <uui use="data.UUI"><uui>
                        <campaign_name use="data.CampaignName"><campaign_name>
                    <response>
                <status>

            <status_callback>
        <callbacks>

    <outgoing_call>

    <incoming_call>

        <response format="json">
            <call_id use="monitorUcid"><call_id>
            <to_number use="did"><to_number>
            <from_number use="callerID"><from_number>
            <user_number use="agentID"><user_number>
            <call_status use="in-progress" default="in-progress"><call_status>
            <gateway use="service"><gateway>
            <campaign_name use="campaignName"><campaign_name>
            <uui use="uui"><uui>
            <direction use="type" default="inbound"><direction>
        <response>

    <incoming_call>
<connector>

Payments Connector

TBD

Sync Connector

Vtiger has enabled sync with different applications like Google, Office365, Xero, Tally etc., But there are a lot more applications with which sync is not enabled yet.

Sync Connector opens the door to enable sync with applications that support REST based apis. With a XML schema, we can enable sync between Vtiger and any third party service.

Let’s see how we can do this.

Build your own

  1. Adding Sync Connector

VTAP introduced a new Resource type in Module Designer called Connectors - see Adding Connector will prompt for Connector Type and Name where we need to fill details - see

  1. XML configuration

On adding the connector, a default XML template will be loaded which needs to be updated with the service details which we want to enable sync with - see. Let’s go through each XML component that needs our attention.

Sync type

We should set up the sync type of this connector. In Vtiger we support 3 types of sync types.

  1. user - will sync data that is directly assigned to user.
  2. userandgroup - will sync data that is directly assigned to user and his related groups.
  3. app - will sync entire app data with respect to sharing rules.
<synctype>user</synctype>

Note: Once a connector is published we can not set or change synctype.

Config

We can configure some fields in config to reuse in the parameters. Fields added in config will be shown in sync settings where the admin can set up the values. For example, we have username, password for service authentication. We can configure fields like this for admin to configure.

<config>
    <fields>
        <field name="username" type="text" />
        <field name="password" type="password" />
    </fields>
</config>

This is categorized into three types

Pre Sync Settings

Any field that is added with presync=true attribute will be considered as pre sync field and becomes a readonly field. You can make the field editable by removing presync=true.

<field name="apikey" required="true" type="text" presync="true" />

Sync will not work until all these fields are filled. Only pre sync settings will be shown in Sync settings page until the user saves the values.

By default Sync Records From field will be shown here. This field decides, from when records should be synced (1 month, 3 months, 6 months, Fetch all).

System Settings We have some system settings

  • Debug Mode - checkbox to enable debug logs. Once this is enabled sync process will be logged into file
  • Background Sync Frequency - Frequency of background cron for sync (3 hours, 6 hours etc)

Sync Settings All other fields in config->fields in the xml will be shown here. We support text, password, date, datetime, reference, integer, url, email, phone, picklist, boolean fields. We can set type attribute with one of these types to show specific field UI in the config section. Few types(reference, picklist) need some extra information. Below are example for those

<config>
    <fields>
        <field name="default_contact" type="reference" module="Contacts" />
        <field name="default_status" type="picklist"> 
            <options>
                <option>Open</option>
                <option>Close</option>
            </options>
        </field>
    </fields>
</config>

We can use these fields in the url attributes, auth or parameters in push or pull request like $config.$fields.username, $config.$fields.password Note : If any one of required field value is not set then sync will not start

Modules

We need to configure modules which we want to sync from Vtiger to Service and vice versa. This is how it will look like if we want to enable syncing Contacts, Accounts modules.

<modules> 
    <module servicemodule="companies" vtigermodule="Accounts"></module>
    <module servicemodule="contacts" vtigermodule="Contacts" dependenton="Accounts"></module>
</modules>

Here:

servicemoduleis the module name of third party service
vtigermoduleis the module name from Vtiger side
dependentonis other modules on which this module is depending on. These can be comma separated values of any other modules that are configured to sync. If this is set then, until all dependent modules are completely synced this module will not start syncing

Field Mapping

Mapping between Vtiger fields and Service fields will go here. While syncing data from Vtiger to Service or vice versa, we need to know what field of Vtiger should go to Service and the other way around. We need to provide field mapping for each module that we need to sync.

Module names that we use in this block will be Vtiger module names. This is how it will look for field mapping for Contacts, Accounts modules.

<fieldmapping>
    <Contacts>
      <field vtigerfield="firstname" servicefield="firstname" />
      <field vtigerfield="lastname" servicefield="lastname" />
      <field vtigerfield="email" servicefield="email" />
    </Contacts>
    <Accounts>
        <field vtigerfield="accountname" servicefield="name" />
        <field vtigerfield="website" servicefield="website" />
        <field vtigerfield="phone" servicefield="phone" />  
    </Accounts>
</fieldmapping>

Line items mapping

<lineitems vtigerfield="_lineitems_" servicefield="line_items">
    <field vtigerfield="productid" servicefield="product_id" />
    <field vtigerfield="listprice" servicefield="item_price" />
    <field vtigerfield="quantity" servicefield="quantity" />
    <field vtigerfield="comment" servicefield="comments" />
</lineitems>

lineitems node is used when the module has items/line items which needs to be added in the record.

Below is the example of the default fieldmapping JSON.

[{"line_items":[{"product_id":null,"item_price":"1000.000000","quantity":"11.000","comments":"StartProduct 1"},{"product_id":null,"item_price":"1500.000000","quantity":"21.000","comments":"StartProduct 2"}],"clientversion":0,"id":""}]

To alter the values while sending the data from vtiger to service provider. Use a key serviceformat for each field which has to be modified before sending it to the service or provider.

Below is the example of the fielmapping for altering lastname and phone.

<fieldmapping>
    <Contacts>
      <field vtigerfield="firstname" servicefield="firstname" serviceformat="prefix:firstname-" />
      <field vtigerfield="lastname" servicefield="lastname" />
      <field vtigerfield="email" servicefield="email" />
    </Contacts>
    <Accounts>
        <field vtigerfield="accountname" servicefield="name" />
        <field vtigerfield="website" servicefield="website" />
        <field vtigerfield="phone" servicefield="phone" serviceformat="int" />  
    </Accounts>
</fieldmapping>

For lineitems altering the values use the below

<lineitems vtigerfield="_lineitems_" servicefield="line_items">
            <field vtigerfield="productid" servicefield="product_id" />
            <field vtigerfield="listprice" servicefield="item_price" />
            <field vtigerfield="quantity" servicefield="quantity" serviceformat="int" />
            <field vtigerfield="comment" servicefield="comments" />
</lineitems>

Example JSON

Here you can see that the quantity is converted to int format.

[{"line_items":[{"product_id":null,"item_price":"1000.000000","quantity":11(int),"comments":"StartProduct 1"},{"product_id":null,"item_price":"1500.000000","quantity":21,"comments":"StartProduct 2"}],"clientversion":0,"id":""}]

NOTE: In the pull response below we will be configuring identifier for records object. In this object we will have records that are pulled from the service. Fields from these records should be configured in servicefield in field mapping.

We can use nested values too, for instance if firstname field is inside properties array of the each record then in the mapping we can keep properties.firstname

<field vtigerfield="firstname" servicefield="properties.firstname" />

We can go to any number of nested levels.

Service

We need to provide service details with which we are trying to sync the data. Here, we will need to set up the service url and the auth details.

<service>
    <url> service_url </url>
    <auth>
        <oauth type="vtap" service="service" module="ext_module_name"></oauth>
    </auth>
</service>

NOTE : If there are any dynamic parameters to be used in the URL we should use url attributes. Examples:-

<url SYNCMODULE="@syncmodule"> service_url/$SYNCMODULE </url>
<url TOKEN="$config.$fields.token"> service_url/$TOKEN  </url>

Auth can be any of these types

  • basic - if service authentication is username, password based then
<basic username="" password="" />
  • apitoken - some services expect to send some api token for those
<apikey token="" />
  • oauth - VTAP also opened the door to do oauth of any service. If we configure the auth this way then while doing the sync it will automatically show auth popup
<oauth type="vtap" service="service" module="ext_module_name"></oauth/>
  1. service = service name with which oauth should perform
  2. ext_module_name = module name of current connector

Pull This is the one that helps us to get data from the Service to CRM. Like any other request, we will have

Overview of pull request

<pull>
   <request>
      <url>endpoint</url>
      <headers>
         <header name="" value="" />
      </headers>
      <parameters>

      </parameters>
   </request>

   <response>
      <error use="message" />
      <records use="results" />
      <recordid use="id" />
      <recordmodifiedtime use="updatedAt" />
      <recorddeleted use="archived" /> 
   </response>
</pull>

Request This will have pull endpoint, headers, and parameters. If the service what we are trying to sync with, has module specific endpoint, headers or parameters then we can overwrite the request for that module using that module tag inside the main request


<request method="post" >
    <url SYNCMODULE="@syncmodule">/$SYNCMODULE/search</url>
    <headers>
        <header name="content-type" value="application/json" />
    </headers>

    <Contacts>
        <parameters raw-post-data="true">
            <parameter name="filterGroups">
            <values>
                <parameter name="filters">
                    <values>
                        <parameter name="value" value="@lastsynctime:M" />
                        <parameter name="propertyName" value="lastmodifieddate" /> 
                        <parameter name="operator" value="GTE" />  
                    </values>
                </parameter>
            </values>
            </parameter>
        </parameters>
    </Contacts>

    <Accounts>
        <parameters raw-post-data="true">
            <parameter name="filterGroups">
                <values>    
                    <parameter name="filters">  
                        <values>
                            <parameter name="value" value="@lastsynctime:M" />
                            <parameter name="propertyName" value="hs_lastmodifieddate" />
                            <parameter name="operator" value="GTE" /> 
                        </values>
                    </parameter>
                </values>
            </parameter>
        </parameters>
    </Accounts>
</request>

If parameters are common for all modules then we can avoid module specific tags in the request and keep parameters in the request tag directly.

Parameters

Different services will expect parameters in different formats with respect to nested levels. Let see some examples of parameters set to understand this better

  1. If you have plain key value pair then
<parameters>
    <parameter name="key1" value="value1" />
    <parameter name="key2" value="value2" />
</parameters>

Output would be {"key1" : "value1", "key2" : "value2"}

  1. Sometimes we may want to send an array of objects, that’s where we can use values tag.
<parameters>
    <parameter name="filtergroup">
        <values>
            <parameter name="key1" value="value1" />
            <parameter name="key2" value="value2" />
        </values>
    </parameter>
</parameters>

Output would be {"filtergroup" : [{"key1" : "value1", "key2" : "value2"}]}

values can be used if we have any sub parameters (i.e., nested). If the values tag has a name attribute then it will be considered as an associative array, else a non-associative array.

For example, if values in the above xml has name attribute name="filter" then output would be {"filtergroup" : {"filter" : {"key1" : "value1", "key2" : "value2"}}}

We can go any number of nested levels with values. Like parameter -> values -> parameter. Then values can again have parameter -> values -> parameter and so on.

  1. If we just want to send array of values then inside values tag we can use value.
<parameters>
    <parameter name="fields">
        <values>
            <value>firstname</value>
            <value>lastname</value>
        </values>
    </parameter>
<parameters>

Output would be {"fields" : ["firstname", "lastname"]}

Pre-defined rules in parameters

  1. values with name will be an associative array, else a non-associative array
  2. values with value inside will be taken as array of values
  3. values with parameter inside will be as array of object

Dynamic values

In the parameters we use dynamic values, which will be calculated and used while hitting pull request

  1. @lastsynctoken - In general sync works based on the last sync time. We need to send filters to the service to give us data that is changed after last sync time. For this, we enabled this dynamic parameter. Wherever we use this parameter, while hitting pull request to service it will take the last sync time.
  2. @lastsynctoken:M - lastsynctoken is a unix timestamp. If we want to send milli seconds in the the lastsynctoken then we can use this parameter.
  3. @lastsynctoken:T - some services expect value in T format. T is just separator of Date and Time part in date time value.
  4. $config.$fields.fieldname - If config is enabled in xml when we can use the config fields. We can use these in request headers also.

Response

This will have identifiers for some required data in the response from the service.

<response format="json">
    <code use="200" />
    <error use="message" />

    <records use="results" />
    <recordid use="id" />
    <recordmodifiedtime use="updatedAt" />
    <recorddeleted use="archived" />                
</response>
  • code = Expected response code. We can use comma separated values without space.
  • error = What value will have error message if request is failed
  • records = What object in the response holds the records. If entire response is records object then we can set use = @response

In the records object each record will have few required details for which we need to configure the identifiers.

  • recordid = what is the identifier for record id
  • recordmodifiedtime = What is the identifier for record last updated time. This is must in the response, because we are using this to store the last sync token.

If response will not have this value then we can use @currenttime which will use current time

  • recorddeleted = If there is any identifier to know the record is deleted or archived

Append fields Here appendfields node is using to re-rwite the record with new values or updated values.

<appendfields>
    <request>
        <url URL="$config.$fields.baseUrl" ID="@id"> $URL/v1/deals/$ID/products </url>
        <response>
            <line_items use="data"></line_items>
        </response>
    </request>
</appendfields>

Note : If any one of these things is missing then sync may misbehave!

Push This helps with configuring details for pushing data from Vtiger to Service. Like pull we will have

Request This will also have endpoint, headers, and parameters. Like pull, If the service what we are trying to sync with, has module specific endpoint, headers or parameters then we can overwrite the request for that module using that module tag inside the main request

This is how push request is going to look like

<request method="post" >
    <url SYNCMODULE="@syncmodule">/$SYNCMODULE/batch</url>

    <headers>
        <header name="content-type" value="application/json" />
    </headers>

    <parameters raw-post-data="true">
        <parameter name="inputs" value="@records"> </parameter>
    </parameters>
</request>

Modes While pushing we will have to create, update or delete on the service side. Some services might have different endpoints, parameters for these actions. In that case we can add those modes with urls which will be appended to the push endpoint. We can overwrite parameters also in these modes

In these modes we can have following tags.

  1. url - endpoint of that mode
  2. batchlimit - if the service is only accepting 10 records at a time we can put 10. If this is set to 1, we assume the service only does single record operations i.e., we send one record at a time and expect only one record in the response.
  3. record - record that needs to push to the service will be generated with respect to the field mapping configuration. If any new property needs to be added or overwritten then we use the property tag (please refer create/update in below code). Sometimes we want to overwrite entire record object, then we can add a overwrite attribute to record tag (please refer delete in below code)
  4. parameters - If mode has any parameters then we will consider those else we will take from request node.

Here is the push request object with modes.

<push>
    <request method="post" >
    <url SYNCMODULE="@syncmodule">/$SYNCMODULE/batch</url>

    <headers>
    <header name="content-type" value="application/json" />
    </headers>

    <parameters raw-post-data="true">
    <parameter name="inputs" value="@records"> </parameter>
    </parameters>

    <create>    
    <url>/create</url>
    <batchlimit>10</batchlimit>
    <record>
        <property name="properties" value="@record:properties" /> 
    </record>
    </create>

    <update>
    <url>/update</url>  
    <batchlimit>10</batchlimit>
    <record> 
        <property name="id" value="@record:id" />
        <property name="properties" value="@record" />
    </record>
    </update>

    <delete>
    <url>/archive</url>
    <batchlimit>10</batchlimit>
    <record overwrite="true">
        <property name="id" value="@record:id" />
    </record>
    </delete>

    </request>
</push>

Dynamic values

  1. @records - In parameters we might want to send all records where we can use this.
  2. While generating record objects for any action (create, update, delete) we might need to set record properties
  • @record - entire record array
  • @record:id - set records id where we can use this
  • @record:fieldname - If we want to set to any property from a record
  • $config.$fields.fieldname - if config is enabled in xml when we can use the config fields NOTE: Whatever nested parameters we configured in field mapping. Record will be generated that way first, then in create, update, delete tags we can access that object using @record: as identifier.

Response

We need to configure identifiers for few mandatory things

<response format="json">
    <createdcode use="200" />
    <updatedcode use="200" />
    <deleteddcode use="200" />

    <error use="message" />
    <createdrecords use="results" />

    <recordid use="id" />   
    <recordmodifiedtime use="updatedAt" />
</response>
  • createdcode = Expected response code for push create. We can use comma separated values with out spaces.
  • updatedcode = Expected response code for push update. We can use comma separated values with out spaces.
  • deletedcode = Expected response code for push delete. We can use comma separated values with out spaces.
  • error = What is the key for error message in the response
  • createdrecords = After records are created in service, it will send response which will have records with ids, we need these ids for mapping in CRM. If entire response is records object then we can set use = @response
  • recordid = How to identify record id from the response
  • recordmodifiedtime = How to identify record modified time

Value format

While using parameters in the pull/push request, sometimes we might want to format the values. For instance, if we have parameter like this which is a timestamp

<parameter name="time" value="@lastsynctoken" />

and, we want to convert this to specific date format then we can do

<parameter name="time" value="@lastsynctoken" format="dateformat:Y-m-d H:i:s" />

(or) short form

<parameter name="time" value="@lastsynctoken|dateformat:Y-m-d H:i:s" />

Likewise we support the following formats

  • int - value|int - To convert the value to an integer
  • float - value|float - To convert the value to decimal
  • string - value|string - To convert the value to string
  • array - value|aray - To wrap the string into an array
  • json - value|json - To convert any array to json
  • trim - value|trim - To trim any spaces at first and end
  • timestamp - value|timestamp - To convert date to timestamp
  • wrap - value|wrap:’ - To wrap value with any character
  • prefix - value|prefix:+ - To prefix value with any character
  • suffix - value|suffix:+ - To suffix value with any character
  • join - value|join:, - To join array with given character
  • separate - value|separate:, - To separate string with given character
  • array_values - value|array_values - To take values of array

We support multiple formats as well for same value For example, if you want to separate value by comma (,) and convert that to json then

<parameter name="field" value="value|separate:,|json" />

Each format should be separated by | symbol

This formatting is supported in URL Attributes, Headers, Parameters, Push record properties

Notes

  • Debug logs will show time in UTC format.
  • While writing xml for pull, always order the records by last modified time in ascending order. Services may give the data in random order, but we depend on fetched records last modified time to fetch the next set of records
  • By default sync records limits are fixed
    • User action - 100 per sync (Pull - 100, Push - 100)
    • CLI (cron) - 500 per sync (Pull - 100, Push - 100)
    • But, if we want to change this limit for any sync connector then we can add these global variables
      • User action - $MODULENAME_$CONNECTORNAME_SYNC_RECORDS_LIMIT
      • CLI (cron) - $MODULENAME_$CONNECTORNAME_SYNC_RECORDS_LIMIT_CLI
  • Module sync will be skipped if any dependent module is not completely synced yet

Security

Vtiger supports 2 algorithms used for encryption and decrytpion.

  • AES - Advanced Encryption Standard(AES) is a symmetric encryption algorithm. Uses key - 32 bits and iv is not mandatory in this algorithm.
  • 3EDS - Triple Data Encryption Standard. Uses Key- 24bits and iv - 8bits. iv is mandatory for using this algorithm.

key and iv are the variables which are enabled in sync connector which helps in encyrption and decryption. Add the key and iv input in config node.

Add the algorithm by deafult while building the sync connector as:

<config>
    <fields>
        <field name="key" label="KEY" type="text" required="true"/>
        <field name="Algo" label="Algo" type="text" value="AES"  />

    </fields>
</config>

To decrypt the response data from the service.

Add the following in the response of pull request.

<algo use="AES"></algo>
<decrypt use ="true" />
<iv use="$config.$fields.iv"/>
<key use="$config.$fields.key"/>

This will decypt the data from the service (which is in encrypted format using the same algorithm).

Here algo node is used to add the algorithm ie, AES or 3DES.

decrypt,key,iv are the nodes.

During the push operation.

We will encrypt the data before passing to the service or provider.

Use the below format inside push node.

<push>
    <request method="post" encrypt="true">
            <headers>  </headers>
            <parameters raw-post-data="true"  key="$config.$fields.key" iv="$config.$fields.iv"  algo="AES">
                <parameter name="_raw_data" value="@records"><parameter>
            </parameters>
    </request>
 </push>

This will encyrpt the data using the algorithm provided.

Resources

Find below the detailed runtime references to make your implementation simple and stonger:

VTAP JS

App Creator provides Javascript runtime under VTAP which provides helper APIs to interact with CRM.

VTAP.Component

Component is the representation of UI element on the page. It controls the presentation of data based on UI state.

.Core

You can create custom components by extending core component as follows:

var MyModule_Component_BasicButton = VTAP.Component.Core.extend({});

To create Settings View component for MyModule you should use:

var MyModule_Component_SettingsView = VTAP.Component.Core.extend({});

Core component has lifecycle hooks as of VueJS 2.x read more

var MyModule_Component_RegisterButton = VTAP.Component.Core.extend({
    props: {
        /* parent to this component */
    },
    data() {
        return {
            /* self data variables */
        }
    },
    components: {
        /* dependent components */
    },

    template: `<div>Custom component HTML structure</div>`,

    created() {
        /* setup listener to events (or) fetch data */
    },

    mounted() {
        /* UI element is mounted to DOM */
    }
})

.Load

VTAP.Component.Load(name, module) - Load and return Vtiger Component Object.

Parameters

namestring
modulestring

Returns

objectVtiger Component

.Find

VTAP.Component.Find(name) - Find Vtiger Component Object defined by name.

Parameters

namestring
modulestring

Returns

objectVtiger Component

.Register

VTAP.Component.Register(type, data, component, filter) - Add and Return custom component on a Page.

Parameters

typestringtype of component to be added, click here for complete list with examples.
dataobjectMany components only depend on data to show in UI, these are generally key-value pair values. See component types for examples
componentVtiger ComponentCustom component that you want to render in the UI.
filterobjectUse this when you want to restrict the component to be loaded for a particular module. For example {‘module’:’Contacts’)

Returns

objectVtiger Component

This API allows subscribing to UI Event Hooks that are emitted by core or custom components on the page. Visual representation of the hooks is shown below:

See all UI Hooks here

VTAP.View

Get current page view name like list, detail etc.

VTAP.User

Get logged in user details like first name, last name, email, profile image, isadmin etc.

VTAP.Utility

Provides utility functions like showing modal, show success/error UI notification etc.

.ShowSuccessNotification

VTAP.Utility.ShowSuccessNotification(message)

Shows success UI notification - useful to inform success after the action.

messagestringThe message that needs to be shown in notification. Default values to "Success".

.ShowErrorNotification

VTAP.Utility.ShowErrorNotification(message)

Shows the error UI notification - useful to inform users about failed actions.

messagestringThe message that needs to be shown in notification. (default=error)

.ShowPopup

VTAP.Utility.ShowPopup({component, componentData, modalOnModalMode})

Shows a popup modal

componentVtiger ComponentThis is Vtiger Vue component, see here for more details.
componentDataobjectIt will hold parameters that need to be sent to the Vtiger component to be shown in popup.
modalOnModalModebooleanIf there is another popup then do you want to show the modal on top of it or not. (default=false)
VTAP.Utility.ShowPopup({
    component : VTAP.Component.Load('Relation', 'MYMODULE'),  
    componentData : {title : 'List of records'}, 
    modalOnModalMode : true
});

var MYMODULE_Component_Relation = VTAP.Component.Core.extend({
   template:
   //We are using boostrap-vue components b-modal here. Check [here](https://bootstrap-vue.org/docs/components/modal) more details.
      `<b-modal id="modal-1" title="Vtiger popup">
          <p class="my-4">Add popup contents here</p>
       </b-modal>`
});

.HidePopup

VTAP.Utility.HidePopup('modal-1') - This will hide the popup opened by ShowPopup API. 'modal-1' is the id value from the above pop up template example.

.UserLabels

VTAP.Utility.UserLabels() - This returns a list of all the CRM users and their ids. This is useful to identify owner of the record as API's return back in the form of ID.

.ShowProgress

VTAP.Utility.ShowProgress() - This can be used to when some background tasks like fetching data from the server is initiated, and inform user to wait for the job to complete.

.HideProgress

VTAP.Utility.HideProgress() - This is used to hide the progress bar initiated by ShowProgress API.

.ShowRecordPreview

This API lets you see the preview of a CRM record, given you have crm ID and the name of the module.

idstringThis is the CRM record ID.
modulestringModule name
VTAP.Utility.ShowRecordPreview('1234', 'Contacts')

.ShowRecordCreate

VTAP.Utility.ShowRecordCreate(record,module,callback)

Shows a quickcreate popup modal

recordobjectHere we need to send params to which quickcreate modal fields will be populated with that data
modulestringModule name
callbackfunctionuser handler function
VTAP.Utility.ShowRecordCreate(
        {'fieldname':'fieldvalue'},
        'MODULENAME',
        ()=>{}
);

var MYMODULE_Component_ComponentName = VTAP.Component.Core.extend({
    //Here record is injected by the VTAP framework.
    props : ['record'],
    created() {
        var showRecordCreate = ()=>{
            VTAP.Utility.ShowRecordCreate(this.record,'MODULENAME',()=>{});
        };
        VTAP.Component.Register('DETAIL_BASIC_BUTTON', 
                                {label:'Custom', 
                                icon:'fa-pencil', 
                                variant:'primary', 
                                clickHandler : showRecordCreate
                            }, '', {module:'MODULENAME'});
   }
});

.ShowRecordEdit

VTAP.Utility.ShowRecordEdit(record,module,callback)

Shows a edit popup modal of a record

recordobjectVtiger_Record_Model object
modulestringModule name
callbackfunctionuser handler function
let record = Vtiger_Record_Model.getCleanInstance();
record.set('id', '12345');

VTAP.Utility.ShowRecordEdit(record, 'Contacts', () => {
    //callback function
});

.ShowRecordDelete

VTAP.Utility.ShowRecordDelete(record_id,module)

Shows a delete popup confirmation of a record.

record_idnumberVtiger_Record_CRMID
modulestringModule name
VTAP.Utility.ShowRecordDelete(record_id, 'Contacts', () => {
    //callback function
});

VTAP.List

Provides helper function to work on List View Page.

.Records

VTAP.List.Records() returns current record objects available in the list page.

Returns void

.NextPageExist

VTAP.List.NextPageExist() - checks if there are records in the next page.

Returns

BooleanReturns true if records exists in next page, if not then returns false

.PrevPageExist

VTAP.List.PrevPageExist() : checks if there are records in the previous page.

Returns

BooleanReturns true if records exists in previous page else returns false

.NextPage

VTAP.List.NextPage() : moves the current page to the next page.

Returns void

.PreviousPage

VTAP.List.PreviousPage() : moves the current page to the previous page if it exists.

Returns void

.Reload

VTAP.List.Reload() : reloads the current page, all the meta data are retained like page, search, ordering etc. It only reloads the list page records not the entire page.

Returns void

VTAP.List.Search(searchobject) : It searches for the search string, you can search for multiple fields. It searches for those fields that are available in the UI.

Parameters

searchObjectobjectjavascript map of fieldname and search string.
VTAP.List.Search({'firstname':'John', 'lastname':'wick'})

.ClearSearch

VTAP.List.ClearSearch() : It will remove all the search values applied for all the fields.

Returns void

.Sort

VTAP.List.Sort(fieldname, order) : It will sort the list page with the fieldname.

Parameters

fieldnamestringName of the field on which sort has to be applied
orderstring'asc' for ascending order, 'desc' for descending order. (default=desc)

.SelectedRecords

VTAP.List.SelectedRecords() : It gives a list of records selected in the list page.

Returns

PromiseReturns Promise function, use a handler to get the list of records.
VTAP.List.SelectedRecords().then( (records) => {
    console.log(records);
});

VTAP.Detail

Provides helper functions to work with Detail View Page.

.Id

VTAP.Detail.Id() : If you are in the detail page, then it returns record id.

Returns

integerit returns record id

.Module

VTAP.Detail.Module() : This function returns the name of the module.

stringit returns module name

.Record

VTAP.Detail.Record() : It returns a current record object.

Returns

PromiseIt returns Promise function, use handler to get record object
VTAP.Detail.Record().then( (record) => {
        //record is of type Vtiger_Record_Model
})

.RefreshRecord

VTAP.Detail.RefreshRecord() : This function is used to refresh the record in detail view when any PUT or POST action is performed.

VTAP.Detail.RefreshRecord();

.Relations

VTAP.Detail.Relations() : This function returns all the relationship meta with the current record(module).

Returns

PromiseIt returns Promise function, use handler to get relation objects
VTAP.Detail.Relations().then( (relations) => { });

.Relation

VTAP.Detail.Relation(module) : Use this function to get relation meta for a particular module.

Parameters

modulestringmodule name of the relation

Returns

PromiseIt returns Promise function, use handler to get relation object
VTAP.Detail.Relation('Contacts').then( (relations) => { });

.RelatedRecords

VTAP.Detail.RelatedRecords(module, filterobject) : It returns related records of the current record.

Parameters

modulestringmodule name of the relation
filterobjectobjectfilterobject lets you set a page, filter conditions etc.
extrafieldsarrayUse this parameter to include extra fields beyond the default ones, such as fields that are not enabled for Quick Create, Header, etc.
You can include the label field to fetch the record's name in the API's response.

Returns

PromiseIt returns Promise function, use handler to get filtered records
VTAP.Detail.RelatedRecords('Contacts', {page : 2, filter : [] }).then(
(records) => {
    console.log(records);
});

Assume you are working with the Contacts module and want to retrieve related Case records that include fields such as casechannel and slastatus, which are not part of the default response. You can now easily include these fields in your API request.

VTAP.Detail.RelatedRecords("Cases", { 'extrafields': ['casechannel', 'slastatus'] })
.then((case_records) => {
    // Process the retrieved case records with extra fields
});

When fetching related Case records, you might want to include the record's name for easy identification. Simply pass label as an additional field.

VTAP.Detail.RelatedRecords("Cases", { 'extrafields': ['label'] })
.then((case_records) => {
    // The response now includes the label (name) of each case
});

.RelatedRecordsCount

VTAP.Detail.RelatedRecordsCount(module, filterobject) : It gives you total records available in the relation.

Parameters

modulestringmodule name of the relation
filterobjectobjectfilterobject lets you set filter conditions etc.

Returns

PromiseIt returns Promise function, use handler to get filtered records
VTAP.Detail.RelatedRecordsCount('Contacts', {filter : [ ] }).then( (count) => { });

.ShowRelatedRecordCreate

VTAP.Detail.ShowRelatedRecordCreate(record,module,callback) : It opens QuickCreateRelatedModal popup and allows to create related records

recordobjectGet detail recordmodel send as record object ,you can see here for reference
modulestringRelated Module name
callbackfunctionuser handler function
var MYMODULE_Component_DetailButton = VTAP.Component.Core.extend({
    var showRelatedRecordCreate = ()=>{
        VTAP.Detail.Record().then((record)=>{
                    VTAP.Detail.ShowRelatedRecordCreate(record,'Tasks',()=>{});
            })
    };
    VTAP.Component.Register('DETAIL_BASIC_BUTTON', 
                                {label:'Custom', 
                                icon:'fa-pencil', 
                                variant:'primary', 
                                clickHandler : showRelatedRecordCreate
                            }, '', {module:'Deals'});
});

VTAP.Modules

Provides helper function to get access to module meta information on any page.

VTAP.Modules.CurrentModuleName

VTAP.Modules.CurrentModuleName() : returns current module name.

Returns

stringmodule name

VTAP.Modules.CurrentModuleModel

VTAP.Modules.CurrentModuleModel() : It returns current module meta information.

Returns

PromiseIt returns Promise function, use handler to get module object
VTAP.Modules.CurrentModuleModel().then( (moduleObject) => { });

VTAP.Modules.GetModule

VTAP.Modules.GetModule(modulename) : It returns module meta information for passed modulename.

Parameters

modulenamestringname of the module

Returns

PromiseIt returns Promise function, use handler to get module object
VTAP.Modules.GetModule('Contacts').then( (moduleObject) => { })

VTAP.Modules.GetAll

VTAP.Modules.GetAll() : It returns list of accessible modules

Returns

PromiseIt returns Promise function, use handler to get module list
VTAP.Modules.GetAll().then( (moduleList) => { });

VTAP.Api

Provides helper functions to interact with core product APIs. For more details on see here

VTAP.Api.Get

VTAP.Api.Get(uri, parameters, callback) : Performs get request on the uri and returns the response to the callback handler.

Parameters

uristringunique server resources, see here for the [list] (Core-server-resources)
parametersobjectparameters required for the uri, it is to be given in key-value format
callbackfunction(error, success)handler function gets response from server

Example: To retrieve record information

VTAP.Api.Get('records', {id : VTAP.Detail.Id(), 
  module : VTAP.Detail.Module()}, (error, response) => {
    //response has record data
});

VTAP.Api.Post

VTAP.Api.Post(uri, parameters, callback) : It is used to add new resources to the server.

uristringunique server resources, see here for the [list] (Core-server-resources)
parametersobjectparameters required for the uri, it is to be given in key-value format
callbackfunction(error, success)handler function gets response from server

Example : To add a Contact record

VTAP.Api.Post('records', {module : 'Contacts', firstname : 'John', 'lastname' : 'Wick', 'email' : 'john@wick.com'}, (error, response) => {
    //response will have record data
});

VTAP.Api.Put

VTAP.Api.Put(uri, parameters, callback) : It updates an existing resource on the server.

uristringunique server resources, see here for the list [list] (Core-server-resources)
parametersobjectparameters required for the uri, it is to be given in key-value format
callbackfunction(error, success) handler function gets response from server

Example: To update Contact record, id is the record crmid or the id you see in the url of detail page.

VTAP.Api.Put('records', {module : 'Contacts', id : '123', 'email' : 'newjohn@newwick.com'}, (error, response) => {
    //response will have record data
});

VTAP.Api.delete

VTAP.Api.delete(uri, parameters, callback) : This function deletes a resource on the server.

uristringunique server resources, see here for the [list] (Core-server-resources)
parametersobjectparameters required for the uri, it is to be given in key-value format
callbackfunction(error, success)handler function gets response from server

Example: To delete a Contact record.

VTAP.Api.Delete('records', {module : 'Contacts', id : '123'}, (error, response) => { });

VTAP.CustomApi

Provides helper functions to invoke API defined through API Designer using logged in user context. Signature of the functions are similar to VTAP.Api scope.

VTAP.CustomApi.Get

VTAP.CustomApi.Get(uri, parameters, callback) : Performs get request on the uri and returns the response to the callback handler.

uristringunique server resources created from API Designer
parametersobjectparameters required for the uri, it is to be given in key-value format
callbackfunction(error, success)handler function gets response from server

Example: To retrieve weather of a Mailing City of a Contact (get_weather added from Api Designer)

VTAP.Detail.Record().then( (record) => { 
    VTAP.CustomApi.Get('get_weather', {'city' : record.mailingcity}, 
        (error, response) => {
            //response has record data
        });
});

VTAP.Event

Provides helper functions register, unregister and listen on Vtiger UI Events.

VTAP.Event.Register

VTAP.Event.Register(eventname, handlerName) : It allows you to register for vtiger events or custom events.

eventnamestringname of the event, list of supported events are here
handlerNamefunctionhandler function

Example: To listen for detail preview popup to show up.

function handler() { }
VTAP.Event.Register('DETAIL_PREVIEW_SHOWN', handler);

VTAP.Event.DeRegister

VTAP.Event.DeRegister(eventname, handlerName) : You can de-register for events that you have registered with the Event.Register API. Parameters and return type are the same as the Register API.

Example :

function handler() { }

VTAP.Event.DeRegister('DETAIL_PREVIEW_SHOWN', handler);

VTAP.Event.Trigger

VTAP.Event.Trigger(eventname, data) : You can trigger custom events using this API, generally used when you have custom buttons, links and widgets.

eventnamestringname of the event, list of supported events are here
dataobjectkey value pair of data that you want to send to the listening handler.

Example: To trigger a custom event on a page..

VTAP.Event.Trigger('MY_CUSTOM_EVENT', ({"id":VTAP.Detail.Id(), "module":VTAP.Detail.Module()});

VTAP.Notification

Provides helper functions to work with real-time notifications.

VTAP.Notification.Trigger

VTAP.Notification.Trigger(module, data) : This is used to send real-time notification from client-side to other connected users on the same module.

modulestringname of the module
dataobjectkey value pair of data that you want to send to the listening handler.

Example: To trigger notification on Contact Detail View for all connected users.

VTAP.Notification.Trigger('Contacts', ({"id":VTAP.Detail.Id(), "user_name":VTAP.User().user_name, "mode":"accessed"});

Example: To trigger notification on Contact Detail View for only specific connected users.

VTAP.Notification.Trigger('Contacts', ({"id":VTAP.Detail.Id(), "user_name":VTAP.User().user_name, "mode":"accessed","users": [1, 6]});

VTAP.Notification.Listen

VTAP.Notification.Listen(module, callback): This helps you listen on the event notification sent using VTAP.Notification.Trigger api.

modulestringname of the module
callbackfunctionuser handler function

Example: To listen to notification triggers.

VTAP.Notification.Listen('Contacts', (data) => {
    //data has the custom info send from Notification.Trigger api
});

VTAP.AppData

VTAP.AppData.Save(module, data, callback) : It saves the data for the module.

modulestringname of the module
dataobjectdata_key is used to store the key and data_value is where actual data is stored. Use onlyadmin flag to allow only admins to read/write. Ex : ({"data_key":"secret", "data_value":"wrfs3fr","onlyadmin":true})
callbackfunctionuser handler function

Example:

VTAP.AppData.Save("my_extension_name", {"data_key" : "username", "data_value" : "something@email.com"}, (error, success) => {

});

.Get

VTAP.AppData.Get(module, data, callback) : This is used to fetch the app data that is stored using VTAP.AppData.Save api.

modulestringname of the module
dataobjectdata_key is used to retrieve the key. Ex : ({"data_key":"secret"})
callbackfunctionuser handler function
VTAP.AppData.Get("my_extension_name", {"data_key":"username"}, 
(error, success) => {

});

.Delete

VTAP.UserData.Delete(module, data, callback) : It will delete the data saved by UserData.Save api.

VTAP.UserData.Delete("extension_name", {"data_key" : "conversation", "id" : "23"}, (error, success) => {

});

VTAP.Resource

Provides helper functions to include external resources like Javascript libraries, style sheets.

.Require

VTAP.Resource.Require(path, type) : This will let you add external resources to use them in your components.

pathStringPath to external script
typeStringDefault value is “script”, if you are including style sheets then set type as “style”
VTAP.Resource.Require('https://unpkg.com/leaflet@1.6.0/dist/leaflet.js');

Note: Before using any resource you need to whitelist the domain in Module Designer > Settings > Add Domain

VTAP.OAuth

Provides a helper function to gather the OAuth grant from the user of the application to further interact with APIs of third-party-services (setup through API designer).

.Authorize

This will open up the service that is registered for the module. This depends on the oauth client app that you have registered in your application.

Any OAuth needs an client app to be registered, this setup is generally done by the administrators. Admins will be first prompted to provide client details like Client ID, Client Secret, Auth URL, Token URL and Scopes. Refer the screenshot here, once the setup is done then only other users will be able to use it.

After saving the details, if they are correct then we will redirect and open popup requesting grant from the target application.

serviceStringname of the service
moduleStringname of the module
callbackfunctionuser handler function
VTAP.OAuth.Authorize("3PService", "Contacts", (error, success) => {

});

.IsAuthorized

Checks if a service is already authorized or not.

VTAP.OAuth.IsAuthorized("3PService", "Contacts", (response) => {
    if(response.authorized) {
    //user has authorized
    }
});

.Revoke

Removes oauth tokens which has earlier obtained using Authorize api.

VTAP.OAuth.Revoke("3PService", "Contacts", (response) => {
});

.ShowAuthClientDetailsPopup

Show oauth client app details which was earlier saved by admins. This should generally be used when admins wants to change client secret or scopes.

VTAP.OAuth.ShowAuthClientDetailsPopup("3PService", "Contacts");

VTAP.OAuth.DeleteAuthClientDetails

Revokes a oauth client app details which was earlier saved by admins.

VTAP.OAuth.DeleteAuthClientDetails("3PService", "Contacts").then((success){
    //success   
}, (error) => {
    //failure
});

VTAP.Field

This object exposes api's that will help you add custom behaviour on the Vtiger Fields. Fields are pieces of information which make a CRM record. For example Contact first name, last name, Company Name are considered as field in Vtiger.

VTAP.Field.RegisterValidator

This API lets you register custom field validation. You may want to avoid your agents making mistakes of filling incorrect values in the CRM. For example, VAT number should be strictly 16 character length, or tracking shipping delivery code should

fieldnameStringname of the field where custom validation should be applied
moduleStringname of the module
handlerFunctionfunctionthe function will have the logic to decide the validation rules, and returns true/false
errorHandlerfunctionreturns the error message that should be shown when validation fails.
//Adding validation for Contact's First name to be not empty. 
VTAP.Field.RegisterValidator('firstname','Contacts', (value) => { 
    if(value == '') {
        return false;
    }
}, (error) => { 
    return "First name cannot be empty"; 
});

//Adding validation for Contact's Mobile number cannot be less than 10 digit 
VTAP.Field.RegisterValidator('mobile','Contacts', (value) => { 
    if(value.length < 10) {
        return false;
    }
}, (error) => { 
    return "Mobile should be atleast 10 digits!!"; 
});

VTAP.Field.RegisterValidatorForType

This API will enable you to add validation for field type. For instance if you want to add same validation to all the email fields, or phone fields or url fields then you need to use this field. Another example is you want to avoid adding any email address in any of the email field with gmail.com/outlook.com in Contacts module. The definition of the API is same as RegisterValidator, except the first argument is field type instead of field name.

fieldtypeStringfield type like 'email', 'phone', 'url', 'multicurrency', 'picklist', 'text', 'boolean', 'percentage', 'richtext', 'image', 'file', 'metricpicklist', 'grid', 'datetime', 'radio', 'anniversary', 'owner'
moduleStringname of the module
handlerFunctionfunctionthe function will have the logic to decide the validation rules, and returns true/false
errorHandlerfunctionreturns the error message that should be shown when validation fails.
//Adding validation for Contact's First name to be not empty. 
VTAP.Field.RegisterValidatorForType('email','Contacts', (value) => { 
    if(value != '' && value.indexOf('gmail.com') !== -1) {
        return false;
    }
}, (error) => { 
    return "We do not accept gmail addresses"; 
});

//Adding validation for Contact's Mobile number cannot be less than 10 digit 
VTAP.Field.RegisterValidatorForType('phone','Contacts', (value) => { 
    if(value.length < 10) {
        return false;
    }
}, (fieldLabel) => { 
    return fieldLabel+" should be atleast 10 digits!!"; 
});

Component Types

TypeDescriptionExample
LIST_ADD_VIEWTYPEAdd custom views like kanban, calendar view in list view.VTAP.Component.Register('LIST_ADD_VIEWTYPE', {name : 'ChartView', icon : 'fa-barchart', 'label' : Chart View'}); Note : Once added it will search for a component named “ChartView”, you need to register component Vtiger_Component_ChartView = VTAP.Component.Core.extend({ }); To understand the typical structure of a vtiger component click here
LIST_ADVANCED_SETTINGIn the List page we show settings icons next to navigation for admins, your custom option will be available here. Generally used to store extension settings. VTAP.Component.Register('LIST_ADVANCED_SETTING', {name : 'Workflows', clickHandler : () => { } });
LIST_BASIC_BUTTONAdd a custom button in the list page next to the Add Record button.VTAP.Component.Register('LIST_BASIC_BUTTON', {label:'Custom', icon:'fa fa-pencil', varient:'btn btn-primary', clickHandler: () => { } }, '', {module:'Contacts'}); For icon you can use any font awesome icon class, and for varient you can use any bootstrap button class. clickHandler is a function.
LIST_ROW_BASIC_ACTIONEvery list row has actions on the right side, your component can add any element but recommended is an icon.VTAP.Component.Register('LIST_ROW_BASIC_ACTION', {icon:'fa fa-plus',label:'Send email',clickHandler: () => {}}, '', {module:'Contacts'});
LIST_ROW_SECONDARY_ACTIONThis will add a component to the left side of every row in the list page.VTAP.Component.Register('LIST_ROW_SECONDARY_ACTION', {icon:'fa fa-plus',label:'Send email',clickHandler: () => {}}, '', {module:'Contacts'});
LIST_MASS_ACTIONIt will add an icon in the list view mass actions.VTAP.Component.Register('LIST_MASS_ACTION', {icon:'fa-pencil',name:'Edit',clickHandler:()=>{},showHandler:()=>{}}, false, {module:'Contacts'});
GLOBAL_ACTIONIt will add a component at the top of the page where search and other actions are available. These actions will be available in all the pages.VTAP.Component.Register('GLOBAL_ACTION', {icon:'fa fa-plus',label:'Send email',clickHandler: () => {}}, '', FILTER);
DETAIL_ONEVIEW_WIDGETIt will let you add widget in One view related tab of the detail pageVTAP.Component.Register('DETAIL_ONEVIEW_WIDGET', {}, VTAP.Component.Load('NAME','MODULE'), FILTER);
DETAIL_MORE_ACTION_ITEMAdd custom actions from 3 dots on the top right side of the detail page.VTAP.Component.Register('DETAIL_MORE_ACTION_ITEM', {icon:'fa fa-plus',label:'Send email',clickHandler: () => {}}, '', FILTER);
DETAIL_BASIC_BUTTONAdd a button in detail view header where other actions are available.VTAP.Component.Register('DETAIL_BASIC_BUTTON', {label:'Custom', icon:'fa fa-pencil', varient:'btn btn-primary', clickHandler: () => { } }, '', FILTER);
DETAIL_ACTION_ICONAdd an actionable icon in more actions(3 dots) in the detail page.VTAP.Component.Register('DETAIL_ACTION_ICON', {name:'',icon:'',showHandler:() => {},clickHandler: () => {}}, '', FILTER);
DETAIL_HEADER_WIDGETAdd component in the detail page header.VTAP.Component.Register('DETAIL_HEADER_WIDGET', {}, VTAP.Component.Load("COMPONENT_NAME","MODULENAME"), FILTER);
DETAIL_RELATED_RECORD_ACTIONAdd a custom action for records appearing in the related list.VTAP.Component.Register('DETAIL_RELATED_RECORD_ACTION', {icon:'fa fa-plus',label:'Send email',clickHandler: () => {}}, '', FILTER);
DETAIL_MORE_ACTION_ACTIVITY_ITEMAdd custom action under the detail page with more actions, inside “Reach out now” section.VTAP.Component.Register('DETAIL_MORE_ACTION_ACTIVITY_ITEM', {icon:'fa fa-plus',label:'Send email',clickHandler: () => {}}, '', FILTER);
DETAIL_SUMMARY_WIDGETAdd custom widget in detail page summary view.VTAP.Component.Register('DETAIL_SUMMARY_WIDGET', {}, VTAP.Component.Load('NAME','MODULE'), FILTER);

Event Types

List of UI Events triggered from core / custom components on different pages.

TypeDescriptionExample
DETAIL_PREVIEW_SHOWNWhen record detail preview popup is shown.VTAP.Event.Register('DETAIL_PREVIEW_SHOWN', (data) => { });
DETAIL_PREVIEW_HIDDENWhen record detail preview popup is hidden.VTAP.Event.Register('DETAIL_PREVIEW_HIDDEN', (data) => { });
DETAIL_ALLFIELDS_SHOWNWhen record detail page all field popup is shown. See here.VTAP.Event.Register('DETAIL_ALLFIELDS_SHOWN', (data) => { });
DETAIL_ALLFIELDS_HIDDENWhen record detail page all fields popup is hidden.VTAP.Event.Register('DETAIL_ALLFIELDS_HIDDEN', (data) => { });
EDIT_MODAL_SHOWNWhen record edit page is shown.VTAP.Event.Register('EDIT_MODAL_SHOWN', (data) => { });
EDIT_MODAL_HIDDENWhen record edit page is hidden.VTAP.Event.Register('EDIT_MODAL_HIDDEN', (data) => { });
CREATE_MODAL_SHOWNWhen record create page is shown.VTAP.Event.Register('CREATE_MODAL_SHOWN', (data) => { });
RECORD_CREATEDWhen record is created from any where inside the crm.VTAP.Event.Register('RECORD_CREATED', (module, record) => { });
RECORD_UPDATEDWhen record is updated from any where inside the crm.VTAP.Event.Register('RECORD_UPDATED', (module, record) => { });
RECORD_SAVEDWhen you want to listen to both record created/updated from any where inside the crm.VTAP.Event.Register('RECORD_SAVED', (module, record) => { });
RECORD_DELETEDThis event is triggered after record is deleted. This is triggered from detail page.VTAP.Event.Register('RECORD_DELETED', (module, id) => { });
MASS_RECORD_DELETEDThis event is triggered after multiple records are deleted from list page.VTAP.Event.Register('MASS_RECORD_DELETED', (module, ids) => { });
ONE_VIEW_RELOADYou can refresh one view related lists by triggering this event. This should be done after you add/update record from one view section.VTAP.Event.Trigger('ONE_VIEW_RELOAD');
EVENT_SHOW_INVITEESTriggers the display of the Invitees block.VTAP.Event.Trigger('EVENT_SHOW_INVITEES');
EVENT_HIDE_INVITEESHides the Invitees block.VTAP.Event.Trigger('EVENT_HIDE_INVITEES');

Client Framework

Vtiger CRM frontend client framework is developed using VueJS 2.x components which serves as building block. UI page, widget, chart, listview in CRM is built using tailored components. These components can be further reused by finding their name read more

The VTAP.Component.Core can be extended to create custom components with naming convention:

ModuleName_Component_ComponentName

To create WeatherWidget component for Contacts module you will have define it as follows:

var Contacts_Component_WeatherWidget = VTAP.Component.Core.extend({});

Each component comprises of following essential properties or methods:

NameTypeDescription
datafunctionshould return object having variable and default value. variables can be in templates or methods
propsarraylist of tag attributes that can be used when embedded in HTML
componentsobjectdependent component-name mapped to target component instance.
methodsobjectinstance specific functions.
createdfunctionlife-cycle function called when instance of component is created.
mountedfunctionlife-cycle function called when component is mounted to DOM.
templatestringHTML content that will be transformed to DOM.

There are other properties but we have defined the commonly used ones only here, if you are interested to learn more then you can give this a read. A skeleton structure of a component is shown below.

var Contacts_Component_WeatherWidget = VTAP.Component.Core.extend({
    props : { 
        //add props from the parent
    }, 
    data() { 
        return {
            //add your component data variables here
        }
    }, 
    components :{ 
        //add other child components here if used in templates
    },
    computed : {
        //add computed properties
    },
    created() {
        //add other component registration or listen to vtiger events
        //perform ajax request required for your component.
    },
    mounted() {
        //this is called after component is mounted on the DOM
    },
    template :
        `<div>Custom component structure</div>`
});

VCAP

Vtiger Custom Application runtime (a.k.a VCAP) is a helper runtime script designed for applications built using VTAP's App Creator. It simplifies the process of interacting with CRM data and user APIs, making it easier for developers to retrieve or post records, trigger events, and manage configurations.

VCAP.userapi

.records.get

VCAP.userapi.records.get(params, cb) performs a GET request to retrieve records from a specified module in the Vtiger CRM. It uses the provided filter ID and search criteria to fetch the records and returns them through the callback function.

paramsobjectparameters required for the API call, it is to be given in key-value format
cb(callback)function(error,success)handler function gets response from server

Example: To retreive record information

VCAP.userapi.records.get({module: module, filterid: filterid,
    q: JSON.stringify(   searchWithOrClause(fields, term) )}, (e, response) => {
        //response has records data
});

.records.post

VCAP.userapi.records.post(params, cb) performs a POST request to create a new record in a specified module within Vtiger CRM. It sends the record data as parameters and returns the result through the callback handler.

paramsobjectparameters required for the API call, it is to be given in key-value format
cb(callback)function(error,success)handler function gets response from server

Example: To add an Event Record

VCAP.userapi.records.post({
    module      : "Events",
    subject     : "Testing with userapi ",
    eventstatus : " ",
    activitytype: " ",
    date_start  : showdate,
    time_start  : "09:00", 
    due_date    : showdate,
    time_end    : "10:30",
    contact_id: contactid
}, (e, response) => {
    //response will contain record data
});

VCAP.customapi

.get

VCAP.customapi.get(CustomApiName, parameters, cb) performs a GET request to a custom API endpoint defined in the Vtiger CRM. It sends parameters to the API and handles the response via a callback function.

CustomApiNameStringName of the Custom API you want to send request to.
You can checkout more about custom apis here
parametersobjectparameters required for the API call, it is to be given in key-value format
cb(callback)function(error,success)handler function gets response from server

Example:

VCAP.customapi.get("SearchTMDB", {moviename: "xxx"
    }, (e, response) => {
        //response contains the data received from the custom api
});

.post

VCAP.customapi.post performs a POST request to a custom API endpoint defined in the Vtiger CRM. It sends data to the API and handles the response via a callback function.

CustomApiNameStringName of the Custom API you want to send request to.
You can checkout more about custom apis here
parametersobjectparameters required for the API call, it is to be given in key-value format
cb(callback)function(error,success)handler function gets response from server

Example:

VTAP.CustomApi.Post("sendRequest",{identifier:13, managerId:652,managerName: "dghfc"}, (e,res) => {
        //response containds the record data
});

VCAP.Event

.trigger

VCAP.event.trigger('eventName', args) can be used to trigger custom events using this API, generally used when you have custom buttons, links and widgets.

eventNamestringname of the Event
args

Example :

$('#alertButton').on('click', function() {
		VCAP.event.trigger('alertEvent');  // Triggers the custom 'alertEvent'
});

.on

VCAP.event.on('eventName', func()) can be used, to listen for specific events that occur within the VCAP environment and perform necessary actions accordingly.

eventNamestringname of the Event
func()functionperforms necessary action once the event is triggered

Example :

VCAP.event.on('alertEvent', function() {
		alert("Showing alert on button click.");
});

VADL

Vtiger Application Definition Language (a.k.a VADL) is custom XML based specification for developing API on VTAP.

XML format makes it easy for administrator or developer to quickly start without needing deep investment in programming skills.

Through VDAL you can define:

  • API to CREATE, RETERIVE, UPDATE or DELETE records.
  • Invoke REST or SOAP external endpoints.
  • Create Inboud API to CRM from external applications.

General structure is as follows:

<?xml version="1.0"?>
<api method="HTTP_METHOD">
    <!-- your definition here. -->
</api>

APIs

NameDescriptionExample
apientire api definition is encapsulated inside api node, method attribute set the type(get,post,put,delete) of http request.<api method='post'></api>
selectthis will let you select the vtiger record data, you can select which fields to be fetched, filter records and sort them<api method='get'><select module='Contacts'></select></api>
recordThis will have all the fields which needs to be retrieved and is placed inside select node.<record><field name="firstname"></field><field name="lastname"></field></record>
whereUse this when you want to filer the records, you can pass multiple fields. Use glue attribute(OR, AND) to decide if all or any fields should match the conditions.<where glue="OR"><field name="firstname" value="@first"></field><field name="lastname" value="@last" condition="like"></field></record>
sortThis will have fields that will sort the data, with order attribute defining descending or ascending.<sort><field name="annual_revenue" order="descending"></field></sort>
limitBy default we return 100 records, but you can pass max attribute to limit records. If there are more records, you can pass page parameter in the request get specific page records.<limit max="5" page="@page"></limit>
createthis will let you create vtiger record<api method='post'><create module='Contacts'></create></api>
updatethis will let you update vtiger record<api method='put'><update module='Contacts'></update></api>
upsertthis will let you update record if exists else create it<api method='put'><upsert module='Contacts'></upsert></api>
deletethis will help you delete vtiger record<api method='delete'><delete module='Contacts'></delete></api>
restthis will help you connect with other application<api method='get'><rest method="get"><url>https://slack.com/api/conversations.list</url></rest></api>
soapconnect with legacy soap api's<api method='get'><soap method="get"><auth><basic username="" password=""></basic></auth><url></url></soap></api>
webhookbuild incoming webhook api's<api><webhook><select module='Contacts'></select></webhook></api>

For examples on how to create custom apis, visit here or to learn how to connect to other applications click here.

VDS

Vtiger Design System (a.k.a VDS) provides core HTML components as part of VTAP. These components can be used when creating your custom pages, widgets, popups.

v-select2

Drop-down component with options.

<v-select2 :required=true :emptyOption=false 
    v-model="" :multiple=false :options=""></v-selec2>
PropertyTypeDefaultDescription
requiredboolfalsetrue - mandatory, false - optional
emptyOptionboolfalsetrue - show empty option, false - otherwise
v-modelstring-store the choice made into variable
multipleboolfalsetrue - allow multiple selection, false - otherwise
optionsobject-list of map: [ {label: 'Private', value: 'private'}. {label: 'Public', value: 'public'} ]