Create Ticket within Workflow

SMCNULTY
Contributor

Good Afternoon,

   I work for a company that has multiple Hubspot accounts. I need to be able to create a new ticket in an account other than my own.

 

I have tested a basic ticket creation in Postman using a private app key setup in the other Hubspot account. I would now like to automate this within a Hubspot workflow under my divisions account.  

 

Can someone help me translate my Postman code over to a custom code solution?  I'm not sure if I still need to use the "SECRET" drop down or "Property to Include in code" drop down if those are embedded in the code below.     I know I have to put my API key within "YOUR_API_KEY" and I expect I have to change the properties to what I want to send to the new ticket but I'm unsure about what else to change.  

 

I am including three snips. One of my working Postman code.

One of the raw code I copied from the Hubspot developer page.

One with as much info as I know to include so far.  Row 17 is marked as having en error so I expect there's a few items I missed here.  Maybe the "string" in row 14 for the contact association.  I just want to get the most BASIC code working first.

 

SMCNULTY_0-1753128799179.png

 

Language set as "Node.js 20.x. This is the code copied from the developer page

SMCNULTY_1-1753128837562.png

 

This is what I have filled in so far:

SMCNULTY_3-1753129295799.png

 

 

7 Accepted solutions
SteveHTM
Solution
Key Advisor

@SMCNULTY - It definitely seems you are 90% of the way to using the API for your purpose. I only have a couple of suggestions that I use in my custom code Python templates:

- Confirm run-time configurtion at the startup of the code:

def main(event):
  
  #get standardized run-time context information
  objectType = event.get('object').get('objectType')
  objectId = event.get('object').get('objectId')
  print("Type: ", objectType, objectId)

- Yes, use the "secret" functionality so that you can rotate you access key as required and not have to find/edit a lot of hard coded values at some point in the future:

  #Get the API key as environment variable
  api_token=os.getenv('CUSTOM_TOKEN')
    
  #HS API Request Headers
  hs_headers = {
       'Content-Type': 'application/json',
       'Authorization': 'Bearer ' + api_token,
  }

- Use property values passed into the code as required and return confirmation value to the containing workflow.

  return {
    "outputFields": {
          "error": hs_error
    }
  }

 

I hope that's helpful.

 

Steve

Steve Christian

HTM Solutions

https://info.htmsolutions.biz/meetings/stevec2

mobilePhone
+1 6195183009
emailAddress
stevec@htmsolutions.biz
website
www.htmsolutions.biz
address
San Diego, CA
Create Your Own Free Signature

View solution in original post

0 Upvotes
MichaelMa
Solution
Top Contributor

You should store your API Key in a secret to prevent it from being hard-coded into the custom coded actions (and therefore available to anyone who has access to the workflow).

 

I can give you a slimmed down version of my code that I use to create tickets. I generally use BatchApi over the BasicApi so I can reuse the code in other locations. The only difference is that for the BasicApi, you send a single object with the properties/associations while in BatchApi you put them in inputs that can store multiple ticket objects.

 

I tried to keep as much relevant code as I could without making it so complicated that it's hard to figure out. I kind of quickly edited the code so forgive me if it doesn't work outright.

 

const hubspot = require('@hubspot/api-client')

/*****
API key is stored in a SECRET. They can be accessed as an enviroment variable once selected
eg, process.env.SECRET_KEY_NAME

To rotate keys if we're create tickets in large batches to avoid rolling 10 limit, 
we push the keys into an array and then modulus to grab a random key when creating the client
*****/
const hapikeyKeys = []
hapikeyKeys.push(process.env.WF_KEY_001)
hapikeyKeys.push(process.env.WF_KEY_002)

var hubspotClient //done outside the main loop so it can be access anywhere

