Changelog

What changed in each HookChat package, newest release first. Each package keeps its own changelog in the repository; this page is generated from them.

The links to GitHub on this page need an account with access while the repository is private. Every release note is on this page in full, so nothing here depends on them.

0.5.0 (2026-10-09)#

@hookchat/node#

Added

  • client.messages.getHold(hold_id) reads one hold (GET /v1/messages/holds/{hold_id}): its status (awaiting_verification, sending, sent, failed or expired), platform_message_id once sent and error (HoldFailure) once failed. verify_url is never in this answer. An unknown id, one more than 30 minutes old, or another workspace's is hold_not_found (404).
  • client.messages.waitForHold(hold_id, { timeout_ms }) reads the hold every second, then every two seconds, and resolves with it once sent. A failed hold rejects with the HookChatError its code carries (send_failed 502, human_agent_unavailable or rate_limited 409, conversation_not_found 404); an expired one rejects with hold_expired (409). timeout_ms defaults to 900 000 (15 minutes); past it, it rejects with the new HoldTimeoutError (code timeout, holdId and lastStatus set) and the hold goes on until it expires. An AbortSignal in the options ends the wait.
  • New Hold, HoldStatus, HoldFailure and WaitForHoldParams types.
  • hold_not_found (404), hold_expired (409) and verification_failed (422) join ErrorCode.
  • ConversationErasure.holds: how many held human-agent messages an erase removed, sent or not.

Changed

  • Breaking: client.messages.sendAsHumanAgent now resolves with a Hold, not a SendResult. Nothing is sent by the call: the API answers 202 and holds the message until the person who typed it opens hold.verify_url and passes a short check in the browser. HookChat then sends it and the usual message.sent event follows. The check raises the cost of sending by script; it does not prove a person. A hold waits 15 minutes, never past the end of the 7 day window, and then expires without sending. verify_url carries a one-time secret: hand it only to that person, and never log it. Code that read platform_message_id from the send now reads it from waitForHold. The send no longer rejects with send_failed; a refusal by Meta comes from waitForHold.

Full @hookchat/node changelog on GitHub

hookchat (Python)#

Added

  • client.messages.get_hold(hold_id) reads one held human-agent message (GET /v1/messages/holds/{hold_id}) as a Hold: its status (awaiting_verification, sending, sent, failed or expired), platform_message_id once sent and error (HoldFailure) once failed. A read never carries verify_url. An unknown, ended or other workspace's hold raises NotFoundError with hold_not_found.
  • client.messages.wait_for_hold(hold_id, timeout=...) reads the hold every second, then every two seconds, and returns it once sent. A failed hold raises the error its code names (HumanAgentUnavailableError, RateLimitError, SendFailedError, NotFoundError or ServerError). An expired hold raises ConflictError with hold_expired: nobody passed the check, and nothing was sent. Past timeout seconds (default 15 minutes) it raises the new HoldTimeoutError (an APITimeoutError, with hold_id and last_status); the hold goes on, and get_hold reads it until it expires.
  • New Hold, HoldState, HoldFailure and HoldTimeoutError exports. Both HookChat and AsyncHookChat have the new methods.
  • The new error codes: hold_not_found (404) raises NotFoundError, hold_expired (409) raises ConflictError and verification_failed (422) raises ValidationError.
  • ConversationErasure.holds: how many held human-agent messages an erase removed, sent or not.

Changed

  • Breaking: client.messages.send_as_human_agent now returns a Hold, not a SendResult. Nothing is sent by the call: the API answers 202 and holds the message until the person who typed it opens hold.verify_url and passes a short check in the browser. HookChat then sends it. The check raises the cost of sending by script; it is not proof of a person. A hold waits 15 minutes, never past the end of the 7 day window, and then expires without sending. verify_url carries a one-time secret: hand it only to that person, and never log it; repr(hold) hides it. Code that read platform_message_id from the send now reads it from wait_for_hold. The send no longer raises SendFailedError; a refusal by Meta comes from wait_for_hold.

Full hookchat (Python) changelog on GitHub

hookchat-go#

Added

  • Hold (ID, Status, ConversationID, CreatedAt, ExpiresAt, VerifyURL, PlatformMessageID, Error), HoldStatus with HoldAwaitingVerification, HoldSending, HoldSent, HoldFailed and HoldExpired, and HoldFailure (Code, Message, Detail).
  • client.Messages.GetHold(ctx, holdID) reads one hold (GET /v1/messages/holds/{hold_id}). VerifyURL is never set on a read. Name an operator key's workspace with ForTenant.
  • client.Messages.WaitForHold(ctx, holdID, WaitForHoldParams{Timeout}) reads the hold every second, then every two seconds, until the message is sent, and returns it. A failed hold returns a *Error with the hold's code, message and detail and the status the API gives that code; an expired one returns hold_expired (409). Past Timeout (default 15 minutes) it returns *HoldTimeoutError (with HoldID, the last Status and Waited; IsTimeout reports it) and the hold goes on.
  • CodeHoldNotFound (404, and IsNotFound reports it), CodeHoldExpired (409) and CodeVerificationFailed (422).
  • ConversationErasure.Holds: Conversations.Erase also counts the held human-agent messages it removed, sent or not.

Changed

  • Breaking: Messages.SendAsHumanAgent no longer sends. The API now answers 202 with a hold, and the method returns a *Hold in place of a *SendResult. The person who typed the message opens Hold.VerifyURL and passes a check in the browser, and HookChat then sends it. The check raises the cost of sending by script; it is not proof that a person is there. VerifyURL carries a one-time secret: hand it only to that person and never log it. A hold waits 15 minutes, or until the 7 day window ends if that is sooner. To keep the old flow, show VerifyURL and call WaitForHold, which returns the hold with PlatformMessageID once sent. The call no longer returns send_failed, because it sends nothing.

