Skip to content
APIHiver

Hiver API Tutorial: Automate Your Gmail Helpdesk (2026)

A complete guide to the Hiver API — authenticate, manage conversations, assign emails, add notes, and build automations on top of your shared Gmail inbox. Node.js and Python code included.

10 min readBy Rachana Sanghani

If your support team lives inside Gmail, there is a good chance Hiver is already managing your shared inboxes. What fewer teams know is that Hiver ships a full REST API — one that lets you do everything the dashboard can do, but programmatically: assign conversations, add notes, update tags, pull analytics, and trigger workflows from any system that can make an HTTP request.

This tutorial covers the complete API surface: authentication, the key endpoints, working code in Node.js and Python, webhooks for event-driven integrations, and the patterns that make Hiver automations reliable at scale.

What is Hiver?#

Hiver is a shared inbox and helpdesk tool built directly inside Gmail. Instead of a separate support platform (Zendesk, Freshdesk, Intercom), your team manages customer emails in the same Gmail interface they already use, with Hiver adding a layer of assignment, tagging, notes, SLA tracking, and analytics on top.

For developers, the key thing to understand is that Hiver's data model maps closely to email: every incoming message becomes a conversation, which belongs to a mailbox (a shared email address like support@company.com), and can be assigned to a user (an agent on your team).

The API gives you full programmatic access to that model.

Getting your API key#

API access is available on the Pro plan and above.

  1. Log into your Hiver account at app.hiverhq.com.
  2. Go to Settings → API.
  3. Click Generate New Key.
  4. Copy the key — it will only be shown once.

Store it as:

export HIVER_API_KEY="your_api_key_here"

Base URL and authentication#

All Hiver API requests go to:

https://api.hiverhq.com/api/v1/

Authentication is Bearer token. Add this header to every request:

Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

A minimal health check to confirm your key works:

curl -s \
  -H "Authorization: Bearer $HIVER_API_KEY" \
  -H "Content-Type: application/json" \
  https://api.hiverhq.com/api/v1/mailboxes

A 200 OK with a JSON array means you're authenticated. A 401 means the key is wrong or hasn't been activated yet.

Core data model#

Before diving into endpoints, it helps to know how the four main objects relate to each other:

ObjectWhat it is
MailboxA shared inbox (e.g. support@company.com). All conversations belong to one.
ConversationA single customer email thread. Has a status (open, pending, closed).
UserAn agent who can be assigned conversations.
NoteAn internal note on a conversation — visible to agents only.
TagA label you can apply to conversations for routing or reporting.

Most API operations take a conversation_id and act on that conversation. The mailbox_id is used for listing and filtering.

Node.js setup#

npm install node-fetch dotenv
// hiver-client.js
import fetch from "node-fetch";
 
const BASE = "https://api.hiverhq.com/api/v1";
const KEY = process.env.HIVER_API_KEY;
 
export async function hiverRequest(path, options = {}) {
  const res = await fetch(`${BASE}${path}`, {
    ...options,
    headers: {
      "Authorization": `Bearer ${KEY}`,
      "Content-Type": "application/json",
      ...(options.headers ?? {})
    }
  });
 
  if (!res.ok) {
    const body = await res.text();
    throw new Error(`Hiver API ${res.status}: ${body}`);
  }
 
  return res.json();
}

Python setup#

# hiver_client.py
import os
import requests
 
BASE = "https://api.hiverhq.com/api/v1"
HEADERS = {
    "Authorization": f"Bearer {os.environ['HIVER_API_KEY']}",
    "Content-Type": "application/json",
}
 
def hiver_request(method, path, **kwargs):
    r = requests.request(method, f"{BASE}{path}", headers=HEADERS, timeout=15, **kwargs)
    r.raise_for_status()
    return r.json()

Key endpoints#

List mailboxes#

The starting point for most integrations — get the IDs for the shared inboxes you want to interact with.

// Node.js
const { mailboxes } = await hiverRequest("/mailboxes");
for (const mb of mailboxes) {
  console.log(mb.id, mb.name, mb.email);
}
# Python
data = hiver_request("GET", "/mailboxes")
for mb in data["mailboxes"]:
    print(mb["id"], mb["name"], mb["email"])

