Skip navigation
All Places > Developer > Blog > Authors SugarCRM Developers
1 2 Previous Next

Developer

23 Posts authored by: SugarCRM Developers

Post originally written by sugarraagaard.

 

 

1. Introduction

 

So you have yourself in a bit of a bind, you need to get an existing API to do something different. Maybe you need more information on an Account record, or perhaps your custom module needs some custom handling to go along with it. This cookbook entry is going to give you a few examples on how to override the default behavior of the REST API.

 

2. Before we get started

 

Before we get started you need to have some basic idea of how to interact with the SugarCRM API from a client standpoint and The School of REST – Part 1 is a great place to start. After that you should probably learn how to add a new API so you could use that instead and for that I will point you to Adding a rest endpoint.With that taken care of let's get started on modifying an existing API.

 

3. Api Helper Classes

 

In order to facilitate different modules having different needs we added the SugarApiHelper class. You can find this class in data/SugarBeanApiHelper.php this class is used by all modules and is responsible for both formatting any bean before it is displayed to the client and also handling populating the bean from the API's requests to save/update.

 

To create a new ApiHelper for the class Contacts put a file in either custom/module/Contacts/ContactsApiHelper.php and then name the class in there CustomContactsApiHelper and be sure to extend the ContactsApiHelper. An important thing to note here is that you use the module name in front of ApiHelper, so it is Contacts instead of just Contact. For your own custom modules you can just put that ApiHelper class in your custom module's base directory. Some of our shipped modules have ApiHelpers, and others don't so if you are creating a custom api helper be sure to check if the base api helper exists and extend that if it does, otherwise just extend the core SugarBeanApiHelper class.

 

4. Overriding FormatForApi()

 

For our cookbook entry here we are going to build a method to display some information about the biggest open opportunity related to this contact.

 