Full hookchat-go changelog on GitHub

hookchat CLI#

Added

  • messages human-agent --no-open prints the link without opening a browser.
  • messages human-agent --no-wait prints the hold (id, status, expires_at, verify_url) and exits 0 at once, without waiting for the send: for scripts and docs samples that must not block. --quiet prints only the link; --output json or yaml prints the one hold document. The browser rules are the same.
  • Exit codes for the new error codes: hold_not_found exits 4 like every other not found, hold_expired exits 8 (nobody passed the check in time, nothing was sent) and verification_failed exits 5. A send that fails after the check exits with the code of its error: send_failed 8, human_agent_unavailable 6, rate_limited 7, conversation_not_found 4.

Changed

  • Breaking: messages human-agent no longer sends at once. The gateway holds the message and answers with a verify_url; the person who typed the message opens it and passes a check in the browser, and HookChat then sends it. The check raises the cost of sending by script; it is not proof that a person is there. The command prints verify_url on stdout as soon as the hold is made, opens it in your browser when stdin and stdout are a terminal, then waits for the send and prints the platform message id as before. The link carries a one-time secret: give it only to the person who typed the message.
  • Breaking: on messages human-agent, --quiet prints the link, then the id, and --output json or yaml prints two documents: the hold as made, with verify_url, then the hold once sent (id, status, platform_message_id and the rest). It used to print one object with platform_message_id.
  • Breaking: on messages human-agent, --wait-timeout now covers the wait for the send as well as any upload, and defaults to 15m, as long as a hold waits. attachments upload and messages reply keep their 2m default. Past it the command exits 10 with a timeout error that names the hold, and the hold goes on until it expires.
  • The CLI now requires hookchat.dev/go v0.5.0.
  • conversations erase prints a holds count: the held human-agent messages it removed, sent or not.

Full hookchat CLI changelog on GitHub

0.4.0 (2026-10-08)#

@hookchat/node#

Added

  • ConversationMessage.attachments: a message's media as MessageAttachment[] (type, url), on conversations.get, messages.list and messages.get. It is always present and empty when the message has none. url is Meta's link and expires after a while; HookChat does not keep a copy.
  • DeliverySummary.body_expires_at: when the event body stops being kept, in epoch milliseconds. A delivery record is kept 90 days, but its event body only 7, so a replay works only within 7 days.
  • replay_expired (409) joins ErrorCode: a replay after body_expires_at is refused, because there is nothing left to send.
  • upload_not_found (404), attachment_too_large (413) and unsupported_media_type (415) join ErrorCode: the errors of the new upload routes, which send a file from your computer.
  • client.attachments.upload({ conversation_id, file }) sends a file from your computer in one call. file is a path, a Buffer or Uint8Array, or a Blob or File; the name and content type come from it unless you set filename and content_type. It asks for an upload url, posts the file to it with no API key, then finishes, and returns the UploadedAttachment Meta now has. HookChat hands the file to Meta in the background, so upload waits: it reads the upload every second, then every two seconds, until Meta has it, on a timer that keeps the process alive, and an AbortSignal in the options ends the wait. wait_timeout_ms sets how long it waits (default 120 000); past it, upload rejects with UploadTimeoutError (code timeout, uploadId set) and the upload goes on. A file Meta refuses rejects with send_failed, as a refused send does. HookChat keeps the file only until Meta has it, at most a day.
  • client.attachments.get(upload_id) reads one upload as UploadStatus: its status (waiting, processing, ready or failed), the attachment (ReadyAttachment: id and type) once ready and the error (UploadFailure) once failed. Use it after an UploadTimeoutError, or to wait in your own way.
  • A send names an uploaded file by id: attachments: [{ type: a.type, attachment_id: a.id }]. SendAttachment is now SendAttachmentUrl | SendAttachmentRef, so a url and an id together do not compile. The Upload and UploadedAttachment types mirror the API.
  • client.conversations.erase(id) erases one person's data, for when your customer asks you to delete it: the conversation, its messages, profile, delivered copies, send records and waiting uploads. It returns { done, removed } with counts, never content. done is false while a big conversation goes on erasing in the background; call again to check. It cannot be undone. New ConversationErasure and ConversationErasureResult types.

Fixed

  • A send carries text or attachments, never both. The API used to take both, send only the first attachment and drop the text without a word; it now refuses the mix with invalid_request (400). The types match: ReplyParams and HumanAgentParams are a text send or a media send, so passing both is a compile error, and a plain JavaScript caller is refused locally (invalid_request, status 0) before any request.
  • Several attachments in one send must all be images, up to 10, and go to Meta as one message with one platform_message_id. Any other mix, or more than 10 images, is refused locally the same way and by the API. A send with no text and no attachments is now refused locally too.

Full @hookchat/node changelog on GitHub

hookchat (Python)#