/*****
Main Loop
*****/
exports.main = async (event, callback) => {

  hubspotClient = new hubspot.Client({
    //apiKey: process.env.HAPIKEY
    accessToken: hapikeyKeys[event.object.objectId % hapikeyKeys.length], //random key selected
    numberOfApiCallRetries: 5,
  });

  let proposedtickets = {"inputs": []} //all new tickets stored here
  
  //for (let item of items) {
	let processingData = { properties: {}, associations: [] } //Current ticket we're making, if we're in a loop, it'll reset
	processingData.properties["hs_pipeline"] = 0
	processingData.properties["hs_pipeline_stage"] = 1
	processingData.properties["subject"] = "Unitec PLT Request"
	
	//Contact Associations
	let processingAssociationsContacts = {
		"to": {
			"id": 30716565066
		},
		"types": [
			{
			"associationCategory": "HUBSPOT_DEFINED",
			"associationTypeId": "16" //https://developers.hubspot.com/docs/guides/api/crm/associations/associations-v4#association-type-id-values
			}
		]
	}
	processingData.associations.push(processingAssociationsContacts)
	
	proposedtickets.inputs.push(processingData) //pushing new ticket into the main object

  //}
  
  console.dir(proposedtickets, { depth: null }) //output to see data``

  let newTickets = await createTicketsBatch(proposedtickets) //if successful, new tickets IDs
  
  callback({
    outputFields: {
      newTickets: newTickets //output so other nodes can access the data
    }
  })
}

/*****
  API call to post all the ticket data to batch create
  Requires: 
  data = tickets (JSON formated batched ticket data)
  
  Output:  
  newTickets (IDs of tickets just created)
*****/
async function createTicketsBatch (data) {
  // API call
  try {
    const results = await hubspotClient.crm.tickets.batchApi.create(data) //api request
    return results.results
  } catch (e) {
    console.log(e)
    throw e
  }
}

 

View solution in original post

0 Upvotes
MichaelMa
Solution
Top Contributor

The error says it's an invalid FROM OBJECT for the association because the object ID (30716565066) is not valid. That's just a object Id for a record that I just put in there.

 

If you aren't doing associations, you can comment out the association code. I put it there for reference if you do need to associate the ticket with another object.

 

If you plan to associate the new ticket to another object, be sure to look up the correct associationTypeId. They are DIRECTIONAL. So in this case, it will always be TICKET -> Object (eg, TICKET -> CONTACT = 16).

View solution in original post

MichaelMa
Solution
Top Contributor

Glad I could help!

 

A few notes on my code.

1. WF_KEY_001/WF_KEY_002: You don't need to have two WF Keys. You can remove one of them or have the both secrets be the same API key.  I just have that code there so I can scale to add more API keys to avoid the rolling 10 limit (aka TEN_SECONDLY_ROLLING) which is when you make too many requests in a short period of time (10 seconds, I believe).

https://developers.hubspot.com/docs/guides/apps/api-usage/usage-details

 

Easy to hit if you're batch creating tickets so I just reuse that code for most of my Custom Coded Actions but you can easily remove that area and just have:

 

  hubspotClient = new hubspot.Client({
    accessToken: process.env.SECRET_NAME_HERE,
    numberOfApiCallRetries: 5,
  });

 

Now, the retry argument when creating the client kind of solves this but it still can get annoying (to me). Likewise, you don't need to name them WF_KEY_XXX. Whatever you want to name your secret, you can.

 

2. If you want to set a hubspot_owner_id on the ticket (aka ticket owner), please note that this is the OWNERS object NOT the contact id.

 

3. If you are having issues (erroring out) with the payload (the ticket object being sent in), you can look at the Private App -> audit log. Errors will show the payload and error given. Success doesn't show any useful information.

 

4. The log when testing is limited to 4kb BUT the output data is limited to 65kb. If you need to access something, you can always add a new output variable/reuse the existing one and output what you want. For me, this is usually the payload so I can use a JSON beautifier to see what's specific is not working properly. 

 

Eg, if you wanted to see the full payload so you can play with it in Postman

  callback({
    outputFields: {
      payload: proposedtickets
    }
  })

 

So when you test (probably comment out the await line so it doesn't create the ticket), you'll see the payload as an output variable that doesn't get cut off like it might in the log.

View solution in original post

0 Upvotes
MichaelMa
Solution
Top Contributor

I totally hear you. I have a ton of workflows running quietly/mostly quietly in the background to make it easier for our client success and services team to do their job.

 

I've started on creating CRM Cards to replace some Hubspot functionality so I have greater control over them for specific areas (eg, a form that creates tickets, autosets certain fields AND auto-associates the relevant contacts/companies/custom objects based on some object). It's been an adventure learning this stuff over the last few years.

 

Hubspot uses the callback function for the output variables (aka, allowing you to pass data from one node to another). Until Hubspot provides an alternative, which as far as I'm aware they haven't yet, it's probably fine to ignore the warning. I don't think it's possible to disable it because AWS Lamda is the service Hubspot uses to run your code. We can't really set any environment variables beyond what they allow.

 

Official documentation still uses the callback and the default code that gets entered when you create a new Custom Coded Action node also uses it:

https://developers.hubspot.com/docs/reference/api/automation/custom-code-actions#how-to-define-outpu...

 

Node v20 is the latest version available in Custom Coded Actions. It might take a bit for Hubspot to get to Node v24 so "soon" might be awhile away.   Especially if it's going to be breaking everyone's Custom Coded Actions.

View solution in original post

0 Upvotes
MichaelMa
Solution
Top Contributor

Hmm... I never saw it coded that way.

 

In NodeJS, you can access the properties of an array just by putting in the names, so to speak. The "Property to include in code" data is stored in event.inputFields (case sensitive) so you can access them by doing:

 

event.inputFields.NAME_OF_FIELD_HERE

 

So in your case, it would just be:

 

event.inputFields.unitec_ticket

or

event.inputFields['unitec_ticket']

 

For reference, you can do a console.log/console.dir to output to the console if you're not sure about where something is stored. So if you know that event is the object that is stored but not sure where the data is, you could have slowly gone through all the objects stored in it:

 

console.log("event", event) //event text and show event object

console.log("event.inputFields", event.inputFields) //event.inputFields text and show event.inputFields object

console.dir(event, { depth: null }) //will show more info if it's has many levels rather than truncating it with something like [object] [object]

 

 

View solution in original post

0 Upvotes
MichaelMa
Solution
Top Contributor

I'm using the Hubspot NodeJS library (https://github.com/HubSpot/hubspot-api-nodejs) instead of directly posting to the endpoint but the JSON can be used interchangably between Postman and the library. It generally shouldn't be an issue as I do it all the time to verify the payload.

 

Ideally, by using the library, it provides a more consistent way to call the endpoints and push the payload.

 

This part of the code:

hubspotClient.crm.tickets.batchApi.create

 

Pushes to the endpoint (as defined by the library)

https://api.hubapi.com/crm/v3/objects/tickets/batch/create

 

So the JSON you used in Postman should work fine with the library endpoint. No conversion is neccesary.

 

Some limitations exist with the library where new features might not be available in the current available version. 

 

Looking at your code, you can copy the existing function and adjust to do update:

 

/*****
  API call to post all the ticket data to batch update
  Requires: 
  data = tickets (JSON formated batched ticket data)
  
  Output:  
  updatedTickets (IDs of tickets just updated)
*****/
async function updateTicketsBatch (data) {
  // API call
  try {
    const results = await hubspotClient.crm.tickets.batchApi.update(data) //api request
    return results.results
  } catch (e) {
    console.log(e)
    throw e
  }
}

 

Basically changing batchApi.create -> batchApi.update (and a few textually changes).

 

As for why the postman converted code doesn't work, it looks like the Authorization method isn't set prooperly. It should have the word Bearer:

 

'Authorization': 'Bearer ••••••',

 

View solution in original post

0 Upvotes
18 Replies 18
MichaelMa
Solution
Top Contributor

You should store your API Key in a secret to prevent it from being hard-coded into the custom coded actions (and therefore available to anyone who has access to the workflow).

 

I can give you a slimmed down version of my code that I use to create tickets. I generally use BatchApi over the BasicApi so I can reuse the code in other locations. The only difference is that for the BasicApi, you send a single object with the properties/associations while in BatchApi you put them in inputs that can store multiple ticket objects.

 

I tried to keep as much relevant code as I could without making it so complicated that it's hard to figure out. I kind of quickly edited the code so forgive me if it doesn't work outright.

 

const hubspot = require('@hubspot/api-client')

/*****
API key is stored in a SECRET. They can be accessed as an enviroment variable once selected
eg, process.env.SECRET_KEY_NAME

To rotate keys if we're create tickets in large batches to avoid rolling 10 limit, 
we push the keys into an array and then modulus to grab a random key when creating the client
*****/
const hapikeyKeys = []
hapikeyKeys.push(process.env.WF_KEY_001)
hapikeyKeys.push(process.env.WF_KEY_002)

var hubspotClient //done outside the main loop so it can be access anywhere

/*****
Main Loop
*****/
exports.main = async (event, callback) => {

  hubspotClient = new hubspot.Client({
    //apiKey: process.env.HAPIKEY
    accessToken: hapikeyKeys[event.object.objectId % hapikeyKeys.length], //random key selected
    numberOfApiCallRetries: 5,
  });

  let proposedtickets = {"inputs": []} //all new tickets stored here
  
  //for (let item of items) {
	let processingData = { properties: {}, associations: [] } //Current ticket we're making, if we're in a loop, it'll reset
	processingData.properties["hs_pipeline"] = 0
	processingData.properties["hs_pipeline_stage"] = 1
	processingData.properties["subject"] = "Unitec PLT Request"
	
	//Contact Associations
	let processingAssociationsContacts = {
		"to": {
			"id": 30716565066
		},
		"types": [
			{
			"associationCategory": "HUBSPOT_DEFINED",
			"associationTypeId": "16" //https://developers.hubspot.com/docs/guides/api/crm/associations/associations-v4#association-type-id-values
			}
		]
	}
	processingData.associations.push(processingAssociationsContacts)
	
	proposedtickets.inputs.push(processingData) //pushing new ticket into the main object

  //}
  
  console.dir(proposedtickets, { depth: null }) //output to see data``

  let newTickets = await createTicketsBatch(proposedtickets) //if successful, new tickets IDs
  
  callback({
    outputFields: {
      newTickets: newTickets //output so other nodes can access the data
    }
  })
}

/*****
  API call to post all the ticket data to batch create
  Requires: 
  data = tickets (JSON formated batched ticket data)
  
  Output:  
  newTickets (IDs of tickets just created)
*****/
async function createTicketsBatch (data) {
  // API call
  try {
    const results = await hubspotClient.crm.tickets.batchApi.create(data) //api request
    return results.results
  } catch (e) {
    console.log(e)
    throw e
  }
}

 

0 Upvotes
SMCNULTY
Contributor

OK so I quickly tried your code.  Confirm this is correct:

I made two private apps with appropriate permissions. I copied the API from each and created two secrets within the workflow custom code.  I named them WF_KEY_001 and WF_KEY_002.   I didn't touch anything else in the code.   I got a whopping error response below.

SMCNULTY_1-1753303329299.png

 

 

SMCNULTY_0-1753303275941.png

 

 

0 Upvotes
MichaelMa
Solution
Top Contributor

The error says it's an invalid FROM OBJECT for the association because the object ID (30716565066) is not valid. That's just a object Id for a record that I just put in there.

 

If you aren't doing associations, you can comment out the association code. I put it there for reference if you do need to associate the ticket with another object.

 

If you plan to associate the new ticket to another object, be sure to look up the correct associationTypeId. They are DIRECTIONAL. So in this case, it will always be TICKET -> Object (eg, TICKET -> CONTACT = 16).

SMCNULTY
Contributor

OK I swapped out the contact ID over to my email.  After so many weeks of struggling I finally got the GREEN light!  First ever in custom code. I've seen a hundred red/fails.  THANK YOU!  I just needed somewhere to start, I can tweak from here for additional properties and associations.

 

SMCNULTY_0-1753389925384.png

 

MichaelMa
Solution
Top Contributor

Glad I could help!

 

A few notes on my code.

1. WF_KEY_001/WF_KEY_002: You don't need to have two WF Keys. You can remove one of them or have the both secrets be the same API key.  I just have that code there so I can scale to add more API keys to avoid the rolling 10 limit (aka TEN_SECONDLY_ROLLING) which is when you make too many requests in a short period of time (10 seconds, I believe).

https://developers.hubspot.com/docs/guides/apps/api-usage/usage-details

 

Easy to hit if you're batch creating tickets so I just reuse that code for most of my Custom Coded Actions but you can easily remove that area and just have:

 

  hubspotClient = new hubspot.Client({
    accessToken: process.env.SECRET_NAME_HERE,
    numberOfApiCallRetries: 5,
  });

 

Now, the retry argument when creating the client kind of solves this but it still can get annoying (to me). Likewise, you don't need to name them WF_KEY_XXX. Whatever you want to name your secret, you can.

 

2. If you want to set a hubspot_owner_id on the ticket (aka ticket owner), please note that this is the OWNERS object NOT the contact id.

 

3. If you are having issues (erroring out) with the payload (the ticket object being sent in), you can look at the Private App -> audit log. Errors will show the payload and error given. Success doesn't show any useful information.

 

4. The log when testing is limited to 4kb BUT the output data is limited to 65kb. If you need to access something, you can always add a new output variable/reuse the existing one and output what you want. For me, this is usually the payload so I can use a JSON beautifier to see what's specific is not working properly. 

 

Eg, if you wanted to see the full payload so you can play with it in Postman

  callback({
    outputFields: {
      payload: proposedtickets
    }
  })

 

So when you test (probably comment out the await line so it doesn't create the ticket), you'll see the payload as an output variable that doesn't get cut off like it might in the log.

0 Upvotes
SMCNULTY
Contributor

Great thank you for that addional information. Especially about the error catching and review.  I haven't experimented with output variables but that's definitely next.  I'd like to make the code robust with error handling. I would even like to be notified when it fails, so I can go in and troubleshoot.  Our every day customer service reps will just be changing ticket status to trigger this code, they won't know or care if it actually worked on the back end or not.

 

I do get this warning from within the hubspot custom code response.  I don't think it's hurting anything, but if I can supress it or modify the code to meet the Node.JS 24 standard, all the better. I noticed you used callback in your above examle and it seems support for that will be gone soon.

 

SMCNULTY_0-1753461381760.png

 

0 Upvotes
MichaelMa
Solution
Top Contributor

I totally hear you. I have a ton of workflows running quietly/mostly quietly in the background to make it easier for our client success and services team to do their job.

 

I've started on creating CRM Cards to replace some Hubspot functionality so I have greater control over them for specific areas (eg, a form that creates tickets, autosets certain fields AND auto-associates the relevant contacts/companies/custom objects based on some object). It's been an adventure learning this stuff over the last few years.

 

Hubspot uses the callback function for the output variables (aka, allowing you to pass data from one node to another). Until Hubspot provides an alternative, which as far as I'm aware they haven't yet, it's probably fine to ignore the warning. I don't think it's possible to disable it because AWS Lamda is the service Hubspot uses to run your code. We can't really set any environment variables beyond what they allow.

 

Official documentation still uses the callback and the default code that gets entered when you create a new Custom Coded Action node also uses it:

https://developers.hubspot.com/docs/reference/api/automation/custom-code-actions#how-to-define-outpu...

 

Node v20 is the latest version available in Custom Coded Actions. It might take a bit for Hubspot to get to Node v24 so "soon" might be awhile away.   Especially if it's going to be breaking everyone's Custom Coded Actions.

0 Upvotes
SMCNULTY
Contributor

I will carefully strip back what I don't really need at a later date. (like the rotating key)  As long as it's working I'm under some pressure to get my demo done for this project.  In case you couldn't tell I'm not a programmer, I just play one on TV.  No one else is willing to do this stuff here.

 

While I have your attention, My next step was to include a property in the data push from one account to another.   When I hardcode the property value, (pipeline/status/ticket name etc) those transfer over fine to the new ticket but when I try to use a property, it's not working. (I am giving the property a value in the workflow)   I think it has to do with my GET command, but I've tried every combo of:

'Unitec Ticket'

Unitec Ticket

'unitec_ticket'

unitec ticket

 

I have IDENTICAL properties between the two accounts with Identical internal names.

For both - Unitec Ticket is the Property Label and unitec_ticket is the internal name.

SMCNULTY_0-1753471966311.png

 

One problem I just found for sure was this:

processingData.properties["Unitec Ticket"] = UnitecTicketNumber

should use the internal name

processingData.properties["unitec_ticket"] = UnitecTicketNumber

 

but still erroring out so I must have multiple issues here

0 Upvotes
MichaelMa
Solution
Top Contributor

Hmm... I never saw it coded that way.

 

In NodeJS, you can access the properties of an array just by putting in the names, so to speak. The "Property to include in code" data is stored in event.inputFields (case sensitive) so you can access them by doing:

 

event.inputFields.NAME_OF_FIELD_HERE

 

So in your case, it would just be:

 

event.inputFields.unitec_ticket

or

event.inputFields['unitec_ticket']

 

For reference, you can do a console.log/console.dir to output to the console if you're not sure about where something is stored. So if you know that event is the object that is stored but not sure where the data is, you could have slowly gone through all the objects stored in it:

 

console.log("event", event) //event text and show event object

console.log("event.inputFields", event.inputFields) //event.inputFields text and show event.inputFields object

console.dir(event, { depth: null }) //will show more info if it's has many levels rather than truncating it with something like [object] [object]

 

 

0 Upvotes
SMCNULTY
Contributor

I coded it that way (as an experiment) because if you follow the link to the HS documentation that's what it showed. but now that I look back, that's a Python example. That was my mistake.

SMCNULTY_1-1753473970915.png

The HS JS example is this:

SMCNULTY_2-1753474040310.png

which is like what you suggested.   Instead of defining "unitecticketnumber" as a VAR up top. I took that out and just put in this line.

SMCNULTY_3-1753474412928.png

 

 

Works!!  Thanks again! i'm creeping toward success here.  I can now create a ticket and push over whatever properties I want.   Next will be different objects(?) like you mentioned Ticket Owner (hubspot_owner_ID) and I also want to look into pulling contact/company internal ID's.   I can do that in Postman and I've been using the lame method of checking the URL for the ID but I have to get that into code. 

 

 

0 Upvotes
MichaelMa
Top Contributor

Everyone starts somewhere and I'm happy help where I can.

 

Sometimes things are just easier to hardcode. I've done this for ticket owner mostly because the ticket owners are a limited number of people. Easier to hardcode it and put it in than to API search for it. 

 

Also note that you can access associated data of the current object in a limited via the Edit -> Edit available records. Super helpful if you need like the company or contacts associated with the Deal so you don't have to API request it. It's relatively limited but sometimes that's enough.

0 Upvotes
SMCNULTY
Contributor

This is likely a dumb question but I looked through your (working) code and I don't see where the URL is called out.    Like api.hubspot.com/crm/v3/objects/tickets.

 

For some reason I have no problem at all getting these API calls to work in Postman.  It would be so much more helpful/easier if I could just copy the JSON code over from Postman to Hubspot but there are almost always errors.

 

This took all of 5 minutes to get working:

SMCNULTY_2-1753716116976.png

 

 

But when I try all four of these JS Postman "conversions" none work. (I am copying in my API code)

SMCNULTY_1-1753716047058.png

 

NodeJS - Axios

 

const axios = require('axios');
let data = JSON.stringify({
  "properties": {
    "otis_ticket": "12345"
  }
});

let config = {
  method: 'patch',
  maxBodyLength: Infinity,
  headers: {
    'Content-Type': 'application/json',
    'Authorization': '••••••',
    'Cookie': '__cf_bm=yZkctTLsozMjwzvhVHKJzQ8xuEdN0fBrIkewSin4mpc-1753715052-1.0.1.1-dpEsJkgVM_EHcb5QfaTjLl3KyNIsezUFg4JtoDvR2jwB7bW9UN5bVLgTxRnHGA_L7AGgKRarHNiE6nUsoOQuUMTbaTk0wqqat1FHrIjq2rk'
  },
  data : data
};

axios.request(config)
.then((response) => {
  console.log(JSON.stringify(response.data));
})
.catch((error) => {
  console.log(error);
});

 

NodeJS - Native

var https = require('follow-redirects').https;
var fs = require('fs');

var options = {
  'method': 'PATCH',
  'hostname': 'api.hubapi.com',
  'path': '/crm/v3/objects/tickets/26941972792',
  'headers': {
    'Content-Type': 'application/json',
    'Authorization': '••••••',
    'Cookie': '__cf_bm=yZkctTLsozMjwzvhVHKJzQ8xuEdN0fBrIkewSin4mpc-1753715052-1.0.1.1-dpEsJkgVM_EHcb5QfaTjLl3KyNIsezUFg4JtoDvR2jwB7bW9UN5bVLgTxRnHGA_L7AGgKRarHNiE6nUsoOQuUMTbaTk0wqqat1FHrIjq2rk'
  },
  'maxRedirects': 20
};

var req = https.request(options, function (res) {
  var chunks = [];

  res.on("data", function (chunk) {
    chunks.push(chunk);
  });

  res.on("end", function (chunk) {
    var body = Buffer.concat(chunks);
    console.log(body.toString());
  });

  res.on("error", function (error) {
    console.error(error);
  });
});

var postData = JSON.stringify({
  "properties": {
    "otis_ticket": "12345"
  }
});

req.write(postData);

req.end();
 

NodeJS - Request

 

var request = require('request');
var options = {
  'method': 'PATCH',
  'headers': {
    'Content-Type': 'application/json',
    'Authorization': '••••••',
    'Cookie': '__cf_bm=yZkctTLsozMjwzvhVHKJzQ8xuEdN0fBrIkewSin4mpc-1753715052-1.0.1.1-dpEsJkgVM_EHcb5QfaTjLl3KyNIsezUFg4JtoDvR2jwB7bW9UN5bVLgTxRnHGA_L7AGgKRarHNiE6nUsoOQuUMTbaTk0wqqat1FHrIjq2rk'
  },
  body: JSON.stringify({
    "properties": {
      "otis_ticket": "12345"
    }
  })

};
request(options, function (error, response) {
  if (error) throw new Error(error);
  console.log(response.body);
});
 

NodeJS - Unirest

 

var unirest = require('unirest');
  .headers({
    'Content-Type': 'application/json',
    'Authorization': '••••••',
    'Cookie': '__cf_bm=yZkctTLsozMjwzvhVHKJzQ8xuEdN0fBrIkewSin4mpc-1753715052-1.0.1.1-dpEsJkgVM_EHcb5QfaTjLl3KyNIsezUFg4JtoDvR2jwB7bW9UN5bVLgTxRnHGA_L7AGgKRarHNiE6nUsoOQuUMTbaTk0wqqat1FHrIjq2rk'
  })
  .send(JSON.stringify({
    "properties": {
      "otis_ticket": "12345"
    }
  }))
  .end(function (res) {
    if (res.error) throw new Error(res.error);
    console.log(res.raw_body);
  });

 

0 Upvotes
MichaelMa
Solution
Top Contributor

I'm using the Hubspot NodeJS library (https://github.com/HubSpot/hubspot-api-nodejs) instead of directly posting to the endpoint but the JSON can be used interchangably between Postman and the library. It generally shouldn't be an issue as I do it all the time to verify the payload.

 

Ideally, by using the library, it provides a more consistent way to call the endpoints and push the payload.

 

This part of the code:

hubspotClient.crm.tickets.batchApi.create

 

Pushes to the endpoint (as defined by the library)

https://api.hubapi.com/crm/v3/objects/tickets/batch/create

 

So the JSON you used in Postman should work fine with the library endpoint. No conversion is neccesary.

 

Some limitations exist with the library where new features might not be available in the current available version. 

 

Looking at your code, you can copy the existing function and adjust to do update:

 

/*****
  API call to post all the ticket data to batch update
  Requires: 
  data = tickets (JSON formated batched ticket data)
  
  Output:  
  updatedTickets (IDs of tickets just updated)
*****/
async function updateTicketsBatch (data) {
  // API call
  try {
    const results = await hubspotClient.crm.tickets.batchApi.update(data) //api request
    return results.results
  } catch (e) {
    console.log(e)
    throw e
  }
}

 

Basically changing batchApi.create -> batchApi.update (and a few textually changes).

 

As for why the postman converted code doesn't work, it looks like the Authorization method isn't set prooperly. It should have the word Bearer:

 

'Authorization': 'Bearer ••••••',

 

0 Upvotes
SMCNULTY
Contributor

ok I answered at least part of this.  I believe it's because of importing the JS library.  That's why there's no direct URL.  I have to go check out the docs on that. 

0 Upvotes
SMCNULTY
Contributor

OK thank you two for the input.  I will try and get this going and report back with any issues.  Sorry I had to remove the solution approval here. I'm not sure if Hubspot automatically selects it after a period of time but I haven't yet had time to evaluate this.

0 Upvotes
SteveHTM
Solution
Key Advisor

@SMCNULTY - It definitely seems you are 90% of the way to using the API for your purpose. I only have a couple of suggestions that I use in my custom code Python templates:

- Confirm run-time configurtion at the startup of the code:

def main(event):
  
  #get standardized run-time context information
  objectType = event.get('object').get('objectType')
  objectId = event.get('object').get('objectId')
  print("Type: ", objectType, objectId)

- Yes, use the "secret" functionality so that you can rotate you access key as required and not have to find/edit a lot of hard coded values at some point in the future:

  #Get the API key as environment variable
  api_token=os.getenv('CUSTOM_TOKEN')
    
  #HS API Request Headers
  hs_headers = {
       'Content-Type': 'application/json',
       'Authorization': 'Bearer ' + api_token,
  }

- Use property values passed into the code as required and return confirmation value to the containing workflow.

  return {
    "outputFields": {
          "error": hs_error
    }
  }

 

I hope that's helpful.

 

Steve

Steve Christian

HTM Solutions

https://info.htmsolutions.biz/meetings/stevec2

mobilePhone
+1 6195183009
emailAddress
stevec@htmsolutions.biz
website
www.htmsolutions.biz
address
San Diego, CA
Create Your Own Free Signature
0 Upvotes
SMCNULTY
Contributor

I was able to get this chunk of code working.  Basically cut and paste luck.  It is producing an error but still actually makes a new ticket.

 

SMCNULTY_1-1753131997790.pngSMCNULTY_2-1753132022852.png

 

 

 

0 Upvotes
MichaelMa
Top Contributor

The error is the generally you want the main part of your code to exist in the exports.main loop. Things outside the main loop are considered "global".

0 Upvotes