API for apps
Connect your software, like a client portal or a CRM, to an organization's conversations in hiza.
Getting started
An admin of an organization registers the app in Business → Apps and gets its client id and client secret (and a webhook signing secret, with a webhook address). An app belongs to that organization and works only in it. It can then act in two ways:
- For a person: someone in the organization connects it on hiza's consent screen (OAuth). An admin's Allow also connects the app to the organization; an agent can only let it act within what it is connected with.
- On its own: once connected, an admin creates an API key. Its messages show the app's name.
Every call is under /api/v1 with Authorization: Bearer …. Answers are { data, meta, error }. The complete schema is the OpenAPI document.
Signing in apps
OAuth 2.1: the authorization code with PKCE (S256 only) and the client secret. Return addresses are matched exactly; use https (http only for localhost while you develop). Codes last a minute and work once; access tokens (hiza_at_…) an hour. The refresh token (hiza_rt_…) changes on every use: store the new one at once. Using an old one again ends the connection, because hiza takes it as stolen.
GET /oauth/authorize: the consent screen (client_id,redirect_uri,response_type=code,scope,state,code_challenge,code_challenge_method=S256).POST /oauth/token:grant_type=authorization_codeorrefresh_token, form-encoded, withclient_idandclient_secret(or HTTP Basic).POST /oauth/revoke: ends the connection behind a token.
import { authorizeUrl, createPkce, createState, exchangeCode } from "@nashyvan/hiza-sdk";
// 1. Send the person to hiza's consent screen.
const { verifier, challenge } = await createPkce();
const state = createState();
// Keep verifier and state in the person's session, then redirect to:
authorizeUrl({
baseUrl: process.env.HIZA_URL,
clientId: process.env.HIZA_CLIENT_ID,
redirectUri: process.env.HIZA_REDIRECT_URI,
scopes: ["conversations.read", "conversations.write", "messages.read"],
state,
codeChallenge: challenge,
});
// 2. On your return address: check state, then exchange the code.
const tokens = await exchangeCode({
baseUrl: process.env.HIZA_URL,
clientId: process.env.HIZA_CLIENT_ID,
clientSecret: process.env.HIZA_CLIENT_SECRET,
code, // from the query
redirectUri: process.env.HIZA_REDIRECT_URI,
codeVerifier: verifier,
});
// tokens.organization_id: the organization the app now works in.API keys (hiza_live_…) are shown once. Keep them, the client secret and the webhook secret on your server only. People see and end their connections in Settings → Connected apps; admins disconnect the app in Business → Apps, which stops its keys and tokens at once.
Scopes
profileSee your name, username and email addressconversations.readSee the organization's conversations that the app started or linked, and who is in themconversations.writeStart conversations for the organization, add people to them and link them to its recordsmessages.readRead messages in those conversations (and receive their text in webhooks)messages.writeSend messages in those conversations
An app sees only the conversations it started or linked to one of its records, and a person through an app never gets more than their role allows.
Conversations with contacts
An app starts a conversation of the organization for one of its records (an external reference: a type such as project and your id). The organization's people are in it through the inbox. Contacts are named by their Coredo Account id (sub): someone with a hiza account joins (or gets a request to accept, if their privacy settings ask for it); someone without one is invited by email when you give it, and joins when they first sign in with their Coredo Account.
import { Hiza } from "@nashyvan/hiza-sdk";
const hiza = new Hiza({ baseUrl: process.env.HIZA_URL, token: accessTokenOrApiKey });
const { conversation, created } = await hiza.conversations.create(orgId, {
title: "Website redesign",
contacts: [{ sub: client.ssoId, email: client.email }],
externalRef: { type: "project", id: project.id, url: projectUrl },
});
// Asking again for the same record returns the same conversation (created: false).
// conversation.links.contacts opens it for the client; links.agents for the team.POST /orgs/{id}/conversations:{ title, contacts?, externalRef }; 201 when new, 200 when the record already has one.GET /external-refs/{type}/{refId}/conversation: the conversation of a record, with its 20 newest messages.GET /orgs/{id}/conversations/{conversationId}/details: its people, who is still invited, links and status.POST …/contactsandDELETE …/contacts/{sub}: add and remove contacts.PUT …/external-refs: link another of the organization's conversations to a record;DELETE …/external-refs/{refId}unlinks it.
Messages
GET …/messages pages by seq: newest first before one, or oldest first after one, up to 100. POST …/messages sends plain text with a clientMessageId (a UUID: sending again with the same one does nothing new). An API key writes as the app; a token writes as the person. Photos and files are not part of this API.
Webhooks
With a webhook address (https), hiza tells the app what happens in the organization's conversations, signed as Standard Webhooks. Each event comes at least once: ignore an id you have already handled. Answer with a 2xx within 10 seconds; anything else is tried again after 5 s, 1 min, 5 min, 30 min, 2 h, 5 h, 8 h, 8 h (about a day). When nothing reaches the app for a day, its webhooks stop and the admin who connected it gets an email; they turn them back on in Business → Apps, where the delivery log is.
webhook-id: evt_…
webhook-timestamp: 1767225600
webhook-signature: v1,<base64 HMAC-SHA256 of "<id>.<timestamp>.<body>">import { verifyWebhook } from "@nashyvan/hiza-sdk";
export async function POST(request: Request) {
const raw = await request.text(); // the body exactly as received
const event = await verifyWebhook(process.env.HIZA_WEBHOOK_SECRET, request.headers, raw);
if (!event) return new Response(null, { status: 400 });
if (await seen(event.id)) return new Response(null, { status: 200 }); // at least once
await handle(event); // event.type, event.data.conversationId, …
return new Response(null, { status: 200 });
}Events carry ids and small facts. A message's text is included only for conversations the app started or linked, and only with messages.read.
conversation.createdA conversation of the organization started (by an app, a person, or a website visitor)conversation.assignedSomeone or a team was given the conversationconversation.status_changedIt became open, pending or resolvedmessage.createdA new messagemessage.updatedA message was editedmessage.deletedA message was deletedmember.addedA contact joinedmember.removedA contact left or was removed
Errors and limits
Errors are problem details in error (type, title, status). 401: the token or key is invalid, expired or revoked. 403 insufficient_scope: it was not granted that. 404: not there, or not the app's to see. 409: the app is not connected. 422: the request needs a change. 429: slow down: a token or key may make 300 calls a minute, and the RateLimit headers say how many are left. OAuth endpoints answer RFC 6749 errors (invalid_grant, invalid_client, …).
The SDK
@nashyvan/hiza-sdk is a typed client for Node 20+, edge runtimes and browsers: OAuth with PKCE, conversations, contacts, messages, links to records and webhook verification. It ships the OpenAPI document as @nashyvan/hiza-sdk/openapi.json.
npm install @nashyvan/hiza-sdkVersioning
v1 only grows: new endpoints, optional fields and new response properties. Nothing an app uses is removed or made required within v1; a change like that would be a v2, announced ahead. Handle unknown event types and fields by ignoring them.
Questions: see contact.