Added

  • Message.attachments (also ConversationMessage): a message's media as a list of MessageAttachment (type, url), on conversations.get, messages.list and messages.get. It is always a list and empty when the message has none, an older gateway included. url is Meta's link and expires after a while; HookChat does not keep a copy.
  • Delivery.body_expires_at: when the event body stops being kept, in epoch milliseconds. A delivery record is kept 90 days, but its event body only 7, so a replay works only within 7 days.
  • ReplayExpiredError (409 replay_expired), a ConflictError: a replay after body_expires_at is refused, because there is nothing left to send.
  • The errors of the new upload routes, which send a file from your computer: upload_not_found (404) raises NotFoundError, and attachment_too_large (413) and unsupported_media_type (415) raise ValidationError.
  • client.attachments.upload(conversation_id, file) sends a file from your computer in one call, on HookChat and AsyncHookChat. file is a path, bytes, or a binary file object; the name and content type come from it unless you pass filename and content_type. It asks for an upload url, posts the file to it with no API key, then finishes, and returns the UploadedAttachment Meta now has. HookChat hands the file to Meta in the background, so upload waits: it reads the upload every second, then every two seconds, until Meta has it. wait_timeout sets how long it waits, in seconds (default 120); past it, upload raises UploadTimeoutError (an APITimeoutError, with upload_id) and the upload goes on. A file Meta refuses raises SendFailedError, as a refused send does. HookChat keeps the file only until Meta has it, at most a day.
  • client.attachments.get(upload_id) reads one upload as UploadStatus: its status (waiting, processing, ready or failed), the attachment (ReadyAttachment: id and type) once ready and the error (UploadFailure) once failed. Use it after an UploadTimeoutError, or to wait in your own way.
  • Attachment(type=a.type, attachment_id=a.id) sends an uploaded file by id; a dict with attachment_id works too. Attachment.url is now optional, and giving both or neither raises ValueError. New Upload and UploadedAttachment types mirror the API.
  • client.conversations.erase(conversation_id) erases one person's data, for when your customer asks you to delete it: the conversation, its messages, profile, delivered copies, send records and waiting uploads. It returns a ConversationErasureResult with done and the removed counts, never content, on HookChat and AsyncHookChat. done is False while a big conversation goes on erasing in the background; call again to check. It cannot be undone.

Fixed

  • A send carries text or attachments, never both. The API used to take both, send only the first attachment and drop the text without a word; it now refuses the mix with invalid_request (400), and reply and send_as_human_agent raise ValueError before any request.
  • Several attachments in one send must all be images, up to 10, and go to Meta as one message with one platform_message_id. Any other mix, or more than 10 images, raises ValueError before any request.

Full hookchat (Python) changelog on GitHub

hookchat-go#

Added

  • Message.Attachments: a message's media as []MessageAttachment (Type, URL), on Conversations.Get, Messages.List and Messages.Get. It is empty when the message has none. URL is Meta's link and expires after a while; HookChat does not keep a copy.
  • DeliverySummary.BodyExpiresAt and BodyExpiresTime(): when the event body stops being kept. A delivery record is kept 90 days, but its event body only 7, so a replay works only within 7 days.
  • CodeReplayExpired and IsReplayExpired: a replay after BodyExpiresAt is refused with 409 replay_expired, because there is nothing left to send.
  • CodeUploadNotFound (404, and IsNotFound reports it), CodeAttachmentTooLarge (413) and CodeUnsupportedMediaType (415): the errors of the new upload routes, which send a file from your computer.
  • client.Attachments.Upload(ctx, UploadParams{ConversationID, Path}) sends a file from your computer in one call; set File (an io.Reader, with Size when you know it) in place of Path for bytes you already hold. The name and content type come from the path unless you set Filename and ContentType. It asks for an upload URL, posts the file to it with no API key, then finishes, and returns the *UploadedAttachment Meta now has. HookChat hands the file to Meta in the background, so Upload waits: it reads the upload every second, then every two seconds, until Meta has it, and a cancelled ctx ends the wait. UploadParams.WaitTimeout sets how long it waits (default two minutes); past it, Upload returns *UploadTimeoutError (with UploadID; IsTimeout reports it) and the upload goes on. A file Meta refuses returns send_failed, as a refused send does. HookChat keeps the file only until Meta has it, at most a day.
  • client.Attachments.Get(ctx, uploadID) reads one upload as *UploadStatus: its Status (UploadWaiting, UploadProcessing, UploadReady or UploadFailed), the Attachment (*ReadyAttachment: ID and Type) once ready and the Error (*UploadFailure) once failed. Use it after an *UploadTimeoutError, or to wait in your own way.
  • Attachment.AttachmentID sends an uploaded file by id: Attachment{Type: a.Type, AttachmentID: a.ID}. URL is now left out of the JSON when empty. New Upload and UploadedAttachment types mirror the API.
  • client.Conversations.Erase(ctx, id) erases one person's data, for when your customer asks you to delete it: the conversation, its messages, profile, delivered copies, send records and waiting uploads. It returns a *ConversationErasureResult with Done and the Removed counts, never content. Done is false while a big conversation goes on erasing in the background; call again to check. It cannot be undone.

Fixed

  • A send carries Text or Attachments, never both. The API used to take both, send only the first attachment and drop the text without a word; it now refuses the mix with invalid_request (400), and Reply and SendAsHumanAgent refuse it locally (CodeInvalidRequest, Status 0) before any request.
  • Several attachments in one send must all be images, up to 10, and go to Meta as one message with one PlatformMessageID. Any other mix, or more than 10 images, is refused locally the same way.

Full hookchat-go changelog on GitHub

hookchat CLI#

Added

  • Messages show their attachments. messages list and conversations get add a MEDIA column with each attachment's type; messages get prints one attachment line per item with its type and Meta's link. JSON and YAML carry attachments as [{type, url}], [] when there is none. Meta's links expire; HookChat does not keep a copy.
  • replay_expired exits 8, like replay_conflict: a replay after the event body expired (7 days) is refused. JSON delivery rows carry body_expires_at.
  • The errors of the new upload routes have exit codes: upload_not_found exits 4 like every other not found, and attachment_too_large and unsupported_media_type exit 5 like invalid_request.
  • hookchat attachments upload --conversation C --file ./photo.jpg sends a file from your computer to Meta and prints the attachment: its id, type and size (--output json or yaml for all of it, --quiet for the id). --type, --filename and --content-type override what the file name implies. HookChat hands the file to Meta in the background, so the command waits until Meta has it, saying so on stderr (not with --quiet, json or yaml). --wait-timeout sets how long it waits (default 2m); past it the command exits 10 with a timeout error that names the upload, and the upload goes on. HookChat keeps the file only until Meta has it, at most a day.
  • hookchat attachments get <upload_id> shows where an upload has got to: waiting, processing, ready (with the attachment_id to send; --quiet prints only that) or failed (with the error). An upload more than a day old is upload_not_found (exit 4).
  • messages reply and messages human-agent take --attachment image=@./photo.jpg: the @ means a file on your computer, uploaded first, waited on until Meta has it (--wait-timeout), and then sent by its attachment id. A plain URL works as before. A send the gateway would refuse is refused before anything is uploaded, and a file that cannot be read is a usage error (exit 2).
  • hookchat conversations erase <id> erases one person's data, for when your customer asks you to delete it, and prints the counts of what went, never content. It cannot be undone, so it asks first at a terminal, and with no terminal it needs --yes: without it nothing is sent and it exits 2. done is false while a big conversation goes on erasing in the background; run it again later.

Changed

  • The CLI now requires hookchat.dev/go v0.4.0.

Fixed

  • messages reply and messages human-agent take --text or --attachment, not both. The API used to take both, send only the first attachment and drop the text without a word; it now refuses the mix, and the CLI stops it first as a usage error (exit 2).
  • Repeat --attachment for up to 10 images in one message, sent as one message with one platform_message_id. Any other mix, or more than 10 images, is a usage error before any request.

Full hookchat CLI changelog on GitHub

0.3.0 (2026-10-02)#

@hookchat/node#

Added

  • participant.updated joins EVENT_TYPES and WebhookEventType. verifyWebhook returns it as ParticipantEvent, whose data.participant is an EventParticipant (id, conversation_id, tenant, platform, account, name, username, picture). It is sent once per conversation, when the other party's profile is first stored, and can arrive before or after the message.received it follows.
  • Conversation.participant_picture: the other party's profile picture URL, or null. Meta's picture URLs expire.
  • Conversation.participant_profile, typed as ParticipantProfileStatus ('pending' | 'complete' | 'unavailable'): where the lookup of the other party's name, username and picture stands.
  • A message sent from the Instagram or Messenger app now reaches your endpoint as message.sent with data.message.direction 'echo', a few seconds after it is sent. A send through the API is still 'outbound'. raw is Meta's payload on an echo.

Changed

  • EventMessage.direction is typed as MessageDirection ('inbound' | 'outbound' | 'echo') instead of a bare string, so a check for an app send narrows in the editor.
  • conversations.list is ordered by the newest message in either direction, and lists a thread your team started from the app (last_inbound_at null, window closed, can_reply false).

Full @hookchat/node changelog on GitHub

hookchat (Python)#

Added

  • participant.updated joins EVENT_TYPES and EventTypeName, and parses as ParticipantEvent with .participant (EventParticipant: id, conversation_id, tenant, platform, account, name, username, picture). It is sent once per conversation, when the other party's profile is first stored, and can arrive before or after the message.received it follows.
  • Conversation.participant_picture: the other party's profile picture URL, or None. Meta's picture URLs expire.
  • Conversation.participant_profile, typed as ParticipantProfileStatus (pending, complete, unavailable): where the lookup of the other party's name, username and picture stands.
  • A message sent from the Instagram or Messenger app now reaches your endpoint as message.sent with event.message.direction "echo", a few seconds after it is sent. A send through the API is still "outbound". raw is Meta's payload on an echo.

Changed

  • EventMessage.direction is typed as Direction (inbound, outbound, echo) instead of a bare str.
  • conversations.list is ordered by the newest message in either direction, and lists a thread your team started from the app (last_inbound_at is None, the window is closed, can_reply is false).

Full hookchat (Python) changelog on GitHub

hookchat-go#

Added

  • EventTypeParticipantUpdated (participant.updated) joins EventTypes, and Event.Participant() decodes it into EventParticipant (ID, ConversationID, Tenant, Platform, Account, Name, Username, Picture). It is sent once per conversation, when the other party's profile is first stored, and can arrive before or after the message.received it follows.
  • Conversation.ParticipantPicture: the other party's profile picture URL, or nil. Meta's picture URLs expire.
  • Conversation.ParticipantProfile, typed as ParticipantProfileStatus with ParticipantProfilePending, ParticipantProfileComplete and ParticipantProfileUnavailable: where the lookup of the other party's name, username and picture stands.
  • A message sent from the Instagram or Messenger app now reaches your endpoint as message.sent with Direction DirectionEcho, a few seconds after it is sent. A send through the API is still DirectionOutbound. Raw is Meta's payload on an echo.

Changed

  • EventMessage.Direction is typed as MessageDirection, so it compares with DirectionInbound, DirectionOutbound and DirectionEcho. An untyped string constant still compares, a string variable needs a cast.
  • Conversations.List is ordered by the newest message in either direction, and lists a thread your team started from the app (LastInboundAt nil, window closed, CanReply false).

Full hookchat-go changelog on GitHub

hookchat CLI#

Added

  • webhooks create and webhooks update accept participant.updated in --events, sent once per conversation when the participant's name, username or picture is first stored.
  • conversations list has a PROFILE column, the conversation's participant_profile (pending, complete or unavailable).
  • conversations get shows participant_username, participant_picture and participant_profile.

Changed

  • messages list --direction help says what each value means: echo is a message sent from the Instagram or Messenger app, which a webhook delivers as message.sent like an outbound send.
  • conversations list help says the list is ordered by the newest message in either direction, and that a thread your team started from the app is listed with no last inbound time.
  • The CLI now requires hookchat.dev/go v0.3.0.

Full hookchat CLI changelog on GitHub

0.2.0 (2026-09-30)#

@hookchat/node#

Added

  • Conversation.participant_username: the other party's Instagram username, without the @, or null. Always null on Messenger. participant_handle is the display name.
  • accounts.disconnect(accountKey) unlinks an account (DELETE /v1/accounts/:account_key): Meta stops sending its webhooks, the token is deleted and account.disconnected fires. account_not_found (404) joins the error codes.
  • route_not_found (404) joins the error codes: a request to a method and path the API does not serve now answers in the envelope instead of a plain-text 404.
  • ConversationMessage.direction and the list filter are typed as MessageDirection (inbound, outbound, echo) instead of a bare string.
  • messages.list and messages.iterate accept direction: 'echo', the account's own sends mirrored back, native-app replies included.
  • events on CreateWebhookParams, UpdateWebhookParams and Endpoint is WebhookEventType[], so a typo in a filter fails in the editor rather than as a 400.
  • EVENT_TYPES, the list of every event type the gateway delivers, typed as WebhookEventType.
  • conversations.get accepts limit and cursor and returns next_cursor for older messages.
  • SendAttachmentType: the attachment type is 'image' | 'video' | 'audio' | 'file', Meta's kinds, so a typo fails in the editor rather than as a 400.
  • HookChatError.docsUrl: the troubleshooting entry for the error (#<code>, or #http for a reply with no envelope); TROUBLESHOOTING_URL is exported.
  • internal_error in ErrorCode: the gateway's answer to an unexpected failure on its side (HTTP 500, envelope and request id intact). Retry once, then quote the request id.
  • ApiKey.last_used: when a listed key last authenticated a call (to the nearest five minutes), or null if never.

Changed

  • ConversationMessage.sent_via is typed as SentVia (api_in_window, api_human_agent, native_app, unknown) rather than any string, matching the spec's list; SentVia is exported.
  • The User-Agent is now HookChat/<version> (Jiffi; node): the same brand form, with the SDK named, matching the Python and Go SDKs.
  • webhooks.rotate is now webhooks.rotateSecret, the name the Python (rotate_secret) and Go (RotateSecret) SDKs already use, so one docs page reads the same in every language. rotate stays as a deprecated alias for 1.x.

Fixed

  • verifyWebhook refuses an empty secret with HookChatSignatureError instead of verifying against the public empty key, matching the Go verifier. List params (cursor, limit, tenant) accept an explicit undefined, so the README's pagination loop compiles under exactOptionalPropertyTypes.
  • iterate treats an empty next_cursor as the end of the list, as it does null. It used to ask for the first page again, yield its items twice, then throw invalid_response.
  • The messages resource doc comment said a rate-limited send is retried; neither send is ever retried, as the README and the retry test say.
  • deliveries.summary and deliveries.replayAll leave status out of the query for an empty list instead of sending status=, as the Python and Go SDKs do.
  • verifyWebhook refuses a signed body that is not an event: JSON that is not an object, or an object with no string id and type, now raises HookChatSignatureError instead of coming back typed as an event with every field undefined. A signature timestamp at or below zero is malformed. Both match the Python and Go SDKs.
  • A rate-limited (429) reply, human-agent send, key or endpoint write, or realtime ticket is no longer retried automatically. Only webhooks.test, deliveries.replay and replayAll retry on a 429, matching the Python and Go SDKs; every other mutation now hands the rate_limited error back so the caller is never left with a message re-sent behind its back.
  • AccountEvent no longer declares account_key and reason on data.account: the gateway emits exactly external_id, platform, tenant and handle, as docs/events/schema.json and the Python and Go SDKs already say. A test now verifies a real gateway-built account event against the schema.

Full @hookchat/node changelog on GitHub

hookchat (Python)#

Added

  • Conversation.participant_username: the other party's Instagram username, without the @, or None. Always None on Messenger. participant_handle is the display name.
  • accounts.disconnect(account_key) unlinks an account (DELETE /v1/accounts/{account_key}) in both clients: Meta stops sending its webhooks, the token is deleted and account.disconnected fires. account_not_found (404) joins the codes, raised as NotFoundError.
  • route_not_found (404) joins the error codes, raised as NotFoundError: a request to a method and path the API does not serve now answers in the envelope.
  • Message.direction is typed as Direction (inbound, outbound, echo) instead of a bare str.
  • messages.list and messages.iterate accept direction="echo", the account's own sends mirrored back, native-app replies included.
  • EventTypeName, a Literal of the six event types; webhooks.create and webhooks.update take events: Sequence[EventTypeName], so a typo in a filter fails under mypy rather than as a 400.
  • conversations.get accepts limit and cursor in both clients; ConversationDetail.next_cursor resumes older messages.
  • SendAttachmentType: Attachment.type is a Literal of Meta's kinds (image, video, audio, file), so a typo fails under mypy rather than as a 400.
  • HookChatError.docs_url: the troubleshooting entry for the error (#<code>, or #http for a reply with no envelope); TROUBLESHOOTING_URL is exported.
  • internal_error maps to ServerError: the gateway's answer to an unexpected failure on its side (HTTP 500, envelope and request id intact).
  • ApiKey.last_used: when a listed key last authenticated a call (to the nearest five minutes), or None if never.

Changed

  • ConversationMessage.sent_via is typed as SentVia (api_in_window, api_human_agent, native_app, unknown) or None, the list the spec pins; SentVia is exported.
  • The User-Agent is now HookChat/<version> (Jiffi; python), the shape every HookChat SDK sends, so the gateway can tell the SDKs apart the same way.
  • Every public method and property carries a docstring (iterate, close, with_options, from_dict, to_dict, the client's base_url and tenant). No behaviour change.

Fixed

  • verify_webhook refuses an empty secret with SignatureVerificationError instead of verifying against the public empty key, matching the Go verifier.
  • messages.reply and messages.send_as_human_agent no longer take tenant=. The send routes have no ?tenant; the conversation id scopes the send, and the keyword was sent and ignored.
  • deliveries.summary takes status as FailedDeliveryStatus (dlq, failed, blocked, retrying) and deliveries.replay_all as ReplayableDeliveryStatus (dlq, failed, blocked, delivered), the sets the routes accept, so a status the gateway would refuse fails under mypy rather than as a 400. Both are exported.
  • webhooks.update takes status as active or paused, not any string.
  • typing-extensions is no longer listed as a dependency on Python 3.10. Nothing imported it, so pip install hookchat pulled a second package for no reason; the README's "one dependency" is true again.
  • verify_webhook refuses a signed object with no string id and type: it raises SignatureVerificationError instead of returning an Event with empty fields, matching the Node and Go SDKs.
  • iterate and aiterate follow an empty page that carries a cursor, as a filtered page can be, instead of stopping early; a cursor that does not advance raises HookChatError with code invalid_response instead of looping.
  • HookChatError.request_id is set on a bare 429 from the edge: the fallback header is now apigw-requestid, which the HTTP API sends, not x-amzn-requestid, which it never did.

Full hookchat (Python) changelog on GitHub

hookchat-go#

Added

  • Conversation.ParticipantUsername: the other party's Instagram username, without the @, or nil. Always nil on Messenger. ParticipantHandle is the display name.
  • Accounts.Disconnect(ctx, accountKey) unlinks an account (DELETE /v1/accounts/:account_key): Meta stops sending its webhooks, the token is deleted and account.disconnected fires. CodeAccountNotFound (account_not_found, 404) joins the codes, and IsNotFound reports it.
  • IsNotFound reports route_not_found too.
  • CodeRouteNotFound (route_not_found, 404): a request to a method and path the API does not serve now answers in the envelope.
  • DirectionEcho for the message feed filter: the account's own sends mirrored back, native-app replies included.
  • EventTypes, the list of every event type the gateway delivers, beside the existing constants.
  • ThreadPage(limit, cursor) paginates Conversations.Get; ConversationDetail.NextCursor resumes older messages.
  • AttachmentType with AttachmentImage, AttachmentVideo, AttachmentAudio and AttachmentFile; Attachment.Type uses it. An untyped string constant still converts, a string variable needs a cast.
  • Error.DocsURL(): the troubleshooting entry for the error (#<code>, or #http for a reply with no envelope); TroubleshootingURL is exported.
  • IsTimeout(err) and IsNetworkError(err): tell a request that ran out of time from one that never connected, the way the Node and Python SDKs classify timeout and network_error. Transport errors are still the wrapped net error, so errors.Is(err, context.DeadlineExceeded) keeps working.
  • CodeInternalError (internal_error): the gateway's answer to an unexpected failure on its side (HTTP 500, envelope and request id intact).
  • Key.LastUsed: when a listed key last authenticated a call (to the nearest five minutes), or nil if never.

Changed

  • The module path is now `hookchat.dev/go` (was github.com/jiffi-co/jiffi-message-gateway/packages/hookchat-go). Change the import to hookchat "hookchat.dev/go"; the package name is still hookchat. HookChat serves the module itself, so go get hookchat.dev/go works with the go command's defaults: no setup and no account.
  • The default User-Agent is now HookChat/<version> (Jiffi; go), the shape every HookChat SDK sends, so the gateway can tell the SDKs apart the same way. WithUserAgent still overrides it.
  • Doc comments describe a 404 for another workspace's id in plain words. No behaviour change.

Fixed

  • MaxItems(0) yields nothing and makes no request, as it does in the Node and Python SDKs. It used to mean no limit, so a budget that had run out walked the whole list.
  • An All walk that resumes from a cursor now checks that cursor advanced on its first page too. A page that handed back the cursor it was asked for used to be followed once more, so its items came out twice before the invalid_response error.
  • All iterators return an *Error with code invalid_response when the server hands back a cursor that does not advance, instead of looping until the context ends.
  • Error.RequestID is set on a bare 429 from the edge: when x-correlation-id is missing the client reads x-request-id, then apigw-requestid, the same order as the other SDKs.

Full hookchat-go changelog on GitHub

hookchat CLI#

Added

  • accounts disconnect <account_key> unlinks an account (DELETE /v1/accounts/:account_key). It asks first at a terminal, like keys revoke; account_not_found exits 4.
  • Exit code 4 (not found) covers route_not_found, the gateway's answer to a method and path it does not serve; the exit table says so.
  • messages list --direction echo lists the account's own sends mirrored back, native-app replies included.
  • webhooks create and webhooks update refuse an unknown --events type before any request, with a usage error that lists the real names; the flag help names them too.
  • conversations get accepts --limit, --cursor and --all to read older messages; output includes the next cursor.
  • keys revoke, webhooks delete and webhooks rotate ask before acting when a person is at the keyboard. --yes (-y) skips the question, and a piped stdin never asks, so scripts run unchanged; a "no" cancels with exit 1 and nothing is sent.
  • With --output json, a failure is one JSON object on stderr ({"error": {"code", "message", "detail", "request_id", "docs", "exit"}}), so a script can parse it instead of the human lines.

Changed

  • Install with `go install hookchat.dev/cli/cmd/hookchat@latest` (was github.com/jiffi-co/jiffi-message-gateway/packages/hookchat-cli/cmd/hookchat). It works with the go command's defaults: no setup and no account. The CLI now requires hookchat.dev/go v0.2.0.
  • Help text says workspace, create, issue and "gave up" where it said tenant, mint and DLQ. --tenant and HOOKCHAT_TENANT keep their names.
  • The User-Agent is now HookChat/<version> (Jiffi; cli), the shape every HookChat client sends, so the gateway can tell the CLI and the SDKs apart the same way.
  • Help text in plain words: webhooks deliveries replay and replay-all say "retry", summary says "count by status", conversations reads "the inbox", and a 404 for another workspace's id is described as such. The command reference is regenerated.
  • Timeout and network failures are classified by the SDK's IsTimeout and IsNetworkError, and print a docs: line to the troubleshooting page's #timeout and #network_error entries like every other error.
  • internal_error from the gateway exits 10 like any other 5xx; the exit code table says so.
  • keys list shows a LAST_USED column, so a key nobody has used for months is easy to spot and revoke.

Fixed

  • --profile help, auth help and the README say what the code does: without a flag or HOOKCHAT_PROFILE, the config's default profile is used, and default only when the config names none.
  • docs generate and docs check print their success line through the record path, so --output json is one JSON object and --quiet prints the path.
  • verify --output json carries verified: true and the event's own created (whole unix seconds, as the body and every SDK do); the table still shows the date.
  • stats --quiet prints nothing, like ping: quiet is for ids and secrets, and stats has neither.
  • webhooks create refuses an unknown --events type before it resolves credentials, so a typo with no key configured exits 2 with the real names, as webhooks update does.
  • auth login writes the profile every other command reads: --profile, then HOOKCHAT_PROFILE, then the config's default profile. It used to fall back to a profile named default, so a key entered after --set-default on another profile went unused and a plain auth logout removed the wrong profile. The table output now shows default too.
  • A masked key never shows the whole key: a key with one or two characters after its prefix now masks to the prefix alone.
  • The red error: prefix on stderr follows stderr's own terminal, not stdout's, so cmd 2> file keeps escape bytes out of the file.
  • auth login reads the key with echo off at a terminal, so it shows on neither the screen nor the shell history.
  • hookchat help <unknown topic> exits 2 like any unknown command, instead of printing usage and exiting 0.
  • The confirm prompt reports a failed read of the answer as an error instead of a no.
  • verify --tolerance 0 means zero: a delivery whose timestamp is not this second is rejected. Zero used to be read as unset and fell back to 300 seconds. A negative tolerance is a usage error.
  • Table cells cut long text by characters, not bytes, so an emoji or accented letter at the cut no longer prints a broken byte; tabs and line breaks inside a message become spaces.
  • install.sh runs its body from a function called on the last line, so a curl | bash download that stops short runs nothing. Its --help no longer depends on reading the script file, so it works under curl | bash too.
  • request_id is printed for a bare 429 from the edge, read from apigw-requestid when x-correlation-id is missing.

Full hookchat CLI changelog on GitHub

0.1.1 (2026-09-03)#

@hookchat/node#

Changed

  • The README and the baseUrl option describe the API origin (https://hookchat.dev by default, a local or self-hosted gateway when testing) without naming any internal environment.

Full @hookchat/node changelog on GitHub

hookchat (Python)#

Changed

  • Documentation only: the base URL is described as the API origin, https://hookchat.dev by default; no internal environment is named.

Full hookchat (Python) changelog on GitHub

hookchat-go#

Changed

  • WithBaseURL documentation and the README example use a local gateway as the alternative origin; no internal environment is named.

Full hookchat-go changelog on GitHub

hookchat CLI#

Changed

  • auth login, auth status and auth logout help and the README show a second workspace profile (--profile acme) instead of an internal environment; the command reference is regenerated.

Full hookchat CLI changelog on GitHub

0.1.0 (2026-09-03)#

@hookchat/node#

Added

  • Every /v1 operation is now a typed method: stats.get, keys.create, keys.list, keys.revoke, realtime.ticket, webhooks.deliveries.summary and webhooks.deliveries.replayAll join the existing resources.
  • Async iteration over every paginated list: conversations.iterate, messages.iterate, audit.iterate and webhooks.deliveries.iterate follow next_cursor until exhausted, fetch lazily, and accept a maxItems guard. The paginate helper they are built on is exported.
  • Retries: a jittered exponential backoff (250 ms doubling to an 8 s ceiling, equal jitter) on 429 and 5xx and on timeouts and network errors for GETs; every other method is retried on 429 only, because a 429 was never processed while a 5xx on a send or a replay may have been. Retry-After (seconds or an HTTP date) is honoured and capped. Defaults: 2 retries, overridable per client and per call.
  • Timeouts: every request runs under an AbortController deadline covering connect, headers and body. Default 30 s, overridable per client (timeoutMs) and per call. A caller-supplied signal cancels a request and is never retried.
  • HookChatError now carries requestId (the x-correlation-id response header) and body (the raw response text) alongside code, status and detail. Transport and non-envelope failures use the same class with the SDK codes timeout, network_error, aborted, server_error and invalid_response.
  • Per-call RequestOptions (signal, timeoutMs, maxRetries) as the trailing argument on every method.
  • A client-level tenant option, applied as ?tenant to every request that does not name one; the way to use an operator key without repeating the slug.
  • messages.reply and messages.sendAsHumanAgent accept attachments (media sends, text optional when at least one attachment is present) and reply_to. The type still requires text or an attachment, still requires actor_id on the human-agent route, and still rejects tags and a generic send.
  • include_test on conversations.get, messages.list and messages.get to opt a read into test-scope rows.
  • ErrorCode gains csrf_failed, key_not_found, oauth_state_invalid and pending_link_not_found, matching the API's closed vocabulary.
  • New resource types: Stats, EndpointDeliveryCounts, ApiKey, CreatedApiKey, KeyScope, RealtimeTicket, SendAttachment, DeliveriesSummary, ReplayableDeliveryStatus, FailedDeliveryStatus.
  • An end-to-end suite (npm run e2e) against a live gateway and a capture receiver, driven by HOOKCHAT_* environment variables and failing when any is missing. It exercises every resource, the full webhook lifecycle, signature verification on captured deliveries across a secret rotation, and the send policy on open and closed conversations.
  • Unit coverage for every method, pagination, retry scheduling, timeouts, aborts and error mapping (64 tests), plus type-level assertions for the send policy.
  • The initial client, previously the unpublished, git-distributed 1.0.0: - The initial client: messages.reply, messages.sendAsHumanAgent, messages.list, messages.get, conversations.list, conversations.get, accounts.list, audit.list, webhooks.create/list/get/update/delete/rotate/test, webhooks.deliveries.list/replay/bulkReplay and ping. - verifyWebhook, scheme-compatible with the gateway's signer, accepting a multi-v1 header for secret rotation. - HookChatError (code, status, detail) and HookChatSignatureError. - The send policy enforced in the types: two send paths, actor_id required on the human-agent path, no generic send, no tags.

Changed

  • webhooks.deliveries.bulkReplay is renamed replayAll; the old name stays as a deprecated alias for 1.x.
  • ReplayResult.status is narrowed to the literal 'pending', matching the API.
  • verifyWebhook is unchanged in contract; its documentation now spells out the 24-hour rotation overlap (two v1= signatures, either secret verifies) and the 300-second timestamp tolerance.
  • A non-JSON or non-envelope response now raises HookChatError (invalid_response, or the status-mapped code for a 4xx/5xx) instead of a plain Error.

Full @hookchat/node changelog on GitHub

hookchat (Python)#

Added

  • HookChat and AsyncHookChat clients sharing one transport, with bearer auth, a default tenant for operator keys, with_options for per-call timeouts and retry budgets, and a request escape hatch.
  • Resources: messages (list, get, reply, send_as_human_agent), conversations (list, get), accounts (list), audit (list), stats (get), keys (create, list, revoke), webhooks (create, list, get, update, delete, rotate_secret, test) with webhooks.deliveries (list, summary, replay, replay_all), realtime (ticket) and ping.
  • Typed frozen dataclasses for every response, plus Page and cursor-following iterate generators with max_items.
  • Jittered exponential retries on 429, 5xx, connection errors and timeouts for reads, 429 only for replay and test, honouring Retry-After.
  • HookChatError with code, status, message, detail, request_id and body, and a subclass per error family including WindowClosedError, NotFoundError, RateLimitError, ValidationError, ServerError, APIConnectionError and APITimeoutError.
  • verify_webhook for HookChat-Signature verification, accepting either digest during a secret rotation and returning the typed event; SignatureVerificationError on any failure.

Full hookchat (Python) changelog on GitHub

hookchat-go#

Added

  • hookchat.New with functional options: WithBaseURL, WithHTTPClient, WithTimeout, WithMaxRetries, WithUserAgent, WithTenant.
  • Services: Messages (List, Get, Reply, SendAsHumanAgent), Conversations (List, Get), Accounts (List), Audit (List), Stats (Get), Keys (Create, List, Revoke), Webhooks (Create, List, Get, Update, Delete, RotateSecret, Test), Webhooks.Deliveries (List, Summary, Replay, ReplayAll), Realtime (Ticket) and Ping.
  • Typed request and response structs for every endpoint, matching the OpenAPI document field for field.
  • Cursor pagination with Page[T], range-over-func All iterators, MaxItems and Collect.
  • Retries with jittered exponential backoff on 429, 5xx and transport errors for reads, 429 only for replay and test, honouring Retry-After and the caller's context deadline.
  • *hookchat.Error with Code, Status, Message, Detail, RequestID and Body, plus IsWindowClosed, IsHumanAgentUnavailable, IsNotFound, IsRateLimited, IsAuth, IsInvalidRequest, IsReplayConflict and IsSendFailed.
  • VerifyWebhook and ParseEvent with typed event payload accessors, rotation-aware multi-v1 verification, a 300 second tolerance and ErrSignature.
  • Sign and SignatureHeaderValue for signing fixtures.
  • Unit tests over httptest servers and a live end-to-end suite behind the e2e build tag.

Full hookchat-go changelog on GitHub

hookchat CLI#

Added

  • The hookchat binary, built on the hookchat-go SDK, with a command for every /v1 resource: ping, keys (create, list, revoke), webhooks (create, list, get, update, delete, rotate, test), webhooks deliveries (list, summary, replay, replay-all), conversations (list, get), messages (list, get, reply, human-agent), accounts list, audit list, stats and realtime ticket.
  • auth login, auth status and auth logout with named profiles stored in $HOOKCHAT_CONFIG_DIR/config.json or the user config directory, written with mode 0600. Keys are read from --api-key or standard input and are never printed in full.
  • Credential precedence: flags, then HOOKCHAT_API_KEY, HOOKCHAT_BASE_URL, HOOKCHAT_TENANT and HOOKCHAT_PROFILE, then the profile.
  • --output table|json|yaml, --quiet (ids only, or the secret for keys create, webhooks create and webhooks rotate), --no-color and NO_COLOR, and --timeout.
  • Cursor pagination on every list: --limit, --cursor and --all, with next_cursor on stderr in table mode and in the JSON or YAML envelope otherwise.
  • verify: offline signature verification of a captured delivery ({"headers": ..., "body": ...} from a file or stdin, or a raw body with --header), honouring rotation overlaps and --tolerance.
  • Stable exit codes (0 to 10) mapped from the API error vocabulary and documented in hookchat --help; API errors print the code, message, detail, request id and a troubleshooting link.
  • completion for bash, zsh, fish and PowerShell; docs generate and docs check for the single-file command reference at docs/cli/reference.md, guarded by a test.
  • version, reporting the release version (or the module version recorded by go install), with Go, OS and architecture under --output json.
  • Unit tests over httptest servers for every command group and a live end-to-end suite behind the e2e build tag, wired into scripts/sdk-e2e.sh.

Full hookchat CLI changelog on GitHub