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.
0.5.0 (2026-10-09)#
@hookchat/node#
Added
client.messages.getHold(hold_id)reads one hold (GET /v1/messages/holds/{hold_id}): itsstatus(awaiting_verification,sending,sent,failedorexpired),platform_message_idonce sent anderror(HoldFailure) once failed.verify_urlis never in this answer. An unknown id, one more than 30 minutes old, or another workspace's ishold_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 theHookChatErrorits code carries (send_failed502,human_agent_unavailableorrate_limited409,conversation_not_found404); an expired one rejects withhold_expired(409).timeout_msdefaults to 900 000 (15 minutes); past it, it rejects with the newHoldTimeoutError(codetimeout,holdIdandlastStatusset) and the hold goes on until it expires. AnAbortSignalin the options ends the wait.- New
Hold,HoldStatus,HoldFailureandWaitForHoldParamstypes. hold_not_found(404),hold_expired(409) andverification_failed(422) joinErrorCode.ConversationErasure.holds: how many held human-agent messages an erase removed, sent or not.
Changed
- Breaking:
client.messages.sendAsHumanAgentnow resolves with aHold, not aSendResult. Nothing is sent by the call: the API answers 202 and holds the message until the person who typed it openshold.verify_urland passes a short check in the browser. HookChat then sends it and the usualmessage.sentevent 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_urlcarries a one-time secret: hand it only to that person, and never log it. Code that readplatform_message_idfrom the send now reads it fromwaitForHold. The send no longer rejects withsend_failed; a refusal by Meta comes fromwaitForHold.
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 aHold: itsstatus(awaiting_verification,sending,sent,failedorexpired),platform_message_idonce sent anderror(HoldFailure) once failed. A read never carriesverify_url. An unknown, ended or other workspace's hold raisesNotFoundErrorwithhold_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,NotFoundErrororServerError). An expired hold raisesConflictErrorwithhold_expired: nobody passed the check, and nothing was sent. Pasttimeoutseconds (default 15 minutes) it raises the newHoldTimeoutError(anAPITimeoutError, withhold_idandlast_status); the hold goes on, andget_holdreads it until it expires.- New
Hold,HoldState,HoldFailureandHoldTimeoutErrorexports. BothHookChatandAsyncHookChathave the new methods. - The new error codes:
hold_not_found(404) raisesNotFoundError,hold_expired(409) raisesConflictErrorandverification_failed(422) raisesValidationError. ConversationErasure.holds: how many held human-agent messages an erase removed, sent or not.
Changed
- Breaking:
client.messages.send_as_human_agentnow returns aHold, not aSendResult. Nothing is sent by the call: the API answers 202 and holds the message until the person who typed it openshold.verify_urland 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_urlcarries a one-time secret: hand it only to that person, and never log it;repr(hold)hides it. Code that readplatform_message_idfrom the send now reads it fromwait_for_hold. The send no longer raisesSendFailedError; a refusal by Meta comes fromwait_for_hold.
Full hookchat (Python) changelog on GitHub
hookchat-go#
Added
Hold(ID,Status,ConversationID,CreatedAt,ExpiresAt,VerifyURL,PlatformMessageID,Error),HoldStatuswithHoldAwaitingVerification,HoldSending,HoldSent,HoldFailedandHoldExpired, andHoldFailure(Code,Message,Detail).client.Messages.GetHold(ctx, holdID)reads one hold (GET /v1/messages/holds/{hold_id}).VerifyURLis never set on a read. Name an operator key's workspace withForTenant.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*Errorwith the hold's code, message and detail and the status the API gives that code; an expired one returnshold_expired(409). PastTimeout(default 15 minutes) it returns*HoldTimeoutError(withHoldID, the lastStatusandWaited;IsTimeoutreports it) and the hold goes on.CodeHoldNotFound(404, andIsNotFoundreports it),CodeHoldExpired(409) andCodeVerificationFailed(422).ConversationErasure.Holds:Conversations.Erasealso counts the held human-agent messages it removed, sent or not.
Changed
- Breaking:
Messages.SendAsHumanAgentno longer sends. The API now answers 202 with a hold, and the method returns a*Holdin place of a*SendResult. The person who typed the message opensHold.VerifyURLand 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.VerifyURLcarries 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, showVerifyURLand callWaitForHold, which returns the hold withPlatformMessageIDonce sent. The call no longer returnssend_failed, because it sends nothing.
Full hookchat-go changelog on GitHub
hookchat CLI#
Added
messages human-agent --no-openprints the link without opening a browser.messages human-agent --no-waitprints 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.--quietprints only the link;--output jsonoryamlprints the one hold document. The browser rules are the same.- Exit codes for the new error codes:
hold_not_foundexits 4 like every other not found,hold_expiredexits 8 (nobody passed the check in time, nothing was sent) andverification_failedexits 5. A send that fails after the check exits with the code of its error:send_failed8,human_agent_unavailable6,rate_limited7,conversation_not_found4.
Changed
- Breaking:
messages human-agentno longer sends at once. The gateway holds the message and answers with averify_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 printsverify_urlon 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,--quietprints the link, then the id, and--output jsonoryamlprints two documents: the hold as made, withverify_url, then the hold once sent (id,status,platform_message_idand the rest). It used to print one object withplatform_message_id. - Breaking: on
messages human-agent,--wait-timeoutnow covers the wait for the send as well as any upload, and defaults to15m, as long as a hold waits.attachments uploadandmessages replykeep their2mdefault. Past it the command exits 10 with atimeouterror that names the hold, and the hold goes on until it expires. - The CLI now requires
hookchat.dev/gov0.5.0. conversations eraseprints aholdscount: 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 asMessageAttachment[](type,url), onconversations.get,messages.listandmessages.get. It is always present and empty when the message has none.urlis 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) joinsErrorCode: a replay afterbody_expires_atis refused, because there is nothing left to send.upload_not_found(404),attachment_too_large(413) andunsupported_media_type(415) joinErrorCode: 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.fileis a path, aBufferorUint8Array, or aBloborFile; the name and content type come from it unless you setfilenameandcontent_type. It asks for an upload url, posts the file to it with no API key, then finishes, and returns theUploadedAttachmentMeta now has. HookChat hands the file to Meta in the background, souploadwaits: it reads the upload every second, then every two seconds, until Meta has it, on a timer that keeps the process alive, and anAbortSignalin the options ends the wait.wait_timeout_mssets how long it waits (default 120 000); past it,uploadrejects withUploadTimeoutError(codetimeout,uploadIdset) and the upload goes on. A file Meta refuses rejects withsend_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 asUploadStatus: itsstatus(waiting,processing,readyorfailed), theattachment(ReadyAttachment:idandtype) once ready and theerror(UploadFailure) once failed. Use it after anUploadTimeoutError, or to wait in your own way.- A send names an uploaded file by id:
attachments: [{ type: a.type, attachment_id: a.id }].SendAttachmentis nowSendAttachmentUrl | SendAttachmentRef, so a url and an id together do not compile. TheUploadandUploadedAttachmenttypes 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.doneisfalsewhile a big conversation goes on erasing in the background; call again to check. It cannot be undone. NewConversationErasureandConversationErasureResulttypes.
Fixed
- A send carries
textorattachments, 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 withinvalid_request(400). The types match:ReplyParamsandHumanAgentParamsare 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(alsoConversationMessage): a message's media as a list ofMessageAttachment(type,url), onconversations.get,messages.listandmessages.get. It is always a list and empty when the message has none, an older gateway included.urlis 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(409replay_expired), aConflictError: a replay afterbody_expires_atis 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) raisesNotFoundError, andattachment_too_large(413) andunsupported_media_type(415) raiseValidationError. client.attachments.upload(conversation_id, file)sends a file from your computer in one call, onHookChatandAsyncHookChat.fileis a path, bytes, or a binary file object; the name and content type come from it unless you passfilenameandcontent_type. It asks for an upload url, posts the file to it with no API key, then finishes, and returns theUploadedAttachmentMeta now has. HookChat hands the file to Meta in the background, souploadwaits: it reads the upload every second, then every two seconds, until Meta has it.wait_timeoutsets how long it waits, in seconds (default 120); past it,uploadraisesUploadTimeoutError(anAPITimeoutError, withupload_id) and the upload goes on. A file Meta refuses raisesSendFailedError, 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 asUploadStatus: itsstatus(waiting,processing,readyorfailed), theattachment(ReadyAttachment:idandtype) once ready and theerror(UploadFailure) once failed. Use it after anUploadTimeoutError, or to wait in your own way.Attachment(type=a.type, attachment_id=a.id)sends an uploaded file by id; a dict withattachment_idworks too.Attachment.urlis now optional, and giving both or neither raisesValueError. NewUploadandUploadedAttachmenttypes 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 aConversationErasureResultwithdoneand theremovedcounts, never content, onHookChatandAsyncHookChat.doneis False while a big conversation goes on erasing in the background; call again to check. It cannot be undone.
Fixed
- A send carries
textorattachments, 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 withinvalid_request(400), andreplyandsend_as_human_agentraiseValueErrorbefore 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, raisesValueErrorbefore any request.
Full hookchat (Python) changelog on GitHub
hookchat-go#
Added
Message.Attachments: a message's media as[]MessageAttachment(Type,URL), onConversations.Get,Messages.ListandMessages.Get. It is empty when the message has none.URLis Meta's link and expires after a while; HookChat does not keep a copy.DeliverySummary.BodyExpiresAtandBodyExpiresTime(): 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.CodeReplayExpiredandIsReplayExpired: a replay afterBodyExpiresAtis refused with 409replay_expired, because there is nothing left to send.CodeUploadNotFound(404, andIsNotFoundreports it),CodeAttachmentTooLarge(413) andCodeUnsupportedMediaType(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; setFile(anio.Reader, withSizewhen you know it) in place ofPathfor bytes you already hold. The name and content type come from the path unless you setFilenameandContentType. It asks for an upload URL, posts the file to it with no API key, then finishes, and returns the*UploadedAttachmentMeta now has. HookChat hands the file to Meta in the background, soUploadwaits: it reads the upload every second, then every two seconds, until Meta has it, and a cancelledctxends the wait.UploadParams.WaitTimeoutsets how long it waits (default two minutes); past it,Uploadreturns*UploadTimeoutError(withUploadID;IsTimeoutreports it) and the upload goes on. A file Meta refuses returnssend_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: itsStatus(UploadWaiting,UploadProcessing,UploadReadyorUploadFailed), theAttachment(*ReadyAttachment:IDandType) once ready and theError(*UploadFailure) once failed. Use it after an*UploadTimeoutError, or to wait in your own way.Attachment.AttachmentIDsends an uploaded file by id:Attachment{Type: a.Type, AttachmentID: a.ID}.URLis now left out of the JSON when empty. NewUploadandUploadedAttachmenttypes 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*ConversationErasureResultwithDoneand theRemovedcounts, never content.Doneis false while a big conversation goes on erasing in the background; call again to check. It cannot be undone.
Fixed
- A send carries
TextorAttachments, 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 withinvalid_request(400), andReplyandSendAsHumanAgentrefuse it locally (CodeInvalidRequest,Status0) 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 listandconversations getadd aMEDIAcolumn with each attachment's type;messages getprints oneattachmentline per item with its type and Meta's link. JSON and YAML carryattachmentsas[{type, url}],[]when there is none. Meta's links expire; HookChat does not keep a copy. replay_expiredexits 8, likereplay_conflict: a replay after the event body expired (7 days) is refused. JSON delivery rows carrybody_expires_at.- The errors of the new upload routes have exit codes:
upload_not_foundexits 4 like every other not found, andattachment_too_largeandunsupported_media_typeexit 5 likeinvalid_request. hookchat attachments upload --conversation C --file ./photo.jpgsends a file from your computer to Meta and prints the attachment: its id, type and size (--output jsonoryamlfor all of it,--quietfor the id).--type,--filenameand--content-typeoverride 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,jsonoryaml).--wait-timeoutsets how long it waits (default2m); past it the command exits 10 with atimeouterror 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 theattachment_idto send;--quietprints only that) orfailed(with the error). An upload more than a day old isupload_not_found(exit 4).messages replyandmessages human-agenttake--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.doneisfalsewhile a big conversation goes on erasing in the background; run it again later.
Changed
- The CLI now requires
hookchat.dev/gov0.4.0.
Fixed
messages replyandmessages human-agenttake--textor--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
--attachmentfor up to 10 images in one message, sent as one message with oneplatform_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.updatedjoinsEVENT_TYPESandWebhookEventType.verifyWebhookreturns it asParticipantEvent, whosedata.participantis anEventParticipant(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 themessage.receivedit follows.Conversation.participant_picture: the other party's profile picture URL, or null. Meta's picture URLs expire.Conversation.participant_profile, typed asParticipantProfileStatus('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.sentwithdata.message.direction'echo', a few seconds after it is sent. A send through the API is still'outbound'.rawis Meta's payload on an echo.
Changed
EventMessage.directionis typed asMessageDirection('inbound' | 'outbound' | 'echo') instead of a bare string, so a check for an app send narrows in the editor.conversations.listis ordered by the newest message in either direction, and lists a thread your team started from the app (last_inbound_atnull, windowclosed,can_replyfalse).
Full @hookchat/node changelog on GitHub
hookchat (Python)#
Added
participant.updatedjoinsEVENT_TYPESandEventTypeName, and parses asParticipantEventwith.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 themessage.receivedit follows.Conversation.participant_picture: the other party's profile picture URL, orNone. Meta's picture URLs expire.Conversation.participant_profile, typed asParticipantProfileStatus(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.sentwithevent.message.direction"echo", a few seconds after it is sent. A send through the API is still"outbound".rawis Meta's payload on an echo.
Changed
EventMessage.directionis typed asDirection(inbound,outbound,echo) instead of a barestr.conversations.listis ordered by the newest message in either direction, and lists a thread your team started from the app (last_inbound_atisNone, the window isclosed,can_replyis false).
Full hookchat (Python) changelog on GitHub
hookchat-go#
Added
EventTypeParticipantUpdated(participant.updated) joinsEventTypes, andEvent.Participant()decodes it intoEventParticipant(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 themessage.receivedit follows.Conversation.ParticipantPicture: the other party's profile picture URL, or nil. Meta's picture URLs expire.Conversation.ParticipantProfile, typed asParticipantProfileStatuswithParticipantProfilePending,ParticipantProfileCompleteandParticipantProfileUnavailable: 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.sentwithDirectionDirectionEcho, a few seconds after it is sent. A send through the API is stillDirectionOutbound.Rawis Meta's payload on an echo.
Changed
EventMessage.Directionis typed asMessageDirection, so it compares withDirectionInbound,DirectionOutboundandDirectionEcho. An untyped string constant still compares, astringvariable needs a cast.Conversations.Listis ordered by the newest message in either direction, and lists a thread your team started from the app (LastInboundAtnil, window closed,CanReplyfalse).
Full hookchat-go changelog on GitHub
hookchat CLI#
Added
webhooks createandwebhooks updateacceptparticipant.updatedin--events, sent once per conversation when the participant's name, username or picture is first stored.conversations listhas aPROFILEcolumn, the conversation'sparticipant_profile(pending,completeorunavailable).conversations getshowsparticipant_username,participant_pictureandparticipant_profile.
Changed
messages list --directionhelp 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 listhelp 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/gov0.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_handleis the display name.accounts.disconnect(accountKey)unlinks an account (DELETE /v1/accounts/:account_key): Meta stops sending its webhooks, the token is deleted andaccount.disconnectedfires.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.directionand the list filter are typed asMessageDirection(inbound,outbound,echo) instead of a bare string.messages.listandmessages.iterateacceptdirection: 'echo', the account's own sends mirrored back, native-app replies included.eventsonCreateWebhookParams,UpdateWebhookParamsandEndpointisWebhookEventType[], 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 asWebhookEventType.conversations.getacceptslimitandcursorand returnsnext_cursorfor older messages.SendAttachmentType: the attachmenttypeis'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#httpfor a reply with no envelope);TROUBLESHOOTING_URLis exported.internal_errorinErrorCode: 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_viais typed asSentVia(api_in_window,api_human_agent,native_app,unknown) rather than any string, matching the spec's list;SentViais exported.- The
User-Agentis nowHookChat/<version> (Jiffi; node): the same brand form, with the SDK named, matching the Python and Go SDKs. webhooks.rotateis nowwebhooks.rotateSecret, the name the Python (rotate_secret) and Go (RotateSecret) SDKs already use, so one docs page reads the same in every language.rotatestays as a deprecated alias for 1.x.
Fixed
verifyWebhookrefuses an empty secret withHookChatSignatureErrorinstead of verifying against the public empty key, matching the Go verifier. List params (cursor,limit,tenant) accept an explicitundefined, so the README's pagination loop compiles underexactOptionalPropertyTypes.iteratetreats an emptynext_cursoras the end of the list, as it doesnull. It used to ask for the first page again, yield its items twice, then throwinvalid_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.summaryanddeliveries.replayAllleavestatusout of the query for an empty list instead of sendingstatus=, as the Python and Go SDKs do.verifyWebhookrefuses a signed body that is not an event: JSON that is not an object, or an object with no stringidandtype, now raisesHookChatSignatureErrorinstead 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. Onlywebhooks.test,deliveries.replayandreplayAllretry on a 429, matching the Python and Go SDKs; every other mutation now hands therate_limitederror back so the caller is never left with a message re-sent behind its back. AccountEventno longer declaresaccount_keyandreasonondata.account: the gateway emits exactlyexternal_id,platform,tenantandhandle, asdocs/events/schema.jsonand 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 @, orNone. AlwaysNoneon Messenger.participant_handleis 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 andaccount.disconnectedfires.account_not_found(404) joins the codes, raised asNotFoundError.route_not_found(404) joins the error codes, raised asNotFoundError: a request to a method and path the API does not serve now answers in the envelope.Message.directionis typed asDirection(inbound,outbound,echo) instead of a barestr.messages.listandmessages.iterateacceptdirection="echo", the account's own sends mirrored back, native-app replies included.EventTypeName, aLiteralof the six event types;webhooks.createandwebhooks.updatetakeevents: Sequence[EventTypeName], so a typo in a filter fails under mypy rather than as a 400.conversations.getacceptslimitandcursorin both clients;ConversationDetail.next_cursorresumes older messages.SendAttachmentType:Attachment.typeis aLiteralof 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#httpfor a reply with no envelope);TROUBLESHOOTING_URLis exported.internal_errormaps toServerError: 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_viais typed asSentVia(api_in_window,api_human_agent,native_app,unknown) orNone, the list the spec pins;SentViais exported.- The
User-Agentis nowHookChat/<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'sbase_urlandtenant). No behaviour change.
Fixed
verify_webhookrefuses an empty secret withSignatureVerificationErrorinstead of verifying against the public empty key, matching the Go verifier.messages.replyandmessages.send_as_human_agentno longer taketenant=. The send routes have no?tenant; the conversation id scopes the send, and the keyword was sent and ignored.deliveries.summarytakesstatusasFailedDeliveryStatus(dlq,failed,blocked,retrying) anddeliveries.replay_allasReplayableDeliveryStatus(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.updatetakesstatusasactiveorpaused, not any string.typing-extensionsis no longer listed as a dependency on Python 3.10. Nothing imported it, sopip install hookchatpulled a second package for no reason; the README's "one dependency" is true again.verify_webhookrefuses a signed object with no stringidandtype: it raisesSignatureVerificationErrorinstead of returning anEventwith empty fields, matching the Node and Go SDKs.iterateandaiteratefollow an empty page that carries a cursor, as a filtered page can be, instead of stopping early; a cursor that does not advance raisesHookChatErrorwith codeinvalid_responseinstead of looping.HookChatError.request_idis set on a bare 429 from the edge: the fallback header is nowapigw-requestid, which the HTTP API sends, notx-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.ParticipantHandleis the display name.Accounts.Disconnect(ctx, accountKey)unlinks an account (DELETE /v1/accounts/:account_key): Meta stops sending its webhooks, the token is deleted andaccount.disconnectedfires.CodeAccountNotFound(account_not_found, 404) joins the codes, andIsNotFoundreports it.IsNotFoundreportsroute_not_foundtoo.CodeRouteNotFound(route_not_found, 404): a request to a method and path the API does not serve now answers in the envelope.DirectionEchofor 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)paginatesConversations.Get;ConversationDetail.NextCursorresumes older messages.AttachmentTypewithAttachmentImage,AttachmentVideo,AttachmentAudioandAttachmentFile;Attachment.Typeuses it. An untyped string constant still converts, astringvariable needs a cast.Error.DocsURL(): the troubleshooting entry for the error (#<code>, or#httpfor a reply with no envelope);TroubleshootingURLis exported.IsTimeout(err)andIsNetworkError(err): tell a request that ran out of time from one that never connected, the way the Node and Python SDKs classifytimeoutandnetwork_error. Transport errors are still the wrapped net error, soerrors.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 tohookchat "hookchat.dev/go"; the package name is stillhookchat. HookChat serves the module itself, sogo get hookchat.dev/goworks with the go command's defaults: no setup and no account. - The default
User-Agentis nowHookChat/<version> (Jiffi; go), the shape every HookChat SDK sends, so the gateway can tell the SDKs apart the same way.WithUserAgentstill 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
Allwalk 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 theinvalid_responseerror. Alliterators return an*Errorwith codeinvalid_responsewhen the server hands back a cursor that does not advance, instead of looping until the context ends.Error.RequestIDis set on a bare 429 from the edge: whenx-correlation-idis missing the client readsx-request-id, thenapigw-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, likekeys revoke;account_not_foundexits 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 echolists the account's own sends mirrored back, native-app replies included.webhooks createandwebhooks updaterefuse an unknown--eventstype before any request, with a usage error that lists the real names; the flag help names them too.conversations getaccepts--limit,--cursorand--allto read older messages; output includes the next cursor.keys revoke,webhooks deleteandwebhooks rotateask 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 requireshookchat.dev/gov0.2.0. - Help text says workspace, create, issue and "gave up" where it said tenant, mint and DLQ.
--tenantandHOOKCHAT_TENANTkeep their names. - The
User-Agentis nowHookChat/<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 replayandreplay-allsay "retry",summarysays "count by status",conversationsreads "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
IsTimeoutandIsNetworkError, and print adocs:line to the troubleshooting page's#timeoutand#network_errorentries like every other error. internal_errorfrom the gateway exits 10 like any other 5xx; the exit code table says so.keys listshows a LAST_USED column, so a key nobody has used for months is easy to spot and revoke.
Fixed
--profilehelp,authhelp and the README say what the code does: without a flag orHOOKCHAT_PROFILE, the config's default profile is used, anddefaultonly when the config names none.docs generateanddocs checkprint their success line through the record path, so--output jsonis one JSON object and--quietprints the path.verify --output jsoncarriesverified: trueand the event's owncreated(whole unix seconds, as the body and every SDK do); the table still shows the date.stats --quietprints nothing, likeping: quiet is for ids and secrets, and stats has neither.webhooks createrefuses an unknown--eventstype before it resolves credentials, so a typo with no key configured exits 2 with the real names, aswebhooks updatedoes.auth loginwrites the profile every other command reads:--profile, thenHOOKCHAT_PROFILE, then the config's default profile. It used to fall back to a profile nameddefault, so a key entered after--set-defaulton another profile went unused and a plainauth logoutremoved the wrong profile. The table output now showsdefaulttoo.- 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, socmd 2> filekeeps escape bytes out of the file. auth loginreads 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 0means 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.shruns its body from a function called on the last line, so acurl | bashdownload that stops short runs nothing. Its--helpno longer depends on reading the script file, so it works undercurl | bashtoo.request_idis printed for a bare 429 from the edge, read fromapigw-requestidwhenx-correlation-idis missing.
Full hookchat CLI changelog on GitHub
0.1.1 (2026-09-03)#
@hookchat/node#
Changed
- The README and the
baseUrloption describe the API origin (https://hookchat.devby 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.devby default; no internal environment is named.
Full hookchat (Python) changelog on GitHub
hookchat-go#
Changed
WithBaseURLdocumentation 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 statusandauth logouthelp 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
/v1operation is now a typed method:stats.get,keys.create,keys.list,keys.revoke,realtime.ticket,webhooks.deliveries.summaryandwebhooks.deliveries.replayAlljoin the existing resources. - Async iteration over every paginated list:
conversations.iterate,messages.iterate,audit.iterateandwebhooks.deliveries.iteratefollownext_cursoruntil exhausted, fetch lazily, and accept amaxItemsguard. Thepaginatehelper 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
AbortControllerdeadline covering connect, headers and body. Default 30 s, overridable per client (timeoutMs) and per call. A caller-suppliedsignalcancels a request and is never retried. HookChatErrornow carriesrequestId(thex-correlation-idresponse header) andbody(the raw response text) alongsidecode,statusanddetail. Transport and non-envelope failures use the same class with the SDK codestimeout,network_error,aborted,server_errorandinvalid_response.- Per-call
RequestOptions(signal,timeoutMs,maxRetries) as the trailing argument on every method. - A client-level
tenantoption, applied as?tenantto every request that does not name one; the way to use an operator key without repeating the slug. messages.replyandmessages.sendAsHumanAgentacceptattachments(media sends, text optional when at least one attachment is present) andreply_to. The type still requires text or an attachment, still requiresactor_idon the human-agent route, and still rejectstagsand a genericsend.include_testonconversations.get,messages.listandmessages.getto opt a read into test-scope rows.ErrorCodegainscsrf_failed,key_not_found,oauth_state_invalidandpending_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 byHOOKCHAT_*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/bulkReplayandping. -verifyWebhook, scheme-compatible with the gateway's signer, accepting a multi-v1header for secret rotation. -HookChatError(code, status, detail) andHookChatSignatureError. - The send policy enforced in the types: two send paths,actor_idrequired on the human-agent path, no genericsend, notags.
Changed
webhooks.deliveries.bulkReplayis renamedreplayAll; the old name stays as a deprecated alias for 1.x.ReplayResult.statusis narrowed to the literal'pending', matching the API.verifyWebhookis unchanged in contract; its documentation now spells out the 24-hour rotation overlap (twov1=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 plainError.
Full @hookchat/node changelog on GitHub
hookchat (Python)#
Added
HookChatandAsyncHookChatclients sharing one transport, with bearer auth, a default tenant for operator keys,with_optionsfor per-call timeouts and retry budgets, and arequestescape 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) withwebhooks.deliveries(list, summary, replay, replay_all),realtime(ticket) andping. - Typed frozen dataclasses for every response, plus
Pageand cursor-followingiterategenerators withmax_items. - Jittered exponential retries on 429, 5xx, connection errors and timeouts for reads, 429 only for replay and test, honouring
Retry-After. HookChatErrorwithcode,status,message,detail,request_idandbody, and a subclass per error family includingWindowClosedError,NotFoundError,RateLimitError,ValidationError,ServerError,APIConnectionErrorandAPITimeoutError.verify_webhookforHookChat-Signatureverification, accepting either digest during a secret rotation and returning the typed event;SignatureVerificationErroron any failure.
Full hookchat (Python) changelog on GitHub
hookchat-go#
Added
hookchat.Newwith 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) andPing. - Typed request and response structs for every endpoint, matching the OpenAPI document field for field.
- Cursor pagination with
Page[T], range-over-funcAlliterators,MaxItemsandCollect. - Retries with jittered exponential backoff on 429, 5xx and transport errors for reads, 429 only for replay and test, honouring
Retry-Afterand the caller's context deadline. *hookchat.ErrorwithCode,Status,Message,Detail,RequestIDandBody, plusIsWindowClosed,IsHumanAgentUnavailable,IsNotFound,IsRateLimited,IsAuth,IsInvalidRequest,IsReplayConflictandIsSendFailed.VerifyWebhookandParseEventwith typed event payload accessors, rotation-aware multi-v1verification, a 300 second tolerance andErrSignature.SignandSignatureHeaderValuefor signing fixtures.- Unit tests over httptest servers and a live end-to-end suite behind the
e2ebuild tag.
Full hookchat-go changelog on GitHub
hookchat CLI#
Added
- The
hookchatbinary, 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,statsandrealtime ticket. auth login,auth statusandauth logoutwith named profiles stored in$HOOKCHAT_CONFIG_DIR/config.jsonor the user config directory, written with mode 0600. Keys are read from--api-keyor standard input and are never printed in full.- Credential precedence: flags, then
HOOKCHAT_API_KEY,HOOKCHAT_BASE_URL,HOOKCHAT_TENANTandHOOKCHAT_PROFILE, then the profile. --output table|json|yaml,--quiet(ids only, or the secret forkeys create,webhooks createandwebhooks rotate),--no-colorandNO_COLOR, and--timeout.- Cursor pagination on every list:
--limit,--cursorand--all, withnext_cursoron 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. completionfor bash, zsh, fish and PowerShell;docs generateanddocs checkfor the single-file command reference atdocs/cli/reference.md, guarded by a test.version, reporting the release version (or the module version recorded bygo 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
e2ebuild tag, wired intoscripts/sdk-e2e.sh.