Response shape:

{
  "mailboxes": [
    {
      "id": "mb_01HXYZ",
      "name": "Support",
      "email": "support@company.com",
      "type": "shared"
    }
  ]
}

List conversations#

Fetch open conversations from a mailbox. The status filter accepts open, pending, or closed.

// Node.js — list open conversations
const { conversations } = await hiverRequest(
  "/conversations?mailbox_id=mb_01HXYZ&status=open&limit=50"
);
 
for (const conv of conversations) {
  console.log(conv.id, conv.subject, conv.assignee?.name ?? "unassigned");
}
# Python — list open conversations
data = hiver_request("GET", "/conversations", params={
    "mailbox_id": "mb_01HXYZ",
    "status": "open",
    "limit": 50,
})
 
for conv in data["conversations"]:
    assignee = conv.get("assignee", {}).get("name", "unassigned")
    print(conv["id"], conv["subject"], assignee)

Assign a conversation#

Assign an open conversation to a specific agent by their user ID.

// Node.js
await hiverRequest(`/conversations/${convId}/assign`, {
  method: "POST",
  body: JSON.stringify({ user_id: "usr_01HABC" })
});
# Python
hiver_request("POST", f"/conversations/{conv_id}/assign",
              json={"user_id": "usr_01HABC"})

Update conversation status#

Close a resolved conversation or reopen a previously closed one.

// Node.js — close a conversation
await hiverRequest(`/conversations/${convId}/status`, {
  method: "PUT",
  body: JSON.stringify({ status: "closed" })
});
# Python — reopen a conversation
hiver_request("PUT", f"/conversations/{conv_id}/status",
              json={"status": "open"})

Valid status values: open, pending, closed.

Add an internal note#

Notes are internal — the customer never sees them. They're the standard way to leave context for teammates, flag follow-ups, or log automation actions.

// Node.js
await hiverRequest(`/conversations/${convId}/notes`, {
  method: "POST",
  body: JSON.stringify({
    body: "Auto-assigned to billing team based on subject keyword.",
    type: "internal"
  })
});
# Python
hiver_request("POST", f"/conversations/{conv_id}/notes", json={
    "body": "Auto-assigned to billing team based on subject keyword.",
    "type": "internal",
})

Apply a tag#

Tags are how you categorize and route conversations. Fetch available tags first, then apply by tag ID.

// Node.js — list tags, then apply one
const { tags } = await hiverRequest("/tags");
const billingTag = tags.find(t => t.name === "billing");
 
await hiverRequest(`/conversations/${convId}/tags`, {
  method: "POST",
  body: JSON.stringify({ tag_id: billingTag.id })
});
# Python
tags_data = hiver_request("GET", "/tags")
billing_tag = next(t for t in tags_data["tags"] if t["name"] == "billing")
 
hiver_request("POST", f"/conversations/{conv_id}/tags",
              json={"tag_id": billing_tag["id"]})

Pagination#

All list endpoints return a next_cursor field when more results exist. Pass it as cursor on the next call:

// Node.js — paginate all open conversations
async function getAllOpenConversations(mailboxId) {
  const results = [];
  let cursor = null;
 
  do {
    const params = new URLSearchParams({
      mailbox_id: mailboxId,
      status: "open",
      limit: "100",
      ...(cursor ? { cursor } : {})
    });
 
    const { conversations, next_cursor } = await hiverRequest(
      `/conversations?${params}`
    );
 
    results.push(...conversations);
    cursor = next_cursor ?? null;
  } while (cursor);
 
  return results;
}
# Python — paginate all open conversations
def get_all_open_conversations(mailbox_id):
    results = []
    cursor = None
 
    while True:
        params = {"mailbox_id": mailbox_id, "status": "open", "limit": 100}
        if cursor:
            params["cursor"] = cursor
 
        data = hiver_request("GET", "/conversations", params=params)
        results.extend(data["conversations"])
        cursor = data.get("next_cursor")
        if not cursor:
            break
 
    return results

Webhooks#

Polling is wasteful for real-time needs. Hiver webhooks push events to your endpoint the moment something happens.

