Hanzo

Integrations

Package integrations is how your org connects third-party accounts like Slack, and revokes them.

Package integrations is how your org connects third-party accounts like Slack, and revokes them.

Base URLhttps://api.hanzo.ai
Operations32
AuthAuthorization: Bearer $HANZO_API_KEY

integrations

GET /v1/integrations/{provider}/callback

OAuth return for any connector

The single address every connector's OAuth flow returns to. It exchanges the authorization the provider granted, records the connection, and ALWAYS redirects the browser back to the console — on success and on every labeled failure alike, so a user never lands on a raw JSON dead end.

It is public and carries no principal, so the org is taken ONLY from the signed state minted when the flow began; no header is trusted here. That state is single-use and is burned BEFORE the exchange, so one authorization is one attempt and a replayed return fails instead of exchanging twice.

Tokens are sealed into the org's KMS namespace BEFORE the connection row is written, so a failure of the secret store leaves no half-connected integration advertising a credential that was never stored. Token values never appear in the redirect, in a log line or in an error.

One generalization is worth knowing: a GitHub App installation returns an installation identifier instead of an OAuth code, and it is accepted in the code's place so the App model needs no second address.

ParameterInTypeRequiredDescription
providerpathstringyes

POST /v1/integrations/{provider}/connect

Acquires the org's credential for one provider. It has TWO paths and the REQUEST picks which: a "token" key in the body seals that credential directly (verify-before-store), and its absence begins the 3-legged OAuth flow — minting a single-use nonce plus an HMAC-signed state that binds this org to this provider, and answering with the provider's authorize URL for the caller to redirect to.

Fail-closed order, unchanged: no principal → 403; unknown provider → 404; an AdminOnly connector without the caller's own-org admin bit → 403; not configured → 503; KMS not ready → 503 (the flow WILL need to seal a token, so refuse now rather than dead-end at the callback).

ParameterInTypeRequiredDescription
providerpathstringyesProvider is the connector's registry id, from the :provider path segment.

Request bodyapplication/json (required)

FieldTypeRequiredDescription
accountIdstringAccountID is the provider account the credential should be scoped to, for the providers whose Verify needs one…
providerstringProvider is the connector's registry id, from the :provider path segment.
tokenstringToken is the customer's provider credential.

POST /v1/integrations/{provider}/disconnect

Revokes (best-effort) and forgets an org's connection: it deletes every custodied KMS secret and the connection row. Idempotent — disconnecting a provider that was never connected still returns {disconnected:true}. Symmetric with connect: an AdminOnly connector needs the caller's own-org admin bit.

ParameterInTypeRequiredDescription
providerpathstringyesProvider is the registry id of the connector — "slack", "github", "cloudflare".

POST /v1/integrations/{provider}/verify

Re-checks a CONNECTED apikey connector's stored credential against the provider, live (hanzo connector verify). Org-scoped (any member may check status); the credential is read from KMS, verified, and NEVER returned or logged. A verification failure is reported as {active:false}, not an error — the console/ CLI renders it. Only apikey providers support verify (OAuth tokens are checked at use, not re-verified here).

ParameterInTypeRequiredDescription
providerpathstringyesProvider is the registry id of the connector — "slack", "github", "cloudflare".

GET /v1/integrations/{provider}

Returns ONE provider with this org's connection status — the same view list carries, for a single id. An unknown id is 404, and so is a user-plane provider: the org surface never resolves one.

ParameterInTypeRequiredDescription
providerpathstringyesProvider is the registry id of the connector — "slack", "github", "cloudflare".

POST /v1/integrations/discord/interactions

Discord interactions endpoint

The Interactions Endpoint URL for the Discord app. It answers Discord's PING with a PONG, and handles the /hanzo slash command by acknowledging with a deferred ephemeral reply and editing that reply with the answer once the agent has run. Any other interaction is acknowledged and ignored.

Requests are verified by ED25519 SIGNATURE over the timestamp and body against the app's public key — not by HMAC, unlike the Slack webhooks. Interactions work over plain HTTP, so no gateway connection and no message-content intent is involved.