To add a api helper for the Contacts module first we check for an existing helper. Out of the box helpers will be located in the modules directory and will be named the module name with ApiHelper.php on the end. So if we look at modules/Contacts/ we will see a ContactsApiHelper.php file (note: It's not ContactApiHelper.php we use the plural here). Since there is a module specific helper already when we create our custom helper we will need to be sure to extend our CustomContactsApiHelper class from the ContactsApiHelper instead of from SugarBeanApiHelper. The method we are going to override here is formatForApi(). This method is responsible for converting a bean object into an array suitable for outputting over the API, it is called from all over the API, single record views, listing records, showing lists of related records, everywhere.  One of the things it does for example is that it reformats all of the dates and times into a standardized ISO-8601 format to make it simple for the other side to parse. To get this started we will make it simple we are just overriding the formatForApi() method, calling the parent method in there and then adding some filler data to the top_opp section and returning the data. This will be an easy way for us to check and make sure our api helper is being called. If you try this and it doesn't work the first thing to do is run a quick repair and rebuild to have it rebuild the file path cache and then the second thing is to make sure you are using the plural version and you name your class correctly.

 

ContactsApiHelper.php

<?php
require_once('modules/Contacts/ContactsApiHelper.php');

// Since the ContactsApiHelper exists, we'll extend it
// If it didn't we would just extend the SugarBeanApiHelper
class CustomContactsApiHelper extends ContactsApiHelper
{
// Mimic the SugarBeanApiHelper->formatForApi() class
public function formatForApi(SugarBean $bean, array $fieldList = array(), array $options = array())
    {
// First off, call the parent class
$data = parent::formatForApi($bean, $fieldList, $options);

// Make sure they requested the top_opp field, or no field restriction at all
if (empty($fieldList) || in_array('top_opp', $fieldList) ) {
// Let's just put some filler data in here for now
$data['top_opp'] = 'Rest in Peas Burger';
        }

// formatForApi returns the bean formatted as a hash in API format
return $data;
    }
}

 

Here is what we get back in our initial response. Notice that we added a fields parameter to the list in order to make sure we fetched the top_opp field.

 

first_response.json

# GET rest/v10/Contacts?fields=id,name,top_opp&max_num=2
{
"next_offset": 2,
"records": [
        {
"id": "f2d4042e-6d9e-88af-3c00-53271512e333",
"name": "Tabitha Johansson",
"date_modified": "2014-03-17T15:29:16+00:00",
"_acl": {
"fields": {}
            },
"top_opp": "Rest in Peas Burger",
"_module": "Contacts"
        },
        {
"id": "f23e19e3-a59e-8985-4202-5327156a36a9",
"name": "Kuchi Kopi",
"date_modified": "2014-03-17T15:29:16+00:00",
"_acl": {
"fields": {}
            },
"top_opp": "Rest in Peas Burger",
"_module": "Contacts"
        }
    ]
}

 

To fill in the rest of this function we turn to our good friend SugarQuery. We build a seed bean, pass that to SugarQuery so it knows how to translate the fields then we join it to the current contact. Finally we restrict it down to the top opportunity. With that we grab the bean using fetchFromQuery() so we don't have to manually populate the bean, run that bean through the Opportunities formatForApi() to keep things nice and consistent and copy the API ready format over to the output data.

 

ContactsApiHelper.php

<?php
require_once('modules/Contacts/ContactsApiHelper.php');

// Since the ContactsApiHelper exists, we'll extend it
// If it didn't we would just extend the SugarBeanApiHelper
class CustomContactsApiHelper extends ContactsApiHelper
{
// Mimic the SugarBeanApiHelper->formatForApi() class
public function formatForApi(SugarBean $bean, array $fieldList = array(), array $options = array())
    {
// First off, call the parent class
$data = parent::formatForApi($bean, $fieldList, $options);

// Make sure they requested the top_opp field, or no field restriction at all
if (empty($fieldList) || in_array('top_opp', $fieldList) ) {
// Build a list of the fields we care about
static $oppFields = array(
'id',
'name',
'currency_id',
'amount',
'date_closed',
            );

// Make sure we don't populate this some other way
if (!isset($bean->top_opp) && !empty($bean->id)) {
// Get a seed for Opportunities
$oppSeed = BeanFactory::newBean('Opportunities');
$q = new SugarQuery();
// Set the from bean
$q->from($oppSeed);
// Join in using the 'contacts' link field
$contactJoin = $q->join('contacts');
// Limit our join to just the contact we care about
$q->where()->equals($contactJoin->joinName().'.id', $bean->id);
$q->where()->notIn('sales_stage', array('Closed Lost', 'Closed Won'));
// Sort by the "usdollar" amount, the normal amount field could be in
// different currencies and so the sorting will be all wrong.
$q->orderBy('amount_usdollar DESC');
// Just get the top opportunity
$q->limit(1);

// Use fetchFromQuery and pass in the fields we care about
$beans = $oppSeed->fetchFromQuery($q, $oppFields);

if (count($beans) > 0) {
// Even though we had a limit of 1, fetchFromQuery still returns an array
// They are indexed by ID, so let's just fetch the first and only one
$bean->top_opp = array_pop($beans);
                } else {
// Flag it so we know we tried to set it, there just wasn't anything there
$bean->top_opp = null;
                }
            }

$data['top_opp'] = array();
if ($bean->top_opp != null) {
// getHelper will get us the helper we want
$data['top_opp'] = ApiHelper::getHelper($GLOBALS['service'], $bean->top_opp)
->formatForApi($bean->top_opp, $oppFields);
            }
        }

// formatForApi returns the bean formatted as a hash in API format
return $data;
    }
}

 

Now you see that we get the top opportunity on every Contact. Also note how the numbers and the dates are formatted the exact same way as everything else in the API thanks to our call to formatForApi()

 

second_response.json

# GET rest/v10/Contacts?fields=id,name,top_opp&max_num=2
{
"next_offset": 2,
"records": [
        {
"id": "f23e19e3-a59e-8985-4202-5327156a36a9",
"name": "Kuchi Kopi",
"date_modified": "2014-03-18T16:45:29+00:00",
"_acl": {
"fields": {}
            },
"top_opp": {
"id": "f116eb68-1f2d-d6be-6d07-532715473850",
"name": "Make glowing burger",
"amount": "12171.000000",
"currency_id": "-99",
"_acl": {
"fields": {}
                }
            },
"_module": "Contacts"
        },
        {
"id": "f2d4042e-6d9e-88af-3c00-53271512e333",
"name": "Tabitha Johansson",
"date_modified": "2014-03-18T16:26:44+00:00",
"_acl": {
"fields": {}
            },
"top_opp": {
"id": "83ef4ea5-56d8-7e30-e592-532715a73955",
"name": "Build Custom Piano Burger",
"amount": "12279.000000",
"currency_id": "-99",
"_acl": {
"fields": {}
                }
            },
"_module": "Contacts"
        }
    ]
}

 

5. Overriding populateFromApi()

 

Typical relate fields in SugarCRM do not respond to changes on the parent record. This is an intentional design decision to prevent users from accidentally changing parent records. For "top_opp" however we want it to be different, we want to support changing that record directly from the related Contact. This is where populateFromApi() comes in. The symmetric method for formatForApi() this method takes API formatted data and populates it into a bean. By overriding this method in the same class we defined above we will be able to force feed the data into the top Opportunity and save it manually. To do this we just call the parent method, make sure there are no errors and that they actually submitted some "top_opp" data. After we have verified that we should populate a bean, we build a bean, retrieve it based on the passed in ID and then run it through the Opportunities ApiHelper to populate that data. If there are no errors there we manually save the Opportunity and return true so that the Contact is saved by whatever called it.

 

ContactsApiHelper.php

<?php
require_once('modules/Contacts/ContactsApiHelper.php');

// Since the ContactsApiHelper exists, we'll extend it
// If it didn't we would just extend the SugarBeanApiHelper
class CustomContactsApiHelper extends ContactsApiHelper
{
// Mimic the SugarBeanApiHelper->formatForApi() class
public function formatForApi(SugarBean $bean, array $fieldList = array(), array $options = array())
    {
// Same as before, snipped for display purposes.
    }

// Mimic SugarBeanApiHelper->populateFromApi()
public function populateFromApi(SugarBean $bean, array $submittedData, array $options = array() )
    {
$errors = parent::populateFromApi($bean, $submittedData, $options);
if ($errors !== true || empty($submittedData['top_opp']['id'])) {
// There were errors in the original, don't bother saving the opp
// Or they aren't trying to save the opp, so just pass back the parent
return $errors;
        }

// Do a full retrieve so the logic hooks on save have everything they need
$opp = BeanFactory::getBean('Opportunities', $submittedData['top_opp']['id']);
// Load up the ApiHelper class for Opportunities and have it populate from
// this subsection of data.
$oppErrors = ApiHelper::getHelper($GLOBALS['service'], $opp)
->populateFromApi($opp, $submittedData['top_opp']);
if ( $oppErrors !== true ) {
// Errors populating the opportunity
return $oppErrors;
        }
// Typically the save happens after this is all done, but since that function doesn't
// know about this extra opportunity, we'll just save it here.
$opp->save();

// Checked both the parent errors, and the opp errors, they are both true so return true
return true;
    }   
}

 

Testing it out we see that we can change the name from the related Contact. Amounts and expected closed dates could also be set in PRO editions, but ENT has some special logic around those so we can't populate them externally as easily.

 

third_response.json

# PUT /rest/v10/Contacts/f23e19e3-a59e-8985-4202-5327156a36a9?fields=id,name,top_opp
# Contents of the PUT request
{
"top_opp": {
"id": "f116eb68-1f2d-d6be-6d07-532715473850",
"name": "Make super glowing burger"
  }
}
# Returned
{
"id": "f23e19e3-a59e-8985-4202-5327156a36a9",
"name": "Kuchi Kopi",
"date_modified": "2014-03-19T10:04:51-06:00",
"_acl": {
"fields": {}
    },
"top_opp": {
"id": "f116eb68-1f2d-d6be-6d07-532715473850",
"name": "Make super glowing burger",
"amount": "12171.000000",
"currency_id": "-99",
"date_closed": "2014-09-23",
"_acl": {
"fields": {}
        }
    },
"_module": "Contacts"
}

 

6. Adding hooks to fetchFromQuery()

 

Eagle eyed readers may have spotted the fact that if you run a query in fetchFromApi() that means on a list you will run that query once per row, for efficiency sake that's not exactly the best thing to do. We will use this as a good example on how to modify list views to run alternative queries. Please note that in the future normal retrieve() calls map run through this same set of code, so be prepared for that. We've seen fetchFromQuery() run in the previous example of fetchFromApi(), so we see that it takes in a query object and returns an array of populated beans. The good news with this function is that it was designed to be hooked into using both the before_fetch_query and after_fetch_query logic hooks. The before hook is perfect for modifying the query to have it return different data for a set of fields, or return extra data that you can get at from the after hook to populate into the bean. While we could have done it using both a before and after hook I decided to make it easy and just run it as a separate query per list and run it all through the after_fetch_query hook.

 

To get started here we need to add a logic hook file into custom/modules/Contacts/logic_hooks.php that looks like this:

 

logic_hooks.php

<?php
// If you are making this module loadable, put it in an extension file instead
// These are needed if you are hand-building a logic hook file
$hook_version = 1;
$hook_array = Array();
// Setup a hook into the after_fetch_query hook
$hook_array['after_fetch_query'][] = array(
90, // Priorty, what order does this hook run in
'DemoProcessQuery', // What do we want to name this hook
'custom/modules/Contacts/DemoHook.php', // What filename contains the hook class
'DemoHook', // What is the demo hook class name
'processQuery', // What function to call in the class
);

 

Now if we look at the actual code of the custom/modules/Contacts/DemoHook.php that we referenced in the logic_hooks.php file we will notice that we skip out early if we aren't in the requested list of fields, or if there are no beans in the list. If we are clear to proceed we go ahead and build up the SugarQuery in much the same way as the previous example, except this time instead of passing in a single contact we pass in an array of contacts to match on and instead of limiting to the first result we group by the contact id and order by the amount_usdollar, this should give us the top opportunity per contact id. This will only work on MySQL, but I figured it was the easiest way to demo this. From there we run fetchFromQuery() on the Opportunities themselves and then using the raw rows that were returned we re-associate them with the parent contact.

 

DemoHook.php

<?php
class DemoHook
{
// Called from our definition in the logic_hooks file
public function processQuery($bean, $event, $args)
    {
if (!empty($args['fields']) && !in_array('top_opp',$args['fields'])) {
// They don't want the top_opp, don't process the query
return;
        }
if (empty($args['beans'])) {
// There are no results to this query, don't need to process it
return;
        }

// Same fields as formatForApi
static $oppFields = array(
'id',
'name',
'currency_id',
'amount',
'date_closed',
        );

// The beans array is keyed by the bean id
$beanIds = array_keys($args['beans']);

// Get a seed for Opportunities
$oppSeed = BeanFactory::newBean('Opportunities');
$q = new SugarQuery();
// Set the from bean
$q->from($oppSeed);
// Join in using the 'contacts' link field
$contactJoin = $q->join('contacts');
// Make sure to select the contact ID, so we can sort it out later
$q->select(array(array($contactJoin->joinName().'.id','contacts__id')));
// Fetch them by the bean id's
$q->where()->in($contactJoin->joinName().'.id', $beanIds);
$q->where()->notIn('sales_stage', array('Closed Lost', 'Closed Won'));
// Sort by the "usdollar" amount, the normal amount field could be in
// different currencies and so the sorting will be all wrong.
$q->orderBy('amount_usdollar DESC');
// Just get the top opportunity
$q->groupBy($contactJoin->joinName().'.id');

// Use fetchFromQuery and pass in the fields we care about
$opps = $oppSeed->fetchFromQuery($q, $oppFields, array('returnRawRows' => true));

// Raw rows are returned keyed by _rows, but we don't want to try and process
// that as a bean, so let's set them aside for later
$rows = $opps['_rows'];
unset($opps['_rows']);

// Loop through the opportunities and with the help of the raw rows link them to their contacts
foreach ($opps as $oppId => $opp) {
$contactId = $rows[$oppId]['contacts__id'];
$args['beans'][$contactId]->top_opp = $opp;
        }
    }
}

 

Don't need to put in a request/response here because with all of that added work everything looks the same just now we don't run a bunch of queries on a list view, only one.

 

7. So you really have to override an endpoint.

 

Some days all of the logic hooks in the world aren't enough. I've tried to give you a lot of very simple and supported methods to override our API but if they just aren't enough for you then you can always override the endpoint. Be cautious and consult an expert before overriding an endpoint, take a stroll and think about it, and just know that we may add functionality to an endpoint that you aren't expecting so you may break things on a normal update.

 

Didn't scare you off and you still want to add an endpoint? Okay then, here you go. Overriding an endpoint is very similar to creating a new endpoint so start by following the directions there. Don't worry about your path conflicting with another path in the base app, the REST lookup code prefers endpoints registered in the custom directory, you will notice that two identical endpoints registered for the same path will appear in /help, but the one with the highest score wins. If you want to live in the danger zone and override an endpoint so that the REST API will match a less-specific path you can add bonus score by putting a value in the option extraScore in your registerApiRest() array for that endpoint. In our example here we will register a new endpoint living on top of the out of the box /Contacts/:record endpoint. Let's create this class in the file custom/modules/Contacts/clients/base/api/FavoriteBurgerApi.php making sure that the filename matches the class name. In our override class we will also extend the ModuleApi because that is what normally handles these requests. In our overridden retrieveRecord() method we call the parent method so it can continue doing the things it normally does and after that is done we just go in and manipulate the returned data before we send it back. Be sure to run a quick repair and rebuild and check the /help to make sure the REST API picked up your newly created endpoint, it helps to make the short help something unique so it's easier to spot.

 

FavoriteBurgerApi.php

<?php

require_once('clients/base/api/ModuleApi.php');

class FavoriteBurgerApi extends ModuleApi
{
static public $favoriteBurgers = array(
"Poutine on the Ritz Burger",
"Mesclun Around Burger",
"The Don't Get Creme Fraiche With Me Burger",
"Onion-tended Consequences Burger",
"Bruschetta Bout It Burger",
"MediterrAin't Misbehavin' Burger",
"I'm Gonna Get You Succotash Burger",
"Every Breath You Tikka Masala Burger",
    );

public function registerApiRest()
    {
return array(
'retrieve' => array(
'reqType' => 'GET',
'path' => array('Contacts','?'),
'pathVars' => array('module','record'),
'method' => 'retrieveRecord',
'shortHelp' => 'Returns a single record, with their favorite burger attached',
'longHelp' => 'include/api/help/module_record_get_help.html',
            ),
        );
    }

public function retrieveRecord($api, $args)
    {
// Have the moduleApi do the hard work
$data = parent::retrieveRecord($api, $args);

// People don't remember what burger they tried so just give them a random favorite
$burgerNum = rand(1,count(self::$favoriteBurgers)) - 1;
$data['favorite_burger'] = self::$favoriteBurgers[$burgerNum];

// Return the modified data
return $data;
    }
}

 

As you can see in the sample request we have now added a "favorite_burger" field to the returned data.

 

fourth_response.json

# GET /rest/v10/Contacts/f23e19e3-a59e-8985-4202-5327156a36a9?fields=id,name
{
"id": "f23e19e3-a59e-8985-4202-5327156a36a9",
"name": "Kuchi Kopi",
"date_modified": "2014-03-19T10:04:51-06:00",
"_acl": {
"fields": {}
    },
"_module": "Contacts",
"favorite_burger": "Bruschetta Bout It Burger"
}

 

8. And that's all

 

While there are many more ways to manipulate the data inside of Sugar, these are the new ways to manipulate the data directly relating to the REST API that all non-backwards compatible code runs through in newer versions of Sugar. I hope this gives you all a good idea on where you can start getting your customizations in for your individual implementations of SugarCRM.

Post originally written by sugarraagaard.

 

So you've seen what the REST API can do and you want more. In this recipe we'll be covering how to create your own REST endpoint.

 

Our sample endpoint is going to go beyond the filter API and give us a list of accounts ordered by the number of open cases.

 

1. Deciding on an endpoint location

 

This is just about the most important step in adding a new endpoint. The URL of an endpoint should be created using RESTful ideas and placed in the virtual directory tree in a way that will make it easy for developers too see the original intent of this API.

 

Since this endpoint is just going to be reading data let's make it a "GET" request.

 

Because we are dealing primarily with account data we'll throw it in "/Accounts".

 

To finish it off we'll name the endpoint "at_risk".

 

So with it all together our endpoint is "GET /Accounts/at_risk", now we could have our URL set to anything else but with a descriptive name and using the correct HTTP verb of GET it will help any other developers coming across calls to this endpoint to better understand what we are asking for.

 

2. Creating the endpoint class

 

The REST service looks in a lot of locations for endpoint classes:

  • clients/:platform/api/*
  • modules/:module/clients/:platform/api/*
  • custom/clients/:platform/api/*
  • custom/modules/:module/clients/:platform/api/*

 

Since we are adding a custom accounts endpoint we'll create a new class "AtRiskApi" and put it in the file "custom/modules/Accounts/clients/base/api/AtRiskApi.php". It is important to name the file so that it is the same as the class name except with .php at the end otherwise the REST service won't find our class.

 

To get this class so it is listening for a specific URL we need to add a function ->registerApiRest(). We are setting the path to array('Accounts', 'at_risk') and set the pathVars to array('', '') to represent "Accounts/at_risk" and not bring any part of the URL in to our array of arguments. If we wanted to match against a wildcard to look at the at risk profile for a single account record for example we could add a path of array('Accounts', '?', 'at_risk') and a pathVars of array('', 'id', '') which would let us listen for "Accounts/*/at_risk" and would take the second part of the path and populate the id element of our arguments with it.

 

Next we will actually add a function, setting the method in our register array to getAtRisk lets the REST API know to call that method in this class. We'll keep this method simple for now and just have it return 'burgers' just so we can tell it is working right away. These methods just need to return data and the REST API will take care of all the json encoding for you.

 

Finally we add a little line in the shortHelp giving a quick description of what this endpoint is for. We're leaving the longHelp out of this little demo but if you are building endpoints for real be sure to add some detailed documents there.

 

So, after everything is all said and done, here's what our little class looks like:

 

AtRiskApi.php

<?php

class AtRiskApi extends SugarApi
{
// This function is only called whenever the rest service cache file is deleted.
// This shoud return an array of arrays that define how different paths map to different functions
public function registerApiRest() {
return array(
'getAtRisk' => array(
// What type of HTTP request to match against, we support GET/PUT/POST/DELETE
'reqType' => 'GET',
// This is the path you are hoping to match, it also accepts wildcards of ? and <module>
'path' => array('Accounts', 'at_risk'),
// These take elements from the path and use them to populate $args
'pathVars' => array('', ''),
// This is the method name in this class that the url maps to
'method' => 'getAtRisk',
// The shortHelp is vital, without it you will not see your endpoint in the /help
'shortHelp' => 'Lists at risk accounts in the system',
// The longHelp points to an HTML file and will be there on /help for people to expand and show
'longHelp' => '',
            ),
        );
    }

function getAtRisk($api, $args)
    {
// Start off with something simple so we can verify the endpoint is registered.
return 'burgers';
    }
}

 

3. Taking it for a test drive

 

Let's do a GET request for /rest/v10/Accounts/at_riskcurl -X GET -H OAuth-Token:some-token http://localhost/burgers/rest/v10/Accounts/at_risk

 

And here is what we get back:

 

response.json

{
"error": "not_found",
"error_message": "Could not find record: at_risk in module: Accounts"
}

 

Hey, what gives? First things first let's check to see if it registered correctly by looking for the endpoint definition in /help, navigate over to /rest/v10/help in your browser and look for it. Not there? didn't think so.

 

We added the class and it didn't load. Since the REST API looks for so many files in so many directories we have to add some heavy duty caching in order to speed up the url lookup on every single request. In order for changes in endpoint definitions to show up we need to login as an admin and run quick repair and rebuild.

 

After quick repair, let's check /rest/v10/help again and you should see a line like this:



 

So let's try that request again.curl -X GET -H OAuth-Token:some-token http://localhost/burgers/rest/v10/Accounts/at_risk

 

Now we get back the correct response:

 

response.json

"burgers"

 

4. Fetching the data

 

While having a new URL that says "burgers" is pretty fancy I think we can accomplish more. While there are many ways to fetch and return this data I want to show you the preferred way to do it in Sugar 7.

 

First things first we need to start off by using SugarQuery. Let's get a seed bean going by fetching a new bean from the bean factory. We pass that through to the SugarQuery ->from() method to let SugarQuery know we will be querying off of the Accounts module. We'll limit our result set to just ID's by adding ->select('id') and then limit our rows to just the first five by adding ->limit(3). From here we can just have it return the results of the ->execute() call and see what that gets us.

 

Now our getAtRisk function looks like this:

 

getAtRisk.php

<?php
function getAtRisk($api, $args)
{
$seed = BeanFactory::newBean('Accounts');

$q = new SugarQuery();
// Set from to the bean first so SugarQuery can figure out joins and fields on the first try
$q->from($seed);
// Adding the ID field so we can validate the results from the select
$q->select('id');
$q->limit(5);

// Return the raw SQL results through the API
return $q->execute();
}

 

and when we make that same GET request to Accounts/at_risk we get back:

 

response.json

[
    {
"id": "160a729d-f808-a046-e901-5315ffa4aa2a"
    },
    {
"id": "2b0f91be-4392-8de5-896e-5315ffc814da"
    },
    {
"id": "2e890e9a-ca34-6b00-860a-5315ffdb0f71"
    }
]

 

Okay so now we have some simple SQL being run and are returning the result set. How about we add some more complex logic here so we actually fetch the results we want. To start things off let's join in the cases by adding this "$caseAlias = $q->join('cases')->joinName();". It's nice that we just need to use the link field to add a join and everything else is handled by SugarQuery. SugarQuery also understands that we have to go beyond it's abilities every once in a while, so we need to add a ->fieldRaw() call to fetch the count and then an ->orderByRaw() to properly sort them. We have to use the Raw versions of the functions because neither of those columns are defined in the field_defs for the modules. The ->groupBy() call just needs to group by the account ID so that is simple. Finally the ->where()->notIn() is there so we only fetch related cases that aren't resolved, no need to quote elements here because SugarQuery will handle that for us.

 

Added all together it looks like this:

 

GetAtRisk.php

<?php
function getAtRisk($api, $args)
{
$seed = BeanFactory::newBean('Accounts');

$q = new SugarQuery();
// Set from to the bean first so SugarQuery can figure out joins and fields on the first try
$q->from($seed);
// We need to save the alias of any join because they are auto-generated (and different if you are on some databases)
$caseAlias = $q->join('cases')->joinName();
// Adding the ID field so we can validate the results from the select
$q->select('id');
// fieldRaw will let us run raw SQL in the select, be sure to quote anything you are getting from users.
// The second argument is the field alias so we can tell what will be coming out of the other end of the query
$q->select->fieldRaw("COUNT(".$caseAlias.".id)","case_count");
// We need to use orderByRaw here instead of just orderBy because case_count isn't part of any vardefs
$q->orderByRaw('case_count');
$q->groupBy('id');
// the ->where() gives us a where object, there are a lot of operators that work on that (including ->and() and ->or())
$q->where()->notIn($caseAlias.'.status', array('Closed', 'Rejected'));
$q->limit(3);

return $q->execute();
}

 

Once again let's hit Accounts/at_risk and see what we get:

 

response.json

[
    {
"id": "f4225353-54a0-502e-409a-5315ffa656ab",
"case_count": "5"
    },
    {
"id": "c4024e67-dee3-553d-0040-5315ff8f2fd1",
"case_count": "5"
    },
    {
"id": "ab6b6784-38cc-077a-fda5-5315ff228fcf",
"case_count": "4"
    }
]

 

Looking good! Now we are getting the data we need how about we make it look nice for the javascript client that needs it?

 

5. Formatting the data

 

To format the data first we have to figure out what to format. Most endpoints accept the argument fields to specify which fields they want returned from the api and we'll keep up that tradition here with some simple parsing of the $args array.

 

Next up we want to convert the results of the query into beans so they can be properly formatted. Previously you would have to perform the PHP equivalent of banging two rocks together to make fire by manually fetching the rows and creating beans and populating them via ->populateFromRow(). Fortunately we are introducing a helper function in SugarBean named ->fetchFromQuery() to help automate and centralize this process, so we'll just call that here. We need to pass ->fetchFromQuery() the option of returnRawRows because we need to populate the case_count field manually because it doesn't exist in any of the field definitions.

 

With the beans returned from ->fetchFromQuery() we strip out the raw rows from the result set and then pass the remaining beans through to ->formatBeans() so that our returned results look the same as every single other API call. After we get the results back as an array from ->formatBeans() we loop through the results and jam the case_count in there.

 

So with all that, here's what our final method looks like:

 

getAtRisk.php

<?php
function getAtRisk($api, $args)
{
$seed = BeanFactory::newBean('Accounts');

$q = new SugarQuery();
// Set from to the bean first so SugarQuery can figure out joins and fields on the first try
$q->from($seed);
// We need to save the alias of any join because they are auto-generated (and different if you are on some databases)
$caseAlias = $q->join('cases')->joinName();
// Adding the ID field so we can validate the results from the select
$q->select('id');
// fieldRaw will let us run raw SQL in the select, be sure to quote anything are getting from users.
// The second argument is the field alias so we can tell what it will be coming out the other end of the query
$q->select->fieldRaw("COUNT(".$caseAlias.".id)","case_count");
// We need to use orderByRaw here instead of just orderBy because case_count isn't part of any vardefs
$q->orderByRaw('case_count');
$q->groupBy('id');
// the ->where() gives us a where object, there are a lot of operators that work on that (including ->and() and ->or())
$q->where()->notIn($caseAlias.'.status', array('Closed', 'Rejected'));
$q->limit(3);

// Let's parse the field array like formatBeans down below
if (empty($args['fields'])) {
$args['fields'] = array();
    } else if (!is_array($args['fields'])) {
$args['fields'] = explode(',', $args['fields']);
    }

// Run the new ->fetchFromQuery() call to get beans out of a query, get the raw rows for non-vardef fields
$accountBeans = $seed->fetchFromQuery($q, $args['fields'], array('returnRawRows' => true));

// The normal beans are in there by id, the raw rows are returned in their own element
// Let's strip that out so we don't try to apply sugarbean code to it.
$rows = $accountBeans['_rows'];
unset($accountBeans['_rows']);

// Part of SugarApi, this will format our list of beans like all of the rest of the API's
// Consistency is good
$accounts = $this->formatBeans($api, $args, $accountBeans);

// Since case_count isn't part of the vardefs, we have to populate it manually
foreach ($accounts as &$account) {
$account['case_count'] = (int)$rows[$account['id']]['case_count'];
    }

return $accounts;
}

 

And when we finally call the Accounts/at_risk, here is what we get:curl -X GET -H OAuth-Token:some-token http://localhost/burgers/rest/v10/Accounts/at_risk?fields=id,name,date_modified

 

response.json

[
    {
"id": "f4225353-54a0-502e-409a-5315ffa656ab",
"name": "A Fridge Too Far",
"date_modified": "2014-03-04T16:28:59+00:00",
"_acl": {
"fields": {}
        },
"_module": "Accounts",
"case_count": "5"
    },
    {
"id": "c4024e67-dee3-553d-0040-5315ff8f2fd1",
"name": "That's Improv-able",
"date_modified": "2014-03-04T16:28:59+00:00",
"_acl": {
"fields": {}
        },
"_module": "Accounts",
"case_count": "5"
    },
    {
"id": "ab6b6784-38cc-077a-fda5-5315ff228fcf",
"name": "Annie Get Your Gum",
"date_modified": "2014-03-04T16:28:59+00:00",
"_acl": {
"fields": {}
        },
"_module": "Accounts",
"case_count": "5"
    }
]

 

6. All done!

 

That's all, I hope this clears how to add your own endpoint to Sugar 7. Along with some helpful tips on how to use a combination of SugarQuery, ->fetchFromQuery() and ->formatBeans() to create easy and standardized code for returning data from the API. Add a comment if you have questions.

Post originally written by sugarmajed.

 

We are pleased to announce the return of UnCon!

 

Sugar is more than just a CRM – it is also a platform where developers can build all sorts of custom widgets and integrations to popular 3rd party applications and APIs. Join us at UnCon to learn how sweet it can be to build integrations and widgets in Sugar!

 

UnCon is a great chance for novice and skilled Sugar developers to hack side-by-side with our core engineers.

 

We'll be giving you a crash course in building integrations, and then we will have our developers join forces with you to make awesome happen in a two day hackathon!

 

This year the core engineering team will be hosting the event on Tuesday, April 29th and Wednesday, April, 30th.

 

You can attend UnCon with a General Conference pass to SugarCon for $799 — or skip the conference and attend only UnCon for $99.Click Here to Visit the official UnCon Website

Post originally written by sugarmajed.

 

This is the final section of the School of Rest. We'll be covering filtering on relationships as well as Global Search, Favorites, and Deleting records.

 

We've already created our Account "Burger Palace" in Part 1 and showed how to filter lists with various conditions.

 

And in Part 2 we created our Contact "Bob Burger" and related him to "Burger Palace". Now we'll show how these same concepts apply to relationships.

 

11. Filtering on Relationships

 

All the same stuff that we learned for filtering on lists can be applied to relationships as well. Let's retrieve all the Contacts that are related to "Burger Palace" whose first and last name starts with "B" and who we have marked as a favorite.

 

To filter on first and last name we know the filter looks like

filter[0][first_name][$starts]=B and filter[0][last_name][$starts]=B

 

But what about favorites? Favorites are a way for users to specify which records are important to them and they have a special filter condition that looks like

filter[0][$favorite]=_this

 

So we will do a GET request to /rest/v10/Accounts/demo_burger_palace/link/contacts?filter[0][last_name][$starts]=b&filter[0][first_name][$starts]=b&filter[0][$favorite]=_this&fields=name,first_name,last_name,email,description

 

CURL Filter Relationships.sh

curl -X GET -H OAuth-Token:8f027f7a-266a-dcd0-8dc4-530cfa3ce6d4 -H Cache-Control:no-cache -H Postman-Token:a86bc38a-8c3f-d364-fbfe-282e41b10645 http://server/pro720/rest/v10/Accounts/demo_burger_palace/link/contacts?filter[0][last_name][$starts]=b&filter[0][first_name][$starts]=b&filter[0][$favorite]=_this

GET Filter Relationships.http

GET /pro720/rest/v10/Accounts/demo_burger_palace/link/contacts?filter%5B0%5D%5Blast_name%5D%5B%24starts%5D=b&filter%5B0%5D%5Bfirst_name%5D%5B%24starts%5D=b&filter%5B0%5D%5B%24favorite%5D=_this HTTP/1.1
Host: server
OAuth-Token: 8f027f7a-266a-dcd0-8dc4-530cfa3ce6d4
Cache-Control: no-cache
Postman-Token: fc8df8eb-c475-28bc-ca03-6b7ee511a069


Response.json

{
"next_offset": -1,
"records": []
}

 

 

Well that didn't work to well. First, we need to mark "Bob Burger" as a favorite.

 

12. Mark a Contact as a Favorite - Bob Burger

 

All we need to do is a PUT to  /rest/v10/Contacts/demo_bob_burger/favorite

 

This works on all modules that support favorites, and allows us to quickly filter our favorite records.

 

CURL PUT Favorite Contact.sh

curl -X PUT -H OAuth-Token:8f027f7a-266a-dcd0-8dc4-530cfa3ce6d4 -H Cache-Control:no-cache -H Postman-Token:69fc3540-0acd-0186-c0c9-cb95ac352bd8 -H Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryp7MA4YWxkTrZu0gW http://server/pro720/rest/v10/Contacts/demo_bob_burger/favorite

PUT Favorite Contact.http

PUT /pro720/rest/v10/Contacts/demo_bob_burger/favorite HTTP/1.1
Host: server
OAuth-Token: 8f027f7a-266a-dcd0-8dc4-530cfa3ce6d4
Cache-Control: no-cache
Postman-Token: cb2d99d7-241d-e4e2-71b3-f9a432d72a12
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryp7MA4YWxkTrZu0gW


Response.json

{
"id": "demo_bob_burger",
"name": "Bob Burger",
"date_entered": "2014-02-25T12:16:41-08:00",
"date_modified": "2014-02-25T12:21:22-08:00",
"modified_user_id": "1",
"modified_by_name": "Administrator",
"created_by": "1",
"created_by_name": "Administrator",
"doc_owner": "",
"user_favorites": [
"1"
    ],
"description": "Bob Burger was updated",
"deleted": false,
"assigned_user_id": "",
"assigned_user_name": "",
"team_count": "",
"team_name": [
        {
"id": 1,
"name": "Global",
"name_2": "",
"primary": true
        }
    ],
"email": [
        {
"email_address": "bob.burger@example.com",
"invalid_email": false,
"opt_out": false,
"primary_address": false,
"reply_to_address": false
        }
    ],
"email1": "bob.burger@example.com",
"email2": "",
"invalid_email": false,
"email_opt_out": false,
"salutation": "",
"first_name": "Bob",
"last_name": "Burger",
"full_name": "Bob Burger",
"title": "",
"facebook": "",
"twitter": "",
"googleplus": "",
"department": "",
"do_not_call": false,
"phone_home": "",
"phone_mobile": "",
"phone_work": "",
"phone_other": "",
"phone_fax": "",
"primary_address_street": "",
"primary_address_street_2": "",
"primary_address_street_3": "",
"primary_address_city": "",
"primary_address_state": "",
"primary_address_postalcode": "",
"primary_address_country": "",
"alt_address_street": "",
"alt_address_street_2": "",
"alt_address_street_3": "",
"alt_address_city": "",
"alt_address_state": "",
"alt_address_postalcode": "",
"alt_address_country": "",
"assistant": "",
"assistant_phone": "",
"picture": "",
"email_and_name1": "",
"lead_source": "",
"account_name": "Burger Palace",
"account_id": "demo_burger_palace",
"dnb_principal_id": "",
"opportunity_role_fields": "",
"opportunity_role_id": "",
"opportunity_role": "",
"reports_to_id": "",
"report_to_name": "",
"birthdate": "",
"campaign_id": "",
"campaign_name": "",
"c_accept_status_fields": "",
"m_accept_status_fields": "",
"accept_status_id": "",
"accept_status_name": "",
"accept_status_calls": "",
"accept_status_meetings": "",
"sync_contact": false,
"mkto_sync": false,
"mkto_id": null,
"mkto_lead_score": null,
"my_favorite": true,
"_acl": {
"fields": {}
    },
"following": true,
"_module": "Contacts"
}

 

You'll notice in the response the record now says "my_favorite" is true.

 

If we wanted to unmark a record as a favorite we would just do a DELETE  to /rest/v10/Contacts/demo_bob_burger/favorite

 

13. Try Step 11 Again!

 

Let's re-run the relationship filter from Step 10! Let's do a GET request to /rest/v10/Accounts/demo_burger_palace/link/contacts?filter[0][last_name][$starts]=b&filter[0][first_name][$starts]=b&filter[0][$favorite]=_this&fields=name,first_name,last_name,email,description

 

CURL filter relationships again.sh

curl -X GET -H OAuth-Token:d1645103-c562-e439-c0d0-530d2f4e3801 -H Cache-Control:no-cache -H Postman-Token:4ab308ee-7902-6a3f-dc9c-f8bb595dec97 http://server/pro720/rest/v10/Accounts/demo_burger_palace/link/contacts?filter[0][last_name][$starts]=b&filter[0][first_name][$starts]=b&filter[0][$favorite]=_this&fields=name,first_name,last_name,email,description

GET Filter Relationships Again.http

GET /pro720/rest/v10/Accounts/demo_burger_palace/link/contacts?filter%5B0%5D%5Blast_name%5D%5B%24starts%5D=b&filter%5B0%5D%5Bfirst_name%5D%5B%24starts%5D=b&filter%5B0%5D%5B%24favorite%5D=_this&fields=name%2Cfirst_name%2Clast_name%2Cemail%2Cdescription HTTP/1.1
Host: server
OAuth-Token: d1645103-c562-e439-c0d0-530d2f4e3801
Cache-Control: no-cache
Postman-Token: 8ee8836c-e7c4-51e8-a187-bbfd92e6a790


Response.json

{
"next_offset": -1,
"records": [
        {
"id": "demo_bob_burger",
"name": "Bob Burger",
"date_modified": "2014-02-25T15:03:01-08:00",
"description": "Bob Burger was updated",
"email": [
                {
"email_address": "bob.burger@example.com",
"invalid_email": false,
"opt_out": false,
"primary_address": false,
"reply_to_address": false
                }
            ],
"first_name": "Bob",
"last_name": "Burger",
"_acl": {
"fields": {}
            },
"_module": "Contacts"
        }
    ]
}

 

 

Excellent! Now that's what we expected!

 

14. Filter by a Related record

 

Now, let's filter our Accounts list for only records that have a contact with the last name "burger". Our filter condition will look like filter[0][contacts.last_name]=burger and remember that "contacts" in this case is the Link or Relationship Name not the module name.

 

So let's do a GET to rest/v10/Accounts?filter[0][contacts.last_name]=burger

curl -X GET \  'http://server/pro720/rest/v10/Accounts?filter%5B0%5D%5Bcontacts.last_name%5D=burger&fields=name%2Cfirst_name%2Clast_name%2Cemail%2Cdescription' \  -H 'cache-control: no-cache' \  -H 'oauth-token: 39ba05eb-2289-4770-ac39-9fe03f99c77a' \  -H 'postman-token: 65094f2f-ae73-851b-11b1-995e66eb1779'
GET /pro720/rest/v10/Accounts?filter[0][contacts.last_name]=burger&amp;fields=name,first_name,last_name,email,description HTTP/1.1
Host: server
OAuth-Token: 39ba05eb-2289-4770-ac39-9fe03f99c77a
Cache-Control: no-cache
Postman-Token: 598e3c09-43cd-2e7c-704e-0ffbd0378a1e
{
     "next_offset": -1,
     "records": [
         {
             "id": "demo_burger_palace",
             "name": "Burger Palace",
             "date_modified": "2017-07-28T10:14:29-04:00",
             "description": "My Example Account",
             "locked_fields": [],
             "email": [
                 {
                     "email_address": "burgers@example.com",
                     "primary_address": true,
                     "reply_to_address": false,
                     "invalid_email": false,
                     "opt_out": false
                 }
             ],
             "_acl": {
                 "fields": {}
             },
             "_module": "Accounts"
         }
    ]
}

Great! We got "Burger Palace" which is what we wanted.

 

15. Global Search/Full Text Search 

 

Now let's just search everywhere for the word "burger" and see what we get! All we have to do is a GET request to /rest/v10/search?q=burger  and that will search against SugarCRM's Full Text Search.

 

CURL Full Text Search.sh

curl -X GET -H OAuth-Token:4ef8594d-5b45-20f4-0a01-530d20d69d1f -H Cache-Control:no-cache -H Postman-Token:e76d2d29-bc10-bea7-c2b6-761e49a8a197 http://server/ent720/rest/v10/search?q=burger&fields=name,email,description

GET Full Text Search.http

GET /ent720/rest/v10/search?q=burger&fields=name%2Cemail%2Cdescription HTTP/1.1
Host: server
OAuth-Token: 4ef8594d-5b45-20f4-0a01-530d20d69d1f
Cache-Control: no-cache
Postman-Token: 7aeedc64-602a-535d-7c0d-b32bf2e1a035

Response.json

{
"next_offset": -1,
"records": [
        {
"id": "demo_bob_burger",
"name": "Bob Burger",
"date_modified": "2014-02-25T23:03:01+00:00",
"description": "Bob Burger was updated",
"email": [
                {
"email_address": "bob.burger@example.com",
"invalid_email": false,
"opt_out": false,
"primary_address": false,
"reply_to_address": false
                }
            ],
"_acl": {
"fields": {}
            },
"_module": "Contacts",
"_search": {
"score": 1,
"highlighted": {
"last_name": {
"text": "<strong>Burger</strong>",
"module": "Contacts",
"label": "LBL_LAST_NAME"
                    }
                }
            },
"following": true
        },
        {
"id": "demo_burger_palace",
"name": "Burger Palace",
"date_modified": "2014-02-25T23:01:42+00:00",
"description": "My Example Account",
"email": [
                {
"email_address": "burgers@example.com",
"invalid_email": false,
"opt_out": false,
"primary_address": false,
"reply_to_address": false
                }
            ],
"_acl": {
"fields": {}
            },
"_module": "Accounts",
"_search": {
"score": 1,
"highlighted": {
"name": {
"text": "<strong>Burger</strong> Palace",
"module": "Accounts",
"label": "LBL_NAME"
                    },
"email1": {
"text": "<strong>burgers</strong>@example.com",
"module": "Accounts",
"label": "LBL_EMAIL_ADDRESS"
                    }
                }
            },
"following": true
        }
    ]
}

 

We got two records back - "Burger Palace" and "Bob Burger". So using Global Search/Full Text Search we can search against all of our objects at once!

 

16.  Let's Clean Up

 

All we need to do is a DELETE request to /rest/v10/Accounts/demo_burger_palace

 

and another DELETE request to /rest/v10/Contacts/demo_bob_burger

 

And remember there is more documentation at /rest/v10/help!

Post originally written by sugarmajed.

 

This is part 2 of 3 of The School of REST. In Part 1 we covered creating our Account "Burger Palace" and using the filter API to get just the records we wanted. Today we'll cover retrieving records, creating our Contact "Bob Burger", updating records, and associating records together. 

 

6. Retrieve a specific Account - Burger Palace 

 

We created "Burger Palace" with the id "demo_burger_palace". To retrieve it using the RESTful interface, we just need to do a GET to rest/v10/Accounts/demo_burger_palace

 

CURL GET Account.sh

curl -X GET -H OAuth-Token:e2505ba8-fc56-de05-ce9f-530d1bf56227 -H Cache-Control:no-cache -H Postman-Token:7e4c0a5d-db74-55d7-64a7-8c363686f9de http://server/pro720/rest/v10/Accounts/demo_burger_palace?fields=name,email,account_type,description


GET Account.http

GET /pro720/rest/v10/Accounts/demo_burger_palace?fields=name%2Cemail%2Caccount_type%2Cdescription HTTP/1.1
Host: server
OAuth-Token: e2505ba8-fc56-de05-ce9f-530d1bf56227
Cache-Control: no-cache
Postman-Token: 7e2f14d9-3868-3d38-c9f2-b3c36a3105b0


Response.json

{
"id": "demo_burger_palace",
"date_modified": "2014-02-25T14:32:25-08:00",
"description": "My Example Account",
"email": [
        {
"email_address": "burgers@example.com",
"invalid_email": false,
"opt_out": false,
"primary_address": false,
"reply_to_address": false
        }
    ],
"account_type": "Customer",
"_acl": {
"fields": {}
    },
"_module": "Accounts"
}


7. Create a Contact - Bob Burger 

 

We have our customer account "Burger Palace", but relationships are all about the people. Let's create a Contact named "Bob Burger". Using what we learned against Accounts, we can apply it to Contacts. Let's do a POST to /rest/v10/Contacts

 

We'll create "Bob Burger" with the id "demo_bob_burger". Remember the we DO NOT need to pass in the id. We are only doing this for demo purposes right now. SugarCRM will automatically generate an id and return it as part of the response.

 

CURL POST Contacts.sh

curl -X POST -H OAuth-Token:8f027f7a-266a-dcd0-8dc4-530cfa3ce6d4 -H Cache-Control:no-cache -H Postman-Token:0444b90d-ba1d-c395-a259-694115e5ec1e -d '{ "id":"demo_bob_burger", "first_name":"Bob", "last_name": "Burger", "description":"Bob Burger is a new contact" }' http://server/pro720/rest/v10/Contacts


POST Contacts.http

POST /pro720/rest/v10/Contacts HTTP/1.1
Host: server
OAuth-Token: 8f027f7a-266a-dcd0-8dc4-530cfa3ce6d4
Cache-Control: no-cache
Postman-Token: 27bb113b-ef6f-86dc-601e-e2f8e2c4878c

{ "id":"demo_bob_burger", "first_name":"Bob", "last_name": "Burger", "description":"Bob Burger is a new contact" }

Response.json

{
"id": "demo_bob_burger",
"name": "Bob Burger",
"date_entered": "2014-02-25T12:16:41-08:00",
"date_modified": "2014-02-25T12:16:41-08:00",
"modified_user_id": "1",
"modified_by_name": "Administrator",
"created_by": "1",
"created_by_name": "Administrator",
"doc_owner": "",
"user_favorites": "",
"description": "Bob Burger is a new contact",
"deleted": false,
"assigned_user_id": "",
"assigned_user_name": "",
"team_count": "",
"team_name": [
        {
"id": 1,
"name": "Global",
"name_2": "",
"primary": true
        }
    ],
"email": [],
"email1": "",
"email2": "",
"invalid_email": "",
"email_opt_out": "",
"salutation": "",
"first_name": "Bob",
"last_name": "Burger",
"full_name": "Bob Burger",
"title": "",
"facebook": "",
"twitter": "",
"googleplus": "",
"department": "",
"do_not_call": false,
"phone_home": "",
"phone_mobile": "",
"phone_work": "",
"phone_other": "",
"phone_fax": "",
"primary_address_street": "",
"primary_address_street_2": "",
"primary_address_street_3": "",
"primary_address_city": "",
"primary_address_state": "",
"primary_address_postalcode": "",
"primary_address_country": "",
"alt_address_street": "",
"alt_address_street_2": "",
"alt_address_street_3": "",
"alt_address_city": "",
"alt_address_state": "",
"alt_address_postalcode": "",
"alt_address_country": "",
"assistant": "",
"assistant_phone": "",
"picture": "",
"email_and_name1": "",
"lead_source": "",
"account_name": "",
"account_id": "",
"dnb_principal_id": "",
"opportunity_role_fields": "",
"opportunity_role_id": "",
"opportunity_role": "",
"reports_to_id": "",
"report_to_name": "",
"birthdate": "",
"campaign_id": "",
"campaign_name": "",
"c_accept_status_fields": "",
"m_accept_status_fields": "",
"accept_status_id": "",
"accept_status_name": "",
"accept_status_calls": "",
"accept_status_meetings": "",
"sync_contact": false,
"mkto_sync": false,
"mkto_id": null,
"mkto_lead_score": null,
"my_favorite": false,
"_acl": {
"fields": {}
    },
"following": true,
"_module": "Contacts"
}

 

8. Update a Record - Bob Burger 

 

Great, we've created "Bob Burger" by doing a POST request. Now let's update him by doing a PUT request to /rest/v10/Contacts/demo_bob_burger

 

CURL PUT Contact.sh

curl -X PUT -H OAuth-Token:8f027f7a-266a-dcd0-8dc4-530cfa3ce6d4 -H Cache-Control:no-cache -H Postman-Token:63ba84bf-612d-7210-c328-29f50f3f4b92 -d '{ "email":[{"email_address":"bob.burger@example.com"}], "description":"Bob Burger was updated" }' http://server/pro720/rest/v10/Contacts/demo_bob_burger

PUT Contact.http

PUT /pro720/rest/v10/Contacts/demo_bob_burger HTTP/1.1
Host: server
OAuth-Token: 8f027f7a-266a-dcd0-8dc4-530cfa3ce6d4
Cache-Control: no-cache
Postman-Token: 24ae44a6-1720-b644-f236-f347226f18e4

{ "email":[{"email_address":"bob.burger@example.com"}], "description":"Bob Burger was updated" }

Response.json

{
"id": "demo_bob_burger",
"name": "Bob Burger",
"date_entered": "2014-02-25T12:16:41-08:00",
"date_modified": "2014-02-25T12:21:22-08:00",
"modified_user_id": "1",
"modified_by_name": "Administrator",
"created_by": "1",
"created_by_name": "Administrator",
"doc_owner": "",
"user_favorites": "",
"description": "Bob Burger was updated",
"deleted": false,
"assigned_user_id": "",
"assigned_user_name": "",
"team_count": "",
"team_name": [
        {
"id": 1,
"name": "Global",
"name_2": "",
"primary": true
        }
    ],
"email": [
        {
"email_address": "bob.burger@example.com",
"invalid_email": false,
"opt_out": false,
"primary_address": false,
"reply_to_address": false
        }
    ],
"email1": "bob.burger@example.com",
"email2": "",
"invalid_email": false,
"email_opt_out": false,
"salutation": "",
"first_name": "Bob",
"last_name": "Burger",
"full_name": "Bob Burger",
"title": "",
"facebook": "",
"twitter": "",
"googleplus": "",
"department": "",
"do_not_call": false,
"phone_home": "",
"phone_mobile": "",
"phone_work": "",
"phone_other": "",
"phone_fax": "",
"primary_address_street": "",
"primary_address_street_2": "",
"primary_address_street_3": "",
"primary_address_city": "",
"primary_address_state": "",
"primary_address_postalcode": "",
"primary_address_country": "",
"alt_address_street": "",
"alt_address_street_2": "",
"alt_address_street_3": "",
"alt_address_city": "",
"alt_address_state": "",
"alt_address_postalcode": "",
"alt_address_country": "",
"assistant": "",
"assistant_phone": "",
"picture": "",
"email_and_name1": "",
"lead_source": "",
"account_name": "",
"account_id": "",
"dnb_principal_id": "",
"opportunity_role_fields": "",
"opportunity_role_id": "",
"opportunity_role": "",
"reports_to_id": "",
"report_to_name": "",
"birthdate": "",
"campaign_id": "",
"campaign_name": "",
"c_accept_status_fields": "",
"m_accept_status_fields": "",
"accept_status_id": "",
"accept_status_name": "",
"accept_status_calls": "",
"accept_status_meetings": "",
"sync_contact": false,
"mkto_sync": false,
"mkto_id": null,
"mkto_lead_score": null,
"my_favorite": false,
"_acl": {
"fields": {}
    },
"following": true,
"_module": "Contacts"
}

 

9. Let's associate our Contact and Account 

 

We have "Burger Palace" and "Bob Burger" now let's bring them together by doing a POST to /rest/v10/Accounts/demo_burger_palace/link

 

We'll need to pass two arguments.

  1.  a list of ids we wish to relate to "Burger Palace" which in this case is demo_bob_burger.
  2.  a link_name which is the name of the relationship we wish to use. In this case it is "contacts", but it's not always the name of the related module.

 

CURL Associate Account Contact.sh

curl -X POST -H OAuth-Token:8f027f7a-266a-dcd0-8dc4-530cfa3ce6d4 -H Cache-Control:no-cache -H Postman-Token:f6e69685-303c-14b4-d1a7-c70cfa00f17f -d '{ "link_name": "contacts", "ids": ["demo_bob_burger"] }' http://server/pro720/rest/v10/Accounts/demo_burger_palace/link

POST Link.http

POST /pro720/rest/v10/Accounts/demo_burger_palace/link HTTP/1.1
Host: server
OAuth-Token: 8f027f7a-266a-dcd0-8dc4-530cfa3ce6d4
Cache-Control: no-cache
Postman-Token: 20fc86b9-644b-f2fb-ca11-8b304587eff6

{ "link_name": "contacts", "ids": ["demo_bob_burger"] }


Response.json

{
"related_records": [
        {
"id": "demo_bob_burger",
"name": "Bob Burger",
"date_entered": "2014-02-25T12:16:41-08:00",
"date_modified": "2014-02-25T12:21:22-08:00",
"modified_user_id": "1",
"modified_by_name": "Administrator",
"created_by": "1",
"created_by_name": "Administrator",
"doc_owner": "",
"user_favorites": "",
"description": "Bob Burger was updated",
"deleted": false,
"assigned_user_id": "",
"assigned_user_name": "",
"team_count": "",
"team_name": [
                {
"id": 1,
"name": "Global",
"name_2": "",
"primary": true
                }
            ],
"email": [
                {
"email_address": "bob.burger@example.com",
"invalid_email": false,
"opt_out": false,
"primary_address": false,
"reply_to_address": false
                }
            ],
"email1": "bob.burger@example.com",
"email2": "",
"invalid_email": false,
"email_opt_out": false,
"salutation": "",
"first_name": "Bob",
"last_name": "Burger",
"full_name": "Bob Burger",
"title": "",
"facebook": "",
"twitter": "",
"googleplus": "",
"department": "",
"do_not_call": false,
"phone_home": "",
"phone_mobile": "",
"phone_work": "",
"phone_other": "",
"phone_fax": "",
"primary_address_street": "",
"primary_address_street_2": "",
"primary_address_street_3": "",
"primary_address_city": "",
"primary_address_state": "",
"primary_address_postalcode": "",
"primary_address_country": "",
"alt_address_street": "",
"alt_address_street_2": "",
"alt_address_street_3": "",
"alt_address_city": "",
"alt_address_state": "",
"alt_address_postalcode": "",
"alt_address_country": "",
"assistant": "",
"assistant_phone": "",
"picture": "",
"email_and_name1": "",
"lead_source": "",
"account_name": "",
"account_id": "",
"dnb_principal_id": "",
"opportunity_role_fields": "",
"opportunity_role_id": "",
"opportunity_role": "",
"reports_to_id": "",
"report_to_name": "",
"birthdate": "",
"campaign_id": "",
"campaign_name": "",
"c_accept_status_fields": "",
"m_accept_status_fields": "",
"accept_status_id": "",
"accept_status_name": "",
"accept_status_calls": "",
"accept_status_meetings": "",
"sync_contact": false,
"mkto_sync": false,
"mkto_id": null,
"mkto_lead_score": null,
"my_favorite": false,
"_acl": {
"fields": {}
            },
"following": true,
"_module": "Contacts"
        }
    ],
"record": {
"id": "demo_burger_palace",
"name": "Burger Palace",
"date_entered": "2014-02-25T10:51:51-08:00",
"date_modified": "2014-02-25T10:51:51-08:00",
"modified_user_id": "1",
"modified_by_name": "Administrator",
"created_by": "1",
"created_by_name": "Administrator",
"doc_owner": "",
"user_favorites": "",
"description": "My Example Account",
"deleted": false,
"assigned_user_id": "",
"assigned_user_name": "",
"team_count": "",
"team_name": [
            {
"id": 1,
"name": "Global",
"name_2": "",
"primary": true
            }
        ],
"email": [
            {
"email_address": "burgers@example.com",
"invalid_email": false,
"opt_out": false,
"primary_address": false,
"reply_to_address": false
            }
        ],
"email1": "burgers@example.com",
"email2": "",
"invalid_email": false,
"email_opt_out": false,
"facebook": "",
"twitter": "",
"googleplus": "",
"account_type": "Customer",
"industry": "",
"annual_revenue": "",
"phone_fax": "",
"billing_address_street": "",
"billing_address_street_2": "",
"billing_address_street_3": "",
"billing_address_street_4": "",
"billing_address_city": "",
"billing_address_state": "",
"billing_address_postalcode": "",
"billing_address_country": "",
"rating": "",
"phone_office": "",
"phone_alternate": "",
"website": "",
"ownership": "",
"employees": "",
"ticker_symbol": "",
"shipping_address_street": "",
"shipping_address_street_2": "",
"shipping_address_street_3": "",
"shipping_address_street_4": "",
"shipping_address_city": "",
"shipping_address_state": "",
"shipping_address_postalcode": "",
"shipping_address_country": "",
"parent_id": "",
"sic_code": "",
"duns_num": "",
"parent_name": "",
"campaign_id": "",
"campaign_name": "",
"my_favorite": false,
"_acl": {
"fields": {}
        },
"following": true,
"_module": "Accounts"
    }
}


10. Get Contacts Associated to an Account - Burger Palace

 

We just related "Bob Burger" to "Burger Palace" now let's view that relationship by retrieving the contacts related to "Burger Palace". All we need to do is a GET request to /rest/v10/Accounts/demo_burger_palace/link/contacts where "contacts" is the same link_name we passed up in step 9. 

 

CURL Related Contacts.sh

curl -X GET -H OAuth-Token:e2505ba8-fc56-de05-ce9f-530d1bf56227 -H Cache-Control:no-cache -H Postman-Token:54bc4cd3-b893-ca1e-1ad4-2eaf40ca9f8e http://server/pro720/rest/v10/Accounts/demo_burger_palace/link/contacts?fields=name,first_name,last_name,email,description

GET  Related Contacts.http

GET /pro720/rest/v10/Accounts/demo_burger_palace/link/contacts?fields=name%2Cfirst_name%2Clast_name%2Cemail%2Cdescription HTTP/1.1
Host: server
OAuth-Token: e2505ba8-fc56-de05-ce9f-530d1bf56227
Cache-Control: no-cache
Postman-Token: 7edacde4-62c2-72ea-8c33-645df954e0cf


Response.json

{
"next_offset": -1,
"records": [
        {
"id": "demo_bob_burger",
"name": "Bob Burger",
"date_modified": "2014-02-25T14:44:24-08:00",
"description": "Bob Burger was updated",
"email": [
                {
"email_address": "bob.burger@example.com",
"invalid_email": false,
"opt_out": false,
"primary_address": false,
"reply_to_address": false
                }
            ],
"first_name": "Bob",
"last_name": "Burger",
"_acl": {
"fields": {}
            },
"_module": "Contacts"
        }
    ]
}

 

Now you'll see in the response that "Bob Burger" is associated to "Burger Palace"

 

Continue on to Part 3  - Filtering on Relationships, Favorites, and Deleting Records 

Post originally written by sugarmajed.

 

Welcome to the School of REST - our first installment in the Sugar 7 Cookbook Series! We've always believed that DATA belongs to you and it should always be accessible to you! With Sugar 7 we've added a completely revamped REST API. Today we are going to cover all the basics of pushing data into and getting data out of your SugarCRM instance. For each of the API calls we'll show how to do it in curl, a generic HTTP Request, as well as showing you the response. Also, before we get started you can always hit /rest/v10/help for detailed documentation on all the APIs.

 

In this Guide we will be creating an Account "Burger Palace" and relating it to a Contact "Bob Burger". We will then show you the ins and outs of Creating and Updating Records, Filtering Lists, Creating Relationships, Filtering on Relationships, Marking Records as Favorites, and Deleting Records.

 

This guide is broken into 3 sections.

 

Part 1 - Connecting, Creating Records, and Filtering Lists

Part 2 - Retrieving, Updating, and Relating Records 

Part 3  - Filtering on Relationships, Favorites, and Deleting Records 

 

You may want to download PostMan for Chrome to follow along interactively from  http://www.getpostman.com/

 

You can download Our Cookbook1 PostMan collection from  https://gist.github.com/mitani/f39e8d94df9fbda4d97d

 

Step 1. Connecting/Authenticating

 

Sugar 7 uses two-legged OAuth2 for authentication. You simply need to do a POST to /rest/v10/oauth2/token with the following params:

grant_typeStringType of request. Available grant types are "password" and "refresh_token".
client_idStringThe client_id of "sugar" will automatically create an OAuth Key in the system and can be used for "password" authentication. The client_id of "support_portal" will create an OAuth Key if the portal system is enabled and will allow for portal authentication. Other client_id's can be created by the administrator in the OAuthKeys section in the Administration section and can be used in the future for additional grant types, if the client secret is filled in, it will be checked to validate use of the client id.
client_secretStringThe clients secret key.
usernameStringThe username of the user authenticating to the system.
passwordStringThe plaintext password the user authenticating to the system.
platformStringDefaults to "base" allows you to have custom meta-data per platform

 

 

So, first we are going to login using a grant_type of "password".

 

CURL.sh

curl -X POST -H Cache-Control:no-cache -H Postman-Token:a6ae8f76-442f-06cf-1072-db18b0ad29a3 -d '{ "grant_type":"password", "client_id":"sugar", "client_secret":"", "username":"username", "password":"password", "platform":"base" }' http://server/pro720/rest/v10/oauth2/token


POST token.http

POST /pro720/rest/v10/oauth2/token HTTP/1.1
Host: Server
Cache-Control: no-cache
Postman-Token: 83a606e1-26f6-ff21-dd5d-f25f6cb735eb

{ "grant_type":"password", "client_id":"sugar", "client_secret":"", "username":"username", "password":"password", "platform":"base" }

response.json

{
"access_token": "5ee48ec7-023e-ecff-5184-530bd0358868",
"expires_in": 3600,
"token_type": "bearer",
"scope": null,
"refresh_token": "5f197357-0167-f7a6-7912-530bd03275b6",
"refresh_expires_in": 1209600,
"download_token": "5f531625-e301-e3ea-1b11-530bd098be41"
}

 

 

Once you get the response you'll need to hold onto the access_token and the refresh_token. Anytime the access_token is invalidated, you'll want to make another request to the token endpoint with a grant_type of "refresh_token". Store just the refresh_token in long term storage - not the username and password.

 

For all the following requests we are going to pass in a header of OAuth-Token: {{your access token here}}

 

2. Create an Account - Burger Palace

 

Now that we have the access_token we can start having some fun! Let's create a new customer - "Burger Palace" as an Account.

 

Being that everything is RESTful creating a new record is just a POST away. In this case we just do a POST to /rest/v10/Accounts.

 

For this example we will be posting an id as part of the record. You do not need to do this as SugarCRM will automatically generate an id for your record.

 

CURL Accounts.sh

curl -X POST -H OAuth-Token:9126cdb4-0c4e-4796-1a4f-530bccd48060 -H Cache-Control:no-cache -H Postman-Token:cb3331fa-7e62-1f11-9fae-cb9731bc3500 -d '{ "id":"demo_burger_palace", "name":"Burger Palace", "account_type":"Customer", "description":"My Example Account", "email":[{"email_address":"burgers@example.com"}] }' http://server/pro720/rest/v10/Accounts


POST Accounts.http

POST /pro720/rest/v10/Accounts HTTP/1.1
Host: server
OAuth-Token: 9126cdb4-0c4e-4796-1a4f-530bccd48060
Cache-Control: no-cache
Postman-Token: 90329f95-f2a9-fd87-c4cf-421fdd9f24ae

{ "id":"demo_burger_palace", "name":"Burger Palace", "account_type":"Customer", "description":"My Example Account", "email":[{"email_address":"burgers@example.com"}] }

Response.json

{
"id": "demo_burger_palace",
"name": "Burger Palace",
"date_entered": "2014-02-25T10:51:51-08:00",
"date_modified": "2014-02-25T10:51:51-08:00",
"modified_user_id": "1",
"modified_by_name": "Administrator",
"created_by": "1",
"created_by_name": "Administrator",
"doc_owner": "",
"user_favorites": "",
"description": "My Example Account",
"deleted": false,
"assigned_user_id": "",
"assigned_user_name": "",
"team_count": "",
"team_name": [
        {
"id": 1,
"name": "Global",
"name_2": "",
"primary": true
        }
    ],
"email": [
        {
"email_address": "burgers@example.com",
"invalid_email": false,
"opt_out": false,
"primary_address": false,
"reply_to_address": false
        }
    ],
"email1": "burgers@example.com",
"email2": "",
"invalid_email": false,
"email_opt_out": false,
"facebook": "",
"twitter": "",
"googleplus": "",
"account_type": "Customer",
"industry": "",
"annual_revenue": "",
"phone_fax": "",
"billing_address_street": "",
"billing_address_street_2": "",
"billing_address_street_3": "",
"billing_address_street_4": "",
"billing_address_city": "",
"billing_address_state": "",
"billing_address_postalcode": "",
"billing_address_country": "",
"rating": "",
"phone_office": "",
"phone_alternate": "",
"website": "",
"ownership": "",
"employees": "",
"ticker_symbol": "",
"shipping_address_street": "",
"shipping_address_street_2": "",
"shipping_address_street_3": "",
"shipping_address_street_4": "",
"shipping_address_city": "",
"shipping_address_state": "",
"shipping_address_postalcode": "",
"shipping_address_country": "",
"parent_id": "",
"sic_code": "",
"duns_num": "",
"parent_name": "",
"campaign_id": "",
"campaign_name": "",
"my_favorite": false,
"_acl": {
"fields": {}
    },
"following": true,
"_module": "Accounts"
}

 

And of course you can add any additional fields that you would like to set.

 

3. Retrieve a list of Accounts 

 

Great, now that we have records in our system let's fetch them back.  Being RESTful you just need to do a GET request to /rest/v10/Accounts. Don't forget to pass in the OAuth-Token header.  To speed things up be specific about the fields that you want. Let's just fetch the name, account type, and description by passing in fields=name,account_type,description. We'll pass in max_num to specify that we only want 3 records at a time. By default if max_num isn't set it will return 20 records. In this case the request will look like /rest/v10/Accounts?fields=name,account_type,description&max_num=3

 

CURL Accounts.sh

curl -X GET -H OAuth-Token:df488865-4a70-6d88-036c-530ce62453d0 -H Cache-Control:no-cache -H Postman-Token:19a88a98-b2bf-da03-a1f9-ab22730bd3e2 http://server/pro720/rest/v10/Accounts?fields=name,account_type,description&max_num=3


GET Accounts.http

GET /pro720/rest/v10/Accounts?fields=name%2Caccount_type%2Cdescription&max_num=3 HTTP/1.1
Host: server
OAuth-Token: df488865-4a70-6d88-036c-530ce62453d0
Cache-Control: no-cache
Postman-Token: 74200fe5-a926-a5d4-d07d-d899bb1a573e


Response.json

{
"next_offset": 3,
"records": [
        {
"id": "demo_burger_palace",
"name": "Burger Palace",
"date_modified": "2014-02-25T10:51:51-08:00",
"description": "My Example Account",
"account_type": "Customer",
"_acl": {
"fields": {}
            },
"_module": "Accounts"
        },
        {
"id": "ec125a78-f202-250b-e75e-530ca4d7d1d0",
"name": "Ink Conglomerate Inc",
"date_modified": "2014-02-25T06:11:00-08:00",
"description": "",
"account_type": "Customer",
"_acl": {
"fields": {}
            },
"_module": "Accounts"
        },
        {
"id": "ebc56ea4-ef13-b024-c07d-530ca40c97e4",
"name": "Constrata Trust LLC",
"date_modified": "2014-02-25T06:11:00-08:00",
"description": "",
"account_type": "Customer",
"_acl": {
"fields": {}
            },
"_module": "Accounts"
        }
    ]
}

 

 

Excellent! If we wanted to paginate we could just add the offset parameter to the request. That is the number of records you want to skip in the list before returning anything. So passing an offset of 3 would get us to the next page of records.

 

4. Filter a list of Accounts by Name 

 

Now, let's filter our list for records that have the name "Burger Palace". To do this we just need to do a GET request to /rest/v10/Accounts and to pass in the filter parameter. In this case the request would look like /rest/v10/Accounts?filter[0][name]=Burger Palace&fields=name,account_type,description

 

CURL Accounts Filter.sh

curl -X GET -H OAuth-Token:34b05aba-e8be-0db8-5fbe-530d19787689 -H Cache-Control:no-cache -H Postman-Token:347571b3-c873-f846-b469-c002810f4538 http://server/pro720/rest/v10/Accounts?filter[0][name]=Burger Palace&fields=name,account_type,description,email

GET Accounts Filter.http

GET /pro720/rest/v10/Accounts?filter%5B0%5D%5Bname%5D=Burger%20Palace&fields=name%2Caccount_type%2Cdescription%2Cemail HTTP/1.1
Host: server
OAuth-Token: 34b05aba-e8be-0db8-5fbe-530d19787689
Cache-Control: no-cache
Postman-Token: 548a67ec-ebfe-fa37-51a3-698783eaff16


Response.json

{
"next_offset": -1,
"records": [
        {
"id": "demo_burger_palace",
"name": "Burger Palace",
"date_modified": "2014-02-25T14:32:25-08:00",
"description": "My Example Account",
"email": [
                {
"email_address": "burgers@example.com",
"invalid_email": false,
"opt_out": false,
"primary_address": false,
"reply_to_address": false
                }
            ],
"account_type": "Customer",
"_acl": {
"fields": {}
            },
"_module": "Accounts"
        }
    ]
}

 

 

You'll notice in the response that the next_offset in the response is set to -1 which means that there are no more records in the collection.

 

5. Filter a list of Accounts by Names starting with "B" and a specific email address

 

Now, let's pass in multiple filter conditions and let's use some of the filter operations.

 

Sugar 7 supports the following filter operations

$equalsPerforms an exact match on that field.
$not_equalsMatches on non-matching values.
$startsMatches on anything that starts with the value.
$inFinds anything where field matches one of the values as specified as an array.
$not_inFinds anything where field does not match any of the values as specified as an array.
$is_nullChecks if the field is null. This operation does not need a value specified.
$not_nullChecks if the field is not null. This operation does not need a value specified.
$ltMatches when the field is less than the value.
$lteMatches when the field is less than or equal to the value.
$gtMatches when the field is greater than the value.
$gteMatches when the field is greater than or equal to the value.

 

 

So to filter for Account names starting with the letter "B" we'll use the $starts operator. So instead of "filter[0][name]=Burger Palace" we can use filter[0][name][$starts]=B.

 

SugarCRM stores email addresses as normalized values. To search for an Account with an email address we need to search using the relationship for email addresses which is email_addresses. Specifically, we want to filter on the field email_address within that relationship. So to do an exact match on a specific email address we pass in  filter[0][email_addresses.email_address]=burgers@example.com Our request will look like /rest/v10/Accounts?filter[0][name][$starts]=B&filter[0][email_addresses.email_address]=burgers@example.com&fields=name,account_type,description,email . You will notice that we also added email to the list of fields that we wish to have returned which will cause it to return a collection of all the email addresses that are associated with this record.

 

curl -X GET -H OAuth-Token:e2505ba8-fc56-de05-ce9f-530d1bf56227 -H Cache-Control:no-cache -H Postman-Token:cb452a8c-2bb9-ebf6-ae66-a0700b10cdcc http://honey-b/pro720/rest/v10/Accounts?filter[0][name][$starts]=B&filter[0][email_addresses.email_address]=burgers@example.com&fields=name,account_type,description,email
GET /pro720/rest/v10/Accounts?filter%5B0%5D%5Bname%5D%5B%24starts%5D=B&filter%5B0%5D%5Bemail_addresses.email_address%5D=burgers%40example.com&fields=name%2Caccount_type%2Cdescription%2Cemail HTTP/1.1
Host: server
OAuth-Token: e2505ba8-fc56-de05-ce9f-530d1bf56227
Cache-Control: no-cache
Postman-Token: 29bd612a-828d-ad84-7f6a-dd8ba2314956
{
    "next_offset": -1,
    "records": [
        {
            "id": "demo_burger_palace",
            "name": "Burger Palace",
            "date_modified": "2014-02-25T14:32:25-08:00",
            "description": "My Example Account",
            "email": [
                {
                    "email_address": "burgers@example.com",
                    "invalid_email": false,
                    "opt_out": false,
                    "primary_address": false,
                    "reply_to_address": false
                }
            ],
            "account_type": "Customer",
            "_acl": {
                "fields": {}
            },
            "_module": "Accounts"
        }
    ]
}


Continue to Part 2 - Retrieving, Updating, and Relating Records 

Post originally written by sugarmajed.

 

Sometimes the best way to learn something is to dive right in and to get hands on experience. While there are several documents on developing Models, Views, and Controllers for Sugar 7, the core developers wanted to do something a bit different to help everyone get caught up to speed. We want to create a rich set of step by step examples or "recipes" for all the different things that you would want to do with Sugar 7.

 

So I am pleased to announce that we will be publishing a series of blog posts that cover real world applications of customizations and integrations you can build with Sugar 7. We are going to start with the basics and continuously get deeper and more complex. We'll provide support materials to help you follow along at your own pace. You'll need a copy of Sugar 7 that is up and running and don't forget to install elasticsearch before you install Sugar 7.

 

Without further ado,  let's get started cooking with Sugar 7.

Post originally written by John Mertic.

 

http://sugarcon.com

 

App Throwdown is back! Are you ready for it?

 

That's right, we are excited to bring back one of our attendee's favorite parts of SugarCon. It's THE event to see the latest and most innovative ways our incredible partner ecosystem bend and shape Sugar to make it more powerful for customers worldwide. And as a developer, you have the chance to get that amazing app you've built in front of not only other Sugar customers, but the whole tech world.

 

So, do you have a killer customization you’ve done into Sugar? Have you built an interesting application on top of the Sugar platform? Or have you taken Sugar to the next level with integrations into popular applications and services?

 

If you answer "yes" to any of these, it's time to submit your app now before February 28th.