Setting up a webhook#

  1. Go to Settings → Integrations → Webhooks.
  2. Click Add Webhook.
  3. Enter your endpoint URL (must be publicly reachable over HTTPS).
  4. Select the events you want: conversation.created, conversation.assigned, conversation.status_changed, note.added, and others.
  5. Save. Hiver will send a test ping to verify the URL.

Handling webhook payloads#

// Node.js — Express webhook handler
import express from "express";
 
const app = express();
app.use(express.json());
 
app.post("/webhooks/hiver", (req, res) => {
  const { event, data } = req.body;
 
  switch (event) {
    case "conversation.created":
      console.log("New conversation:", data.conversation.id, data.conversation.subject);
      // Auto-tag, auto-assign, etc.
      break;
 
    case "conversation.assigned":
      console.log(
        `Assigned ${data.conversation.id} to ${data.assignee.name}`
      );
      break;
 
    case "conversation.status_changed":
      if (data.status === "closed") {
        console.log("Conversation closed:", data.conversation.id);
        // Trigger post-resolution workflow
      }
      break;
  }
 
  res.sendStatus(200);
});
 
app.listen(3000);
# Python — Flask webhook handler
from flask import Flask, request, jsonify
 
app = Flask(__name__)
 
@app.route("/webhooks/hiver", methods=["POST"])
def hiver_webhook():
    payload = request.get_json()
    event = payload.get("event")
    data = payload.get("data", {})
 
    if event == "conversation.created":
        conv = data.get("conversation", {})
        print(f"New: {conv.get('id')}{conv.get('subject')}")
 
    elif event == "conversation.status_changed":
        if data.get("status") == "closed":
            print(f"Closed: {data['conversation']['id']}")
 
    return jsonify({"ok": True}), 200

Real-world pattern: auto-assign by keyword#

Here's a complete example that wires together several endpoints — pull open unassigned conversations, detect keywords in the subject line, tag and assign them to the right team member, and leave a note explaining why.

// auto-assign.js — run on a schedule (e.g. every 5 minutes)
import "dotenv/config";
import { hiverRequest } from "./hiver-client.js";
 
const RULES = [
  { keyword: "invoice",  tag: "billing",   assignee: "usr_01BILLING" },
  { keyword: "refund",   tag: "billing",   assignee: "usr_01BILLING" },
  { keyword: "login",    tag: "technical", assignee: "usr_01TECH" },
  { keyword: "password", tag: "technical", assignee: "usr_01TECH" },
];
 
async function getTagId(name) {
  const { tags } = await hiverRequest("/tags");
  return tags.find(t => t.name === name)?.id;
}
 
async function autoAssign(mailboxId) {
  const tagIds = Object.fromEntries(
    await Promise.all(
      [...new Set(RULES.map(r => r.tag))].map(async name => [name, await getTagId(name)])
    )
  );
 
  const { conversations } = await hiverRequest(
    `/conversations?mailbox_id=${mailboxId}&status=open&limit=100`
  );
 
  const unassigned = conversations.filter(c => !c.assignee);
 
  for (const conv of unassigned) {
    const subject = (conv.subject ?? "").toLowerCase();
    const rule = RULES.find(r => subject.includes(r.keyword));
    if (!rule) continue;
 
    await Promise.all([
      hiverRequest(`/conversations/${conv.id}/assign`, {
        method: "POST",
        body: JSON.stringify({ user_id: rule.assignee })
      }),
      hiverRequest(`/conversations/${conv.id}/tags`, {
        method: "POST",
        body: JSON.stringify({ tag_id: tagIds[rule.tag] })
      }),
      hiverRequest(`/conversations/${conv.id}/notes`, {
        method: "POST",
        body: JSON.stringify({
          body: `Auto-assigned: subject matched keyword "${rule.keyword}".`,
          type: "internal"
        })
      })
    ]);
 
    console.log(`Assigned ${conv.id} (${conv.subject}) → ${rule.assignee}`);
  }
}
 
autoAssign("mb_01HXYZ");

This pattern runs well as a cron job or a serverless function triggered on a schedule. For lower latency, replace the polling loop with a webhook on conversation.created and run the same assignment logic on each event.