Discord does not retry, so this is the one bridge where being at capacity is shown to the user as an ephemeral ask-to-run-it-again rather than answered as a retriable failure — nothing is recorded either way, so the next attempt is clean.

The caller here is the PLATFORM, not a Hanzo tenant, so there is no bearer and no principal. The signature check IS the authentication, and it fails closed. The tenant is never read from the payload either: it is resolved from the verified platform identifier through the connection map, so an event from a workspace nobody connected does nothing. Refusals are written with their own status rather than being flattened to a 500, so a rejected signature reads as 401 and a malformed body as 400.

GET /v1/integrations/discord/link/callback

Complete the Discord account link

The final leg: it binds the verified Discord user to the Hanzo account that just signed in, and answers a short confirmation page telling them to return to Discord. The Hanzo credential is sealed into the connected org's KMS namespace rather than stored beside the link.

This is one leg of a three-leg flow, and the legs are not interchangeable: a browser is expected to arrive here only from the leg before it. The link URL's state proves the prompt was server-minted and carries the CHAT it started from — it is provenance only, and it never decides which account gets linked. The account identity always comes from the platform's own verified sign-in and a host-bound cookie, so forwarding a link to someone else cannot bind their account, and a session lifted into another browser is refused rather than completed. Each link is single-use, and a deployment without linking configured answers 503.

GET /v1/integrations/discord/link/discord

Discord sign-in return leg

Where Discord returns the user after the identify consent. It resolves the verified Discord user, confirms the server is connected to an org, and hands the browser to the Hanzo sign-in that completes the link.

This is one leg of a three-leg flow, and the legs are not interchangeable: a browser is expected to arrive here only from the leg before it. The link URL's state proves the prompt was server-minted and carries the CHAT it started from — it is provenance only, and it never decides which account gets linked. The account identity always comes from the platform's own verified sign-in and a host-bound cookie, so forwarding a link to someone else cannot bind their account, and a session lifted into another browser is refused rather than completed. Each link is single-use, and a deployment without linking configured answers 503.

Begin linking a Hanzo account from Discord

The entry point behind the connect prompt Hanzo shows in a Discord server. It starts a link session and redirects to Discord's OAuth identify consent — the narrowest scope that establishes which Discord user is asking, and nothing more.

This is one leg of a three-leg flow, and the legs are not interchangeable: a browser is expected to arrive here only from the leg before it. The link URL's state proves the prompt was server-minted and carries the CHAT it started from — it is provenance only, and it never decides which account gets linked. The account identity always comes from the platform's own verified sign-in and a host-bound cookie, so forwarding a link to someone else cannot bind their account, and a session lifted into another browser is refused rather than completed. Each link is single-use, and a deployment without linking configured answers 503.

POST /v1/integrations/github/issues/backfill

Seeds the native tracker with the EXISTING issues across the org's granted repos (default state=open); the webhook keeps them live thereafter. Org-scoped by the validated principal — a caller only ever backfills its OWN org. Synchronous + bounded (a total time budget and an issue cap) so it returns the counts directly; idempotent by ExtRef, so a re-run continues where a truncated pass left off and never duplicates.

Request bodyapplication/json (required)

FieldTypeRequiredDescription
statestringState is the GitHub issue state to walk: "open" (the default), "closed" or "all".

POST /v1/integrations/github/repos/{repo}/pages/builds

Requests a Pages rebuild and returns the queued build's status. The build is queued AT GITHUB, not completed here, so the answer is 202 Accepted and its status is the one GitHub reported at queue time. 404 when the repository has no Pages site, or when the org's installation was not granted it.

