buzz
buzz ¶
Domo Buzz API routes.
Buzz has two, unrelated APIs. This module wraps the product API — the one
with 136 endpoints that a Slack-style client is built on. Do not confuse it
with the deprecated 2-endpoint "Buzz Integration API" (api.domo.com +
OAuth bearer, bot registration only) — that one is a different surface with
different auth and is out of scope here.
Auth: X-DOMO-Developer-Token against https://{instance}.domo.com/api
(the same header crew-dcs uses everywhere else), NOT api.domo.com OAuth.
Undocumented behaviour, confirmed live on datacrew-space 2026-08-28:
GET /buzz/v1/channels/mereturns{"me": {<channelId>: ChannelMeta}, "nextOffset": N}— a map keyed by channel id, not a list. EachChannelMetais a thin summary (unreadCounts,importantCounts,mentioned,channelType,lastViewedTimestamp), not a full channel.- Both
channels/meandchannelsare paginated viaoffset/limitin,nextOffsetout.list_my_channels/list_channelsfollownextOffsetto exhaustion by default (loop_until_end=True) and merge every page, so a caller can never silently receive only page one — passloop_until_end=Falsefor a single page, ormaximumto cap the total fetched either way. - Channel history paging is a bespoke DSL —
pagingMode,pagingId,pagingField,pagingLimit— not offset/limit. Plus a pile ofinclude*expansion flags. Nothing is expanded by default. POST /buzz/v1/messagesis a Slack incoming-webhook payload verbatim:{alert, channel, icon_url, text, username}. The snake_caseicon_url/usernameinside an otherwise camelCase API is intentional — do not "fix" it to camelCase.POST /buzz/v1/channels/{id}/messagestakes a client-suppliedguid(idempotency key), generated here when the caller doesn't supply one.POST /buzz/v1/sockets/authenticateis a standard Pusher channel authorizer, confirmed live ondatacrew-space2026-08-28 — not the bare-string shape a Postman sample once suggested (that claim was never live-verified and was wrong; see :func:authenticate_socket). Request is form-urlencoded (socket_id,channel_name,Content-Type: application/x-www-form-urlencoded); response is JSON{"auth": "appkey:sig"}. SendingAccept: text/plaingets a 406; sending a JSON body gets a 400.GET /buzz/v1/botsreturns 403 on a plain developer token — confirmed uniform on bothdatacrew-spaceanddomo-community2026-08-28, so this is not a per-instance rollout gap.GET /authorization/v1/authoritieslists 129 authorities with exactly one buzz-related entry (buzz.admin, "Edit conversations and messages") and nothing bot- or integration-related, so the 403 is not grantable via a role authority — it most likely requires an integration/bot principal rather than a developer token, same family asPOST /buzz/v1/messages's failure below. Bots are out of scope for this module.POST /buzz/v1/messagesis unusable with a developer token — see :func:post_message's docstring for the live investigation.- Pusher realtime fires different events for different roles relative to a post — this is the single easiest thing to get wrong here, see the "Pusher realtime" section below.
- Buzz channel permissions can exceed the caller's instance role: a role 4
(Participant) user got a clean
403 Access DeniedonDELETE /buzz/v1/channels/{id}(confirms the instance role is enforced there) but a channel-levelINVITE_OTHERSgrant made at invite time let that same user re-invite others on that channel — confirmed live ondatacrew-space2026-08-28. Do not infer a caller's Buzz capabilities from their instance role alone; effective rights are per-channel. (Topic invite:PUT /api/buzz/v1/topics/{channelId}/invite, body[{"id": "<userId>", "permission": "INVITE_OTHERS", "type": "USER"}]— works immediately; not wrapped by a route here yet."ADMIN"is rejected on topic create, same ascreate_topic'sdefaultPermission/publicPermission—INVITE_OTHERSis the server's real default in both places.) None-valued optional fields are omitted from every POST body here (via_clean_body, the body-side sibling of_clean_params) rather than sent as JSONnull. Live-verified forcreate_topic'sdescription: an omitted key and an explicitnullbehave identically.POST /buzz/v1/messages(post_message) couldn't be live-verified either way — see its docstring — but omission is used there too, for one consistent convention.
Buzz_CRUD_Error ¶
Buzz_CRUD_Error(
operation: str,
channel_id: str | None = None,
message: str | None = None,
res=None,
**kwargs
)
Bases: RouteError
Raised when a Buzz create, update, or delete operation fails.
Source code in src/crew_dcs/routes/buzz.py
137 138 139 140 141 142 143 144 145 146 147 148 149 150 | |
Buzz_GET_Error ¶
Buzz_GET_Error(
channel_id: str | None = None,
message: str | None = None,
res=None,
**kwargs
)
Bases: RouteError
Raised when a Buzz retrieval operation fails.
Source code in src/crew_dcs/routes/buzz.py
119 120 121 122 123 124 125 126 127 128 129 130 131 | |
authenticate_socket
async
¶
authenticate_socket(
auth: DomoAuth,
socket_id: str,
channel_name: str,
*,
context: RouteContext | None = None,
**context_kwargs
) -> ResponseGetData
Authenticate a private Pusher channel subscription.
POST /buzz/v1/sockets/authenticate is a standard Pusher channel
authorizer — live-verified on datacrew-space 2026-08-28, correcting
an earlier claim (sourced from a Postman sample, never live-verified)
that this returned a bare string:
- request body: form-urlencoded,
socket_id+channel_name,Content-Type: application/x-www-form-urlencoded - response: JSON,
{"auth": "appkey:sig"}->res.responseis adict, not a string
Also live-verified: Accept: text/plain gets a 406; a JSON body gets
a 400 — this endpoint wants exactly the Pusher-standard shape above, not
this API's usual JSON-everything convention.
Raises :class:Buzz_CRUD_Error if the response isn't the expected
{"auth": ...} JSON shape (e.g. a bare string) — that shape must not
be treated as a silent success.
Source code in src/crew_dcs/routes/buzz.py
946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 | |
create_channel_message
async
¶
create_channel_message(
auth: DomoAuth,
channel_id: str,
content: str,
guid: str | None = None,
attachments: list[dict[str, Any]] | None = None,
mentions_grant_permission: bool | None = None,
*,
context: RouteContext | None = None,
**context_kwargs
) -> ResponseGetData
Post a message to a Buzz channel (the richer, per-channel form).
POST /buzz/v1/channels/{channelId}/messages -> body
{attachments[], content, guid}.
The response nests the created message: it is
{"message": {...}, "properties": {...}, "me": {...}}, so the new id is
at res.response["message"]["id"] — NOT res.response["id"], which is
absent. Verified live 2026-08-28. Reading it wrong yields None
silently, and the next call using that id (edit, delete, react) then 400s
against /buzz/v1/messages/None.
Related, for anyone building a live status surface:
PUT /buzz/v1/messages/{message_id} edits a message in place (body
{"content": {"text": ...}}; a bare string also works) and emits
message-updated-v1 over Pusher to other channel members, so an edit
is visible to everyone, not just its author.
DELETE /buzz/v1/messages/{message_id} is a soft delete — the
message stays in the channel reading "Message deleted." rather than
disappearing. Neither is wrapped as a route here yet; the verified request
shapes live in
infrastructure/bonker/apps/letta-code-channels/channels/buzz-stream/src/buzz.ts.
guid is client-supplied and
usable for idempotency; a fresh uuid4 is generated when not provided —
attachments/guid already never reach the wire as null because
of those fallbacks, but the body still goes through _clean_body for
consistency with the other Buzz POST routes (see create_topic's
docstring for the live finding this follows).
Source code in src/crew_dcs/routes/buzz.py
593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 | |
create_topic
async
¶
create_topic(
auth: DomoAuth,
title: str,
description: str | None = None,
accesses: list[dict[str, Any]] | None = None,
default_permission: str = "INVITE_OTHERS",
public_permission: str = "INVITE_OTHERS",
filters: list[dict[str, Any]] | None = None,
pin: bool = False,
*,
context: RouteContext | None = None,
**context_kwargs
) -> ResponseGetData
Create a Buzz topic (a channel with an ACL).
POST /buzz/v1/topics -> body {accesses, defaultPermission,
description, filters, pin, publicPermission, title}. Response is
{"channel": {...}}. accesses[] entries are
{id, permission, type} where type is USER/GROUP/etc —
per-conversation, per-user scoping is native here.
Live-verified on datacrew-space 2026-08-28: "ADMIN" (the sample
value in Bryce's Postman collection) is NOT a valid
defaultPermission/publicPermission value on create and returns a
bare 400 with no field-level detail. Domo's own server default —
confirmed by round-tripping a body that omits both fields — is
"INVITE_OTHERS", used here instead. Empty accesses/filters
lists are fine; the 400 is specific to the permission enum.
Realtime delivery follows the invite, not membership — including for
the creator. Verified live 2026-08-29 with two identities, roles run
both ways: an identity that CREATED a topic receives no Pusher event
when another member posts into it, while an identity that was INVITED
receives message-created-v2. Any integration that needs to hear
messages (a bot, an agent, a listener) must be invited explicitly, even
if it owns the channel. This fails silently — the socket connects,
subscribes successfully, and simply never delivers anything.
accesses at create time is NOT equivalent to an invite. A topic
created with accesses=[{id, permission, type}] for another user does
grant that user access, but does NOT subscribe them to the channel's
realtime stream: in a two-identity test on 2026-08-28 the invitee's Pusher
connection received no message-created-v2 at all for messages
posted into that topic. Re-running the identical test with an explicit
PUT /buzz/v1/topics/{channel_id}/invite (body
[{"id", "permission", "type"}], 200 + empty body) delivered the event
~0.1s after the post. If you need the other party to receive anything,
invite them explicitly — accesses alone looks like it worked and is
silent at runtime.
Also live-verified: an explicit JSON null and an omitted key behave
identically for description — both produce a 200 with
channel.description coming back null. description is
nonetheless omitted from the body when None (via _clean_body)
rather than sent as null, for a single consistent convention across
this module's POST bodies rather than a per-route judgment call.
Source code in src/crew_dcs/routes/buzz.py
768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 | |
delete_channel
async
¶
delete_channel(
auth: DomoAuth,
channel_id: str,
*,
context: RouteContext | None = None,
**context_kwargs
) -> ResponseGetData
Delete a Buzz channel (topic, group chat, etc.).
DELETE /buzz/v1/channels/{channelId}.
Source code in src/crew_dcs/routes/buzz.py
462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 | |
get_channel_by_id
async
¶
get_channel_by_id(
auth: DomoAuth,
channel_id: str,
include_permission_count: bool | None = None,
include_pinned_count: bool | None = None,
include_messages: bool | None = None,
include_huddles: bool | None = None,
include_inception_messages: bool | None = None,
include_filters: bool | None = None,
include_accesses: bool | None = None,
include_recent_people: bool | None = None,
include_my_meta: bool | None = None,
include_video: bool | None = None,
message_limit: int | None = None,
huddle_limit: int | None = None,
inception_message_limit: int | None = None,
inception_message_sort_mode: str | None = None,
access_limit: int | None = None,
recent_people_limit: int | None = None,
message_offset_id: str | None = None,
*,
context: RouteContext | None = None,
**context_kwargs
) -> ResponseGetData
Get a single Buzz channel by id.
GET /buzz/v1/channels/{channelId}. Nothing is expanded by default —
pass the relevant include_* flag for what you need (reactions,
accesses, recent people, etc.).
Source code in src/crew_dcs/routes/buzz.py
383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 | |
get_channel_messages
async
¶
get_channel_messages(
auth: DomoAuth,
channel_id: str,
paging_mode: str | None = None,
paging_id: str | None = None,
paging_field: str | None = None,
paging_limit: int | None = None,
include_reactions: bool | None = None,
include_favorited: bool | None = None,
include_my_meta: bool | None = None,
include_video: bool | None = None,
include_permission_count: bool | None = None,
include_accesses: bool | None = None,
include_recent_people: bool | None = None,
recent_people_limit: int | None = None,
include_inception_messages: bool | None = None,
inception_message_limit: int | None = None,
inception_message_sort_mode: str | None = None,
message_types: str | None = None,
*,
context: RouteContext | None = None,
**context_kwargs
) -> ResponseGetData
Get message history for a Buzz channel.
GET /buzz/v1/channels/{channelId}/messages. Paging is a bespoke DSL —
pagingMode/pagingId/pagingField/pagingLimit — NOT
offset/limit. Every include_* flag is passed through as given;
nothing is expanded unless requested.
paging_mode is only valid together with paging_id. Sending
pagingMode=BEFORE (or AFTER) on its own returns a bare 400 Bad
Request, as does pairing it with paging_field but no id; omitting it
entirely returns 200. Verified live on datacrew-space 2026-08-28
across seven parameter combinations. So the first page must not set
paging_mode — only a follow-up page anchored on a message id may.
Callers that default paging_mode to a sort direction will 400 on
every first call.
Source code in src/crew_dcs/routes/buzz.py
506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 | |
get_socket_config
async
¶
get_socket_config(
auth: DomoAuth,
*,
context: RouteContext | None = None,
**context_kwargs
) -> ResponseGetData
Get the caller's Pusher realtime configuration.
GET /buzz/v1/sockets/me -> {applicationKey, host, port, sslPort,
channel, buzzChannel}. channel is per-user (subscribe to your own
stream); buzzChannel is company-wide.
applicationKey is per-instance — live-verified values:
datacrew-space -> cf468a162798e3ce79ce, domo-community ->
e640cbc9816caa81b971. Always read it from this endpoint at runtime;
never hardcode it.
See the "Sockets (Pusher realtime)" section comment above this function for the poster-vs-observer event role split — the single easiest thing to get wrong when consuming the channel/buzzChannel this returns.
Source code in src/crew_dcs/routes/buzz.py
902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 | |
list_channels
async
¶
list_channels(
auth: DomoAuth,
offset: int | None = None,
limit: int | None = None,
loop_until_end: bool = True,
maximum: int | None = None,
debug_loop: bool = False,
*,
context: RouteContext | None = None,
**context_kwargs
) -> ResponseGetData
List all Buzz channels visible to the caller.
GET /buzz/v1/channels -> {"channels": [...], "me": {}, "nextOffset": N, "properties": {}}.
Paginated to exhaustion by default (loop_until_end=True): follows
nextOffset — a falsy (None/0) nextOffset, an empty page,
or a nextOffset that fails to advance all signal the last page — and
concatenates every page's channels list. A caller can never silently
receive just page one this way. Pass loop_until_end=False for exactly
one page (the returned response["nextOffset"] then tells you whether
more exist); maximum caps the total number of channels collected in
either mode.
Source code in src/crew_dcs/routes/buzz.py
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 | |
list_my_channels
async
¶
list_my_channels(
auth: DomoAuth,
offset: int | None = None,
limit: int | None = None,
loop_until_end: bool = True,
maximum: int | None = None,
debug_loop: bool = False,
*,
context: RouteContext | None = None,
**context_kwargs
) -> ResponseGetData
List the calling user's Buzz channels.
GET /buzz/v1/channels/me. The response is a map keyed by channel
id — {"me": {<channelId>: ChannelMeta}, "nextOffset": N} — not a
list. Do not assume list semantics on response["me"].
Paginated to exhaustion by default (loop_until_end=True): follows
nextOffset — a falsy (None/0) nextOffset, an empty page,
or a nextOffset that fails to advance all signal the last page — and
merges every page's me map by key. A caller can never silently
receive just page one this way. Pass loop_until_end=False for exactly
one page (the returned response["nextOffset"] then tells you whether
more exist); maximum caps the total number of entries merged in
either mode.
Source code in src/crew_dcs/routes/buzz.py
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 | |
post_message
async
¶
post_message(
auth: DomoAuth,
channel: str,
text: str,
username: str | None = None,
icon_url: str | None = None,
alert: bool = False,
*,
context: RouteContext | None = None,
**context_kwargs
) -> ResponseGetData
Post a Slack-shaped incoming-webhook message.
POST /buzz/v1/messages -> body {alert, channel, icon_url, text,
username}. This is a Slack incoming-webhook payload adopted verbatim by
Domo: icon_url and username are snake_case on purpose, inside an
otherwise camelCase API. Do NOT rename them to iconUrl/userName —
that is the regression this route guards against. username/
icon_url are omitted from the body when None (via
_clean_body) rather than sent as JSON null.
Live investigation on datacrew-space 2026-08-28: this endpoint 400s
on every body shape tried against a plain developer token, regardless of
whether channel is a topic id or title, whether username/
icon_url are real strings, null, or omitted entirely — even
POST /buzz/v1/channels/{channelId}/messages (this module's other,
working message route) succeeds against the same channel in the same
run, isolating the failure to this endpoint specifically. No live signal
on null-vs-omit was obtainable here since nothing succeeds either way;
most likely this Slack-webhook-shaped endpoint needs an incoming-webhook
registration this token doesn't carry (paralleling GET /buzz/v1/bots's
403 on a plain developer token — see the module docstring). Omission is
used anyway, for consistency with the rest of this module's POST bodies
and because it is provably no worse than null for the one shape that
does work (create_topic).
This endpoint is unusable with a developer token, confirmed again
2026-08-28: 400 on every body shape tried (the full Slack shape above,
text+channel only, and an empty body); text alone with no
channel gets a 500 instead of a 400, so the handler does read
and reject channel rather than bailing before touching the body at
all. Errors are opaque either way — {"status":400,"statusReason":"Bad
Request","message":"Bad Request","toe":"<trace id>"} — no field-level
detail. toe is a Domo trace id, worth quoting verbatim to Domo
support if this is ever escalated. Practical consequence: the Slack-shaped
username/icon_url display-name override is not an available
way to give a bot its own identity in Buzz — use
create_channel_message (POST
/buzz/v1/channels/{channelId}/messages) instead, which works but has
no display-name override of its own.
Source code in src/crew_dcs/routes/buzz.py
673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 | |