API reference summary#

EndpointMethodWhat it does
/mailboxesGETList all mailboxes you have access to
/conversationsGETList conversations (filter by mailbox, status, tag)
/conversations/:idGETFetch a single conversation
/conversations/:id/assignPOSTAssign to a user
/conversations/:id/statusPUTUpdate status (open / pending / closed)
/conversations/:id/notesPOSTAdd an internal note
/conversations/:id/tagsPOSTApply a tag
/conversations/:id/tags/:tagIdDELETERemove a tag
/usersGETList agents on your account
/tagsGETList all tags
/tagsPOSTCreate a new tag

Error handling#

The Hiver API uses standard HTTP status codes:

CodeMeaningWhat to do
200Success
400Bad requestCheck request body shape and required fields
401UnauthorizedAPI key missing, wrong, or expired
403ForbiddenKey doesn't have access to this mailbox or resource
404Not foundConversation or resource ID doesn't exist
429Rate limitedBack off — respect the Retry-After header
500Server errorRetry once with exponential backoff

A minimal retry wrapper for 429 and 5xx:

// Node.js — retry with backoff
async function hiverRequestWithRetry(path, options = {}, retries = 3) {
  for (let attempt = 0; attempt < retries; attempt++) {
    const res = await fetch(`https://api.hiverhq.com/api/v1${path}`, {
      ...options,
      headers: {
        "Authorization": `Bearer ${process.env.HIVER_API_KEY}`,
        "Content-Type": "application/json",
        ...(options.headers ?? {})
      }
    });
 
    if (res.status === 429) {
      const retryAfter = parseInt(res.headers.get("Retry-After") ?? "5", 10);
      await new Promise(r => setTimeout(r, retryAfter * 1000));
      continue;
    }
 
    if (res.status >= 500 && attempt < retries - 1) {
      await new Promise(r => setTimeout(r, 1000 * 2 ** attempt));
      continue;
    }
 
    if (!res.ok) throw new Error(`Hiver API ${res.status}`);
    return res.json();
  }
}

What you can build#

Pros

    Cons

      Share this post

      Frequently asked questions

      What is the Hiver API?
      The Hiver API is a REST API that lets you programmatically interact with your Hiver helpdesk — create and update conversations, assign emails to agents, add internal notes, manage tags, and pull analytics data. It is useful for integrating Hiver into custom dashboards, CRMs, automation scripts, and internal tools.
      How do I get a Hiver API key?
      Go to your Hiver dashboard → Settings → API. Generate an API key there. API keys are scoped to the user who created them, so the key has the same mailbox access that user has. Store it as an environment variable and never commit it to source code.
      Is the Hiver API free to use?
      API access is included on the Pro plan and above. The free and Lite plans do not include API access. If you're on a lower plan and need API access, you'll need to upgrade.
      What authentication does the Hiver API use?
      The Hiver API uses Bearer token authentication. You pass your API key in the Authorization header as `Bearer YOUR_API_KEY`. All requests must be made over HTTPS.
      Can I use webhooks with Hiver?
      Yes. Hiver supports outgoing webhooks that fire on events like a new conversation arriving, an assignment changing, a conversation being closed, or a note being added. You configure the webhook URL in Settings → Integrations → Webhooks. The payload is JSON and includes the full conversation object.
      What can I build with the Hiver API?
      Common use cases include syncing Hiver conversations to a CRM (Salesforce, HubSpot), auto-assigning tickets based on external rules, pulling SLA and response-time metrics into a custom dashboard, triggering Slack or Teams notifications on new conversations, and building internal bots that auto-reply to common questions by adding notes or draft responses.
      Does Hiver have rate limits?
      Yes. The Hiver API is rate-limited to 100 requests per minute per API key. Exceeding the limit returns a 429 response with a Retry-After header. For bulk operations, implement a queue with a delay between requests rather than firing them all at once.
      Data & Analytics

      Google Flights API Key: How to Get One in 2026

      There is no official Google Flights API key — here is what to get instead, with step-by-step signup instructions for the APIs developers actually use.

      4 min read