HLAI Experts

Guide · Email

HighLevel email API gotchas: threading, scheduled sends and sender name

Four details catch out GoHighLevel (GHL) email integrations. A reply stays in its thread when you send both replyMessageId and threadId; scheduledTimestamp is in UTC seconds; and a scheduled email is cancelled with DELETE on its own email message id, not the conversation id. In our builds, the sender name only came through when emailFrom carried the name and address together.

Rob EasonPublished

Replies that start a new thread

If a reply sent through the API shows up as a fresh message instead of under the email it answers, look at two fields in the send request. They do different jobs.

replyMessageId identifies the message you're answering. threadId identifies the parent email thread it belongs to. Send both on every reply. Keep the ids from the inbound message when you receive it, so they're on hand when the reply goes out.

There's one more field to set on purpose: emailReplyMode takes reply or reply_all. Pick the one you mean. A reply that should reach everyone on the thread needs reply_all.

Scheduled sends: UTC, in seconds

scheduledTimestamp is a UTC time counted in seconds. Two mistakes follow from that, and both send the email at the wrong moment.

  • Sending local time. Convert the contact's or the business's local send time to UTC before you build the timestamp.
  • Sending milliseconds. Some languages hand you milliseconds by default; JavaScript's Date.now() is one. Divide by 1,000 and round to a whole number.

Cancelling a scheduled email

The cancel call is DELETE /conversations/messages/email/{emailMessageId}/schedule. It needs the conversations/message.write scope and a sub-account token, and the id in the path is the email message id.

Don't put the conversation id there. It's an easy id to reach for, and it's the wrong one. Store the email message id when you schedule the send, so cancelling doesn't depend on looking it up later.

HighLevel's cancel page is inconsistent with itself: one line says to post the messageId, while the route it documents uses DELETE and an emailMessageId. Follow the route and the method.

The call returns a status and a message when it fails. Log both and check them, rather than assuming a scheduled email is gone because the request didn't throw.

Cancel by the email's own id, with DELETE. The conversation id won't do.

Getting the sender name to show

emailFrom sets the sending address. In our builds, the recipient only saw a sender name when that field was written as a name and address together, in the form Name <address>.

That's our observation, not something HighLevel's API page spells out. Before you rely on it, send a test to an inbox you control and look at the sender line the way a contact would.

Inbound email with no direction

If you list messages and sort them into inbound and outbound by the direction field, watch for empty values. We've seen inbound email come back from the message list with a null direction.

Code that treats anything not marked inbound as outbound will misfile those messages, and anything that reacts to inbound mail will miss them. Treat a null direction as unknown, log it, and decide from the rest of the message what it is.

A check before you ship

Run through this list against a test contact before the integration touches real conversations.

  1. Send a reply with replyMessageId and threadId and confirm it lands inside the original thread.
  2. Schedule a send a few minutes out, in UTC seconds, and confirm it goes at the right minute.
  3. Schedule another, cancel it with DELETE and its email message id, and confirm it never sends.
  4. Check the sender line on a received test email.
  5. List messages and look for any with a null direction.

Rather have it set up for you?

Once we've had a 30-minute 1:1 with Rob, we wire your email sends, replies and scheduled messages into HighLevel and test each against a live inbox.

Book a 1:1 with Rob

Questions

Email API questions from developers

How do I reply inside an existing email thread with the HighLevel API?
Send the reply with both replyMessageId, the message you're answering, and threadId, the parent thread. Set emailReplyMode to reply or reply_all depending on who should get it.
What format does scheduledTimestamp use?
UTC, counted in seconds. Convert local times to UTC first, and if your language gives milliseconds, divide by 1,000 before sending.
How do I cancel a scheduled email in HighLevel through the API?
Call DELETE /conversations/messages/email/{emailMessageId}/schedule with a sub-account token that has the conversations/message.write scope. Use the email message id in the path, not the conversation id.
Why does my cancel request fail?
Check the id first: the route expects the email message id, and a conversation id won't match. Then check the token is a sub-account token with conversations/message.write. The response includes a status and a message that say what went wrong.
How do I set the sender name on an email sent through the API?
Set it in emailFrom. In our builds the name showed only when the field held the name and address together, as Name <address>. Send yourself a test and check the sender line before going live.
Rob Eason

Who you talk to

Rob Eason

Rob runs HLAI Experts and takes the 1:1 calls himself.

He is a HighLevel Certified Admin with the second-most reviews in HighLevel's Certified Admin Directory worldwide, all of them 5 out of 5.

He won HighLevel's Top New Subaccounts via AI Employee award in its 2025 Summer of AI Awards and taught HighLevel's official masterclass on its AI Employee.

Before HighLevel, he co-owned a 60-employee financial services firm and sold his share.

On the call he looks at your account, not a script.

5/5 across 83 reviews in HighLevel's Certified Admin Directory