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_code or refresh_token, form-encoded, with client_id and client_secret (or HTTP Basic).
  • POST /oauth/revoke: ends the connection behind a token.
Connecting, with the SDK
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 address
  • conversations.readSee the organization's conversations that the app started or linked, and who is in them
  • conversations.writeStart conversations for the organization, add people to them and link them to its records
  • messages.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.

Starting a conversation for a record
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 …/contacts and DELETE …/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.

Headers
webhook-id: evt_…
webhook-timestamp: 1767225600
webhook-signature: v1,<base64 HMAC-SHA256 of "<id>.<timestamp>.<body>">
Receiving, with the SDK
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 conversation
  • conversation.status_changedIt became open, pending or resolved
  • message.createdA new message
  • message.updatedA message was edited
  • message.deletedA message was deleted
  • member.addedA contact joined
  • member.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-sdk

Versioning

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.