ParameterInTypeRequiredDescription
repopathstringyesRepo is the repository's short name within the org's installation, with no owner prefix (the owner is server-derived…

GET /v1/integrations/github/repos/{repo}/pages

Returns the repo's Pages status, live URL, custom domain and build source. The repo is resolved against the org installation's GRANTED set, so a caller can never address a repo the App was not granted; 404 when the repo has no Pages site.

ParameterInTypeRequiredDescription
repopathstringyesRepo is the repository's short name within the org's installation, with no owner prefix (the owner is server-derived…

POST /v1/integrations/github/repos/{repo}/pages

Creates the repo's Pages site and answers 201 Created with it. With buildType "workflow" the site builds via GitHub Actions; otherwise it builds from a branch source, defaulting to the repo's own default branch when none is given. Only "/" and "/docs" are legal source paths (GitHub's rule).

ParameterInTypeRequiredDescription
repopathstringyesRepo is the repository, from the :repo path segment.

Request bodyapplication/json (required)

FieldTypeRequiredDescription
branchstringBranch is the legacy source branch; empty defaults to the repo's own default branch.
buildTypestringBuildType selects the builder: "workflow" builds via GitHub Actions, anything else builds from the branch source above.
pathstringPath is the source directory within the branch: "/" (the default) or "/docs".
repostringRepo is the repository, from the :repo path segment.

PUT /v1/integrations/github/repos/{repo}/pages

Sets or clears the custom domain (cname) and updates HTTPS enforcement, build type, or source. ONLY the provided fields are sent to GitHub, so an update never resets a setting the caller did not mention.

ParameterInTypeRequiredDescription
repopathstringyesRepo is the repository, from the :repo path segment.

Request bodyapplication/json (required)

FieldTypeRequiredDescription
branchstringBranch switches the legacy source branch.
buildTypestringBuildType switches the builder: "legacy" or "workflow".
cnamestringCNAME is the custom domain.
httpsEnforcedbooleanHTTPSEnforced toggles GitHub's enforce-HTTPS bit.
pathstringPath is the source directory to pair with Branch: "/" (the default) or "/docs".
repostringRepo is the repository, from the :repo path segment.

DELETE /v1/integrations/github/repos/{repo}/pages

Deletes the repo's Pages site. 404 when there is none, so a caller can tell "turned it off" from "there was nothing on".

ParameterInTypeRequiredDescription
repopathstringyesRepo is the repository's short name within the org's installation, with no owner prefix (the owner is server-derived…

POST /v1/integrations/github/repos/import

Imports the selected (or all) granted repos into git.hanzo.ai. The selection is intersected with the installation's GRANTED set, so a client can never import a repo the App was not granted (org isolation + a grant check). The import runs in a bounded background worker (don't block the request), so the answer is 202 Accepted; poll GET /v1/integrations/github/repos for the per-repo status to flip to imported.

Request bodyapplication/json (required)

FieldTypeRequiredDescription
allbooleanAll imports every repository the installation grants, instead of naming them.
reposstring[]Repos names the repositories to import, either owner-qualified ("hanzo-apps/ai") or as a bare name ("ai"); a trailing…

GET /v1/integrations/github/repos

Lists the org's granted GitHub repositories, each annotated with its native import + sync status from the git object plane. Org-authed: the org comes from the validated principal, and the granted set is bounded to THAT org's installation token — an org can never enumerate another org's repos. The console polls it to watch an import flip a repo to imported.

POST /v1/integrations/slack/commands

Slack slash command webhook

The address Slack posts a slash command to, form-encoded. It acknowledges inside Slack's three-second budget and posts the answer afterwards to the command's own response URL, which is why the immediate reply is empty.

The body is verified against the same app signing secret as the events webhook, and a repeat of the same command invocation is absorbed rather than answered twice.

The caller here is the PLATFORM, not a Hanzo tenant, so there is no bearer and no principal. The signature check IS the authentication, and it fails closed. The tenant is never read from the payload either: it is resolved from the verified platform identifier through the connection map, so an event from a workspace nobody connected does nothing. Refusals are written with their own status rather than being flattened to a 500, so a rejected signature reads as 401 and a malformed body as 400.

The answer is acknowledged immediately and the work happens afterwards, because every one of these platforms times out a slow webhook. Duplicate deliveries are absorbed durably, so a platform retry of an event that already ran never runs it a second time or bills for it twice. When the agent pool is full nothing at all is recorded and the delivery is refused as retriable, so the message is re-delivered later rather than being lost or half-processed.

POST /v1/integrations/slack/events

Slack Events API webhook

The address a Slack app posts workspace events to. It answers Slack's url_verification handshake with the challenge, and routes an @mention or a direct message to an agent turn that replies in the same thread. A prompt beginning with code: is routed to the coding flow instead, which runs under its own pool.

The raw body and its timestamp are verified against the app's signing secret before anything is read from them. Hanzo's own bot messages are dropped, so a reply cannot trigger another reply.

The caller here is the PLATFORM, not a Hanzo tenant, so there is no bearer and no principal. The signature check IS the authentication, and it fails closed. The tenant is never read from the payload either: it is resolved from the verified platform identifier through the connection map, so an event from a workspace nobody connected does nothing. Refusals are written with their own status rather than being flattened to a 500, so a rejected signature reads as 401 and a malformed body as 400.

The answer is acknowledged immediately and the work happens afterwards, because every one of these platforms times out a slow webhook. Duplicate deliveries are absorbed durably, so a platform retry of an event that already ran never runs it a second time or bills for it twice. When the agent pool is full nothing at all is recorded and the delivery is refused as retriable, so the message is re-delivered later rather than being lost or half-processed.

GET /v1/integrations/slack/link/callback

Complete the Slack account link

The final leg: the user has proved both who they are in Slack and who they are in Hanzo, and this binds the two. It answers a short confirmation page telling them to return to Slack.

The Hanzo credential obtained here is sealed into the connected workspace's own KMS namespace; it is never written to a database column and never logged. A deployment whose secret store is unavailable refuses the link rather than completing it without custody of the credential.

This is one leg of a three-leg flow, and the legs are not interchangeable: a browser is expected to arrive here only from the leg before it. The link URL's state proves the prompt was server-minted and carries the CHAT it started from — it is provenance only, and it never decides which account gets linked. The account identity always comes from the platform's own verified sign-in and a host-bound cookie, so forwarding a link to someone else cannot bind their account, and a session lifted into another browser is refused rather than completed. Each link is single-use, and a deployment without linking configured answers 503.

GET /v1/integrations/slack/link/slack

Slack sign-in return leg

Where Slack returns the user after they sign in. It establishes the verified Slack workspace and user, confirms that workspace is connected to an org, and hands the browser on to the Hanzo sign-in that completes the link.

The verified pair is carried onward in a host-bound cookie rather than in the URL, so the identity being linked cannot be edited in transit.

This is one leg of a three-leg flow, and the legs are not interchangeable: a browser is expected to arrive here only from the leg before it. The link URL's state proves the prompt was server-minted and carries the CHAT it started from — it is provenance only, and it never decides which account gets linked. The account identity always comes from the platform's own verified sign-in and a host-bound cookie, so forwarding a link to someone else cannot bind their account, and a session lifted into another browser is refused rather than completed. Each link is single-use, and a deployment without linking configured answers 503.

Begin linking a Hanzo account from Slack

The entry point behind the connect prompt Hanzo posts in Slack. It starts a link session in the browser and redirects to Slack's own sign-in, which is what proves which Slack user is asking.

This is one leg of a three-leg flow, and the legs are not interchangeable: a browser is expected to arrive here only from the leg before it. The link URL's state proves the prompt was server-minted and carries the CHAT it started from — it is provenance only, and it never decides which account gets linked. The account identity always comes from the platform's own verified sign-in and a host-bound cookie, so forwarding a link to someone else cannot bind their account, and a session lifted into another browser is refused rather than completed. Each link is single-use, and a deployment without linking configured answers 503.

POST /v1/integrations/teams/events

Microsoft Teams Bot Framework webhook

The messaging endpoint for the Teams bot. A message activity is routed to an agent turn and answered proactively through the Bot Connector; anything that is not a message with text is acknowledged and ignored.

Authentication is the Bot Framework's RS256 JWT, verified against its published keys and bound BOTH to this deployment's app id and to the activity's own service URL. The service-URL binding is the part that matters: without it a token valid for one activity could point the outbound reply somewhere else.

The caller here is the PLATFORM, not a Hanzo tenant, so there is no bearer and no principal. The signature check IS the authentication, and it fails closed. The tenant is never read from the payload either: it is resolved from the verified platform identifier through the connection map, so an event from a workspace nobody connected does nothing. Refusals are written with their own status rather than being flattened to a 500, so a rejected signature reads as 401 and a malformed body as 400.

The answer is acknowledged immediately and the work happens afterwards, because every one of these platforms times out a slow webhook. Duplicate deliveries are absorbed durably, so a platform retry of an event that already ran never runs it a second time or bills for it twice. When the agent pool is full nothing at all is recorded and the delivery is refused as retriable, so the message is re-delivered later rather than being lost or half-processed.

GET /v1/integrations/teams/link/aad

Microsoft sign-in return leg

Where Microsoft returns the user after sign-in. It resolves the verified directory identity and then re-checks the tenant: the signed-in user's tenant must equal the tenant of the chat the link started from, so a valid Microsoft sign-in from a different organization is refused here rather than accepted.

This is the leg Teams has and the other platforms do not, which is why the Teams flow has an extra address.

This is one leg of a three-leg flow, and the legs are not interchangeable: a browser is expected to arrive here only from the leg before it. The link URL's state proves the prompt was server-minted and carries the CHAT it started from — it is provenance only, and it never decides which account gets linked. The account identity always comes from the platform's own verified sign-in and a host-bound cookie, so forwarding a link to someone else cannot bind their account, and a session lifted into another browser is refused rather than completed. Each link is single-use, and a deployment without linking configured answers 503.

GET /v1/integrations/teams/link/callback

Complete the Teams account link

The final leg: it binds the verified directory identity to the Hanzo account that just signed in, and answers a short confirmation page telling them to return to Teams. The Hanzo credential is sealed into the connected org's KMS namespace.

This is one leg of a three-leg flow, and the legs are not interchangeable: a browser is expected to arrive here only from the leg before it. The link URL's state proves the prompt was server-minted and carries the CHAT it started from — it is provenance only, and it never decides which account gets linked. The account identity always comes from the platform's own verified sign-in and a host-bound cookie, so forwarding a link to someone else cannot bind their account, and a session lifted into another browser is refused rather than completed. Each link is single-use, and a deployment without linking configured answers 503.

Begin linking a Hanzo account from Teams

The entry point behind the connect prompt Hanzo shows in Teams. It starts a link session and redirects to Microsoft sign-in addressed to the CHAT'S OWN tenant, not the common endpoint, so only a member of that tenant can complete it.

This is one leg of a three-leg flow, and the legs are not interchangeable: a browser is expected to arrive here only from the leg before it. The link URL's state proves the prompt was server-minted and carries the CHAT it started from — it is provenance only, and it never decides which account gets linked. The account identity always comes from the platform's own verified sign-in and a host-bound cookie, so forwarding a link to someone else cannot bind their account, and a session lifted into another browser is refused rather than completed. Each link is single-use, and a deployment without linking configured answers 503.

POST /v1/integrations/telegram/connect

Mints a short, single-use deep-link code bound to the caller's org and returns the t.me link the console navigates to. Org-authed: a caller with no validated principal is 403 (same gate as the framework connect). The code is stored as an oauth_nonce (org,telegram); the webhook's /start handler claims it to bind chat→org. It is short (128-bit hex) so it fits Telegram's 64-char start payload limit.

GET /v1/integrations/telegram/link/auth

Telegram Login Widget return leg

Where Telegram's Login Widget sends the user with its signed authentication data. That data is verified against the bot token — this is the identity source, and it is the widget's signature rather than a code exchange — and the chat is confirmed to be bound to an org before the browser is handed to the Hanzo sign-in.

Widget data is only accepted while it is fresh, so a captured sign-in blob cannot be replayed later even though its signature stays valid.

This is one leg of a three-leg flow, and the legs are not interchangeable: a browser is expected to arrive here only from the leg before it. The link URL's state proves the prompt was server-minted and carries the CHAT it started from — it is provenance only, and it never decides which account gets linked. The account identity always comes from the platform's own verified sign-in and a host-bound cookie, so forwarding a link to someone else cannot bind their account, and a session lifted into another browser is refused rather than completed. Each link is single-use, and a deployment without linking configured answers 503.

GET /v1/integrations/telegram/link/callback

Complete the Telegram account link

The final leg: it binds the verified Telegram user to the Hanzo account that just signed in, and answers a short confirmation page telling them to return to Telegram. The Hanzo credential is sealed into the connected org's KMS namespace.

This is one leg of a three-leg flow, and the legs are not interchangeable: a browser is expected to arrive here only from the leg before it. The link URL's state proves the prompt was server-minted and carries the CHAT it started from — it is provenance only, and it never decides which account gets linked. The account identity always comes from the platform's own verified sign-in and a host-bound cookie, so forwarding a link to someone else cannot bind their account, and a session lifted into another browser is refused rather than completed. Each link is single-use, and a deployment without linking configured answers 503.

Begin linking a Hanzo account from Telegram

The entry point behind the connect prompt Hanzo sends in Telegram. Unlike the other platforms it answers an HTML PAGE rather than a redirect: Telegram has no OAuth flow, so the page hosts Telegram's Login Widget, and the browser is sent onward only after the user signs in through it.

The widget only appears on the domain registered for the bot, so a deployment whose bot domain is unset renders a page with nothing on it.

This is one leg of a three-leg flow, and the legs are not interchangeable: a browser is expected to arrive here only from the leg before it. The link URL's state proves the prompt was server-minted and carries the CHAT it started from — it is provenance only, and it never decides which account gets linked. The account identity always comes from the platform's own verified sign-in and a host-bound cookie, so forwarding a link to someone else cannot bind their account, and a session lifted into another browser is refused rather than completed. Each link is single-use, and a deployment without linking configured answers 503.

POST /v1/integrations/telegram/webhook

Telegram Bot API webhook

The update webhook for the Telegram bot. It does two jobs: /start <code> or /connect <code> binds the chat it was sent from to an org, idempotently; anything else is treated as a possible agent trigger.

What counts as a trigger differs by chat type, and it is easy to get wrong: in a private chat every message is a trigger, while in a group the message must mention the bot or use the /hanzo command. Non-triggers and non-message updates are acknowledged and dropped.

Authentication is the secret token Telegram echoes on every update, compared in constant time. A message in a chat that has never been bound is dropped, which is why the bind command exists.

The caller here is the PLATFORM, not a Hanzo tenant, so there is no bearer and no principal. The signature check IS the authentication, and it fails closed. The tenant is never read from the payload either: it is resolved from the verified platform identifier through the connection map, so an event from a workspace nobody connected does nothing. Refusals are written with their own status rather than being flattened to a 500, so a rejected signature reads as 401 and a malformed body as 400.

The answer is acknowledged immediately and the work happens afterwards, because every one of these platforms times out a slow webhook. Duplicate deliveries are absorbed durably, so a platform retry of an event that already ran never runs it a second time or bills for it twice. When the agent pool is full nothing at all is recorded and the delivery is refused as retriable, so the message is re-delivered later rather than being lost or half-processed.

GET /v1/integrations

Returns every registered integration provider together with THIS org's connection status for it — the catalog the console's Integrations page renders. Org-authed: a caller with no validated principal is 403, because the status is per-org and there is no org-less answer. User-plane providers (the /v1/connectors surface) are omitted; the two planes are disjoint.


All Hanzo APIs · Interactive reference

How is this guide?

On this page