-
Notifications
You must be signed in to change notification settings - Fork 15
Slack Developer Guide
How the Slack channel is built, why it is built that way, and the traps that only appeared when somebody actually set it up. The user-facing page is Slack.
Shipped as #977, 2026-08-07.
The question that started this was "we've done Jira and Azure DevOps, shall we do Slack next?" β which sounds like adding tracker #3.
It is not. Slack has no work items and no states. The entire tracker engine β raise an issue, poll its status, pull comments back, map priorities β has nothing to attach to. Forcing it in would have meant pretending Slack is something it is not.
The right seam is the messaging channel engine, the one behind WhatsApp and web chat. Slack is provider #3 there, and the fit is unusually close:
MessagingProvider |
Slack |
|---|---|
verifyWebhook() |
v0= HMAC-SHA256 over v0:{ts}:{body}, plus a β€5-minute replay window |
verifyChallenge() |
the url_verification handshake (the hook already existed for Meta) |
parseInbound() |
event_callback β message events |
sendMessage() |
chat.postMessage with thread_ts
|
testConnection() |
auth.test |
downloadMedia() |
authenticated fetch of url_private
|
Worth noting: a third of it already shipped. BUILTIN_WEBHOOK_FORMATS['slack'] in the workflow engine has always posted out to Slack. This work is about the other direction.
There is no FreeITSM-published Slack app and no hosted OAuth service. includes/messaging/slack_manifest.php generates an app manifest the customer pastes into their own workspace.
The alternative was seriously considered and rejected. Publishing one app means Slack requires one fixed request URL, baked into the app config β so that URL has to be a server this project runs. Which means: every customer's Slack events flow through it, forever; an hour of downtime breaks every install; it can never be switched off without breaking people who did nothing wrong; and the project becomes a data processor for other companies' ticket contents.
For a free, self-hosted product that is the wrong trade. Bring-your-own costs the customer about five minutes and gives them an install nothing else can reach into.
The consequence to be honest about: inbound needs the customer's install to be internet-reachable over HTTPS. messaging_channels.ingress_mode has a relay value reserved for solving that; it is not built.
Slack sits at /system/integrations/slack with the trackers β because that is where a user looks for "integrations" β while storing rows in messaging_channels like the other channels.
Those two facts must not get confused, so the provider registry gained a kind:
'slack' => ['kind' => 'messaging', 'page' => 'slack.php', β¦]-
integrationsTrackerProviders()is the safe list for anything asking "can we raise a work item in it?". Use it, or Slack gets offered as an escalation target and fails when someone tries. -
system/integrations/provider.phpdelegates a messaging-kind provider to its own page rather than growing a second form shape. -
system/integrations/index.phpcounts messaging-kind providers frommessaging_channels, or Slack reads "Not set up" forever. -
api/integrations/save_connection.php/api/integrations/test_connection.phpreject a non-tracker provider outright β a stored Slack row there could never dispatch, becauseintegrationsProviderFor()would throw.
π΄ Known debt: WhatsApp and web chat are still tabs in Tickets settings, so there are two homes for third-party connections, split by an engine distinction that means nothing to a user. Decide deliberately before a fourth channel arrives.
Colour key: π provider Β· π₯ inbound Β· π€ outbound Β· π₯οΈ UI Β· π diagnostics Β· ποΈ schema
| π¨ | File | What you do there |
|---|---|---|
| π | includes/messaging/SlackProvider.php |
Everything Slack-specific: signature + replay check, event parsing, posting, profile lookup, file download, scope reporting, channel membership |
| π | includes/messaging/slack_manifest.php |
The manifest, the scope list with a reason per scope, and slackWebhookUrlProblem()
|
| π | includes/messaging/messaging.php |
messagingProvider() dispatch Β· normaliseChannelIdentifier() Β· messagingWebhookUrl() Β· messagingAdminMayAdministerChannel()
|
| π₯ | api/messaging/webhook.php |
The public endpoint. Per-channel, signature-verified, handles the one-time URL challenge |
| π₯ | includes/messaging/ingest.php |
Message β ticket. findOpenChannelTicket() and resolveSlackRequester()
|
| π€ | api/messaging/send_message.php |
The analyst reply, posted into the thread |
| π₯οΈ | system/integrations/slack.php |
Workspaces, the app-setup modal, the health check |
| π₯οΈ | system/integrations/help_slack.php |
The in-app guide |
| π | api/messaging/slack_diagnose.php |
The nine health checks |
| π | api/messaging/slack_manifest.php |
Serves the manifest to the page |
| ποΈ | messaging_channels |
provider='slack', channel_type='slack', channel_ref = workspace id, credentials encrypted |
Not touched: the tracker connectors, the workflow engine, the webhook engine.
findOpenChannelTicket() matches on the thread for Slack and on the sender for phone channels. On WhatsApp a person has one running conversation with the service desk. In Slack the same person can have five unrelated threads open at once, and matching on the sender would pile all five onto one ticket.
The thread address (C08HELP:1719500000.000100) rides on the inbound row's to_recipients, which already means "where this arrived". parseInbound() returns it as to β a field the normalised-message contract always had and nothing used.
Halo's Slack integration requires a person's Slack email to equal their Halo email. That fails for guests, contractors, and anyone whose Slack account is a personal address.
resolveSlackRequester() asks Slack for the email, uses it when it matches an existing user, and otherwise falls back to a named case β "Sam Okafor (Slack)", or "Slack user @Uβ¦". Never blank, never silent.
getOrCreateChannelUser() only sets a display name on INSERT, so anyone first seen while the lookup was failing would keep the placeholder for ever. The name is now corrected on that person's next message β but only when the stored value matches our own fallback string exactly, so a name an analyst edited by hand is never clobbered.
The manifest asks for seven scopes and no more. Deliberately absent: channels:manage, users:write, anything writing files β and channels:read, even though it would have made the diagnostics easier. A Slack admin reads that list before approving, and an unexplained permission is a reason to refuse.
That constraint shaped the health check: membership is tested with conversations.history (needs channels:history, which we already require) rather than conversations.info (needs channels:read, which we do not).
That is a WhatsApp Business rule. api/messaging/send_message.php refuses, and api/tickets/get_ticket_thread.php greys the composer out. Disagree and the UI blocks a reply the API would have accepted.
The unit suite was green for all of these.
Slack verifies the request URL the instant the app is created β which is also the instant the signing secret first exists. Requiring a signature on that handshake does not make setup strict, it makes it impossible:
create the app β Slack POSTs the challenge β 403 (no secret yet)
β Slack refuses the URL β the secret can never be collected β βΊ
The handshake is answered unsigned only while the channel has no stored secret, and logged when it happens. event_callback always requires a valid signature, so nothing can be injected through the gap. Verified all four ways: no-secret + challenge β 200; no-secret + event β 403 with nothing reaching the database; secret set + unsigned challenge β 403; secret cleared β 200 again.
chat.postMessage accepts JSON. users.info does not β given a JSON body it parses no arguments at all and answers user_not_found for a user that plainly exists in users.list. Nothing errors.
Everything is form-encoded now, which every Slack method accepts. This survived a full test suite because auth.test takes no arguments, so the one call that could be tested without arguments was the one that could not expose it.
normaliseChannelIdentifier() stripped every non-digit, turning U08ABCDEF into +08:
U08ABCDEF β +08 U08XYZQRS β +08 U08LMNOPQ β +08
W01AAAAAA β +01 W01BBBBBB β +01
5 distinct Slack users β 2 identities
Five people sharing two requesters, reading each other's tickets. It now takes a channel type; any future non-phone channel must pass one. WhatsApp's behaviour was proven byte-identical over ten inputs before and after.
Ingesting one makes the ticket talk to itself for ever. Three separate guards (bot_id, bot_profile, subtype === 'bot_message') because Slack marks bot messages inconsistently across subtypes.
messagingWebhookUrl() appended the app root to a configured public base URL that already contained it β /freeitsm-app/freeitsm-app/β¦. That string is what gets pasted into Slack, and the 404 fails verification with an error mentioning nothing about a path. Pre-existing, and it affected WhatsApp too.
Not a code bug β a Slack behaviour that produces a silently degraded result. An app created from a manifest holds a token missing most of its scopes until somebody clicks Reinstall to Workspace. Tickets still arrive; they just have no names on them. This is why the health check compares granted scopes against required ones.
api/messaging/slack_diagnose.php runs nine checks, each carrying the sentence that fixes the problem. A diagnostic that names a fault and stops has moved the problem, not solved it.
A check that cries wolf gets ignored the one time it is right.
-
tests/integrations/run.phpβ 353 assertions; the registry gained akindso this must stay green - Signature checks with negative controls: tampered body, wrong secret, 10-minute-old timestamp, missing headers
- Event parsing including every echo guard, and the
watch_channelscoping - An end-to-end ingest against the real database that asserts its own cleanup
β οΈ A live run. Every bug in section 6 was found this way, with the suite green. Post a message, reply from the inbox, reply again in Slack, and read the thread back withconversations.repliesto prove the reply threaded rather than landing in the channel.
- Slack β the user-facing page
- Web chat β Developer Guide Β· WhatsApp β the same engine
- External issue trackers β Developer Guide β the engine Slack deliberately does not use
- Multi-Tenancy β Developer Guide β company routing for channels
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- Theming & Dark Mode
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
- MobileβFriendly
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- Problem Management
- Tasks
- Assets
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
- Forms
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)