What breaks when a Slack approval modal's trigger_id expires before approval?
Slack's trigger_id is valid for only three seconds after the interaction that generated it, and can be used once. If an AP approval workflow does anything asynchronous — a permissions check, a lookup — between the button click and calling views.open, the trigger_id can expire, returning an expired_trigger_id error instead of opening the modal. The click appears to do nothing.
Part of the finance integrations guide.
| Symptom | A user clicks an AP approval button in Slack and nothing visibly happens |
|---|---|
| Root cause | The trigger_id from the interaction expired before views.open was called |
| What Slack's own docs say | "Triggers expire in three seconds" |
| Also true | A trigger_id can only be used once — a second attempt returns exchanged_trigger_id, not expired_trigger_id |
| Where this shows up | An expired_trigger_id or exchanged_trigger_id error in the response from views.open |
Why Slack enforces such a short window
Loopfour's Slack integration is the only one of the three communication channels in this matrix (Slack, Outlook, Gmail) with an interactive modal action — openModal and pushModal both require a trigger_id, which Slack issues on every user interaction (a button click, a shortcut invocation) and treats as extremely short-lived by design. Slack's own documentation states plainly that triggers expire in three seconds, and that a trigger_id can be used for only one operation — a second attempt with the same trigger_id returns an exchanged_trigger_id error rather than working again. Both limits exist to keep interactivity synchronous and predictable: Slack wants the modal opened as close as possible to the moment the user clicked something, not after an arbitrary delay.
Where the three seconds actually go
An AP approval workflow rarely opens a modal with zero logic in between the click and the views.open call. A realistic sequence — verify the clicking user is actually an authorized approver, look up the invoice's current status in case it changed since the message was posted, build the modal's view payload with that data — can easily add up to more than three seconds, especially if any of those steps involves a network call to a database or another API. Every one of those steps happens after Slack has already started the clock on the trigger_id.
How to design around the window
Call views.open with the trigger_id as the very first action after receiving the interaction payload, before any lookup or validation — open a minimal or loading-state modal immediately, then use views.update (which isn't trigger_id-gated) to populate it once the slower checks finish. If a check needs to reject the interaction entirely (the user isn't an authorized approver), that's a case for opening an error-state modal immediately and updating it, rather than doing the check before opening anything. Because a trigger_id is single-use, retry logic also can't simply call views.open again after an expired_trigger_id error — the only fix is to wait for the next user interaction and get a fresh trigger_id.
Next step
Map the finance workflow with the most exposure and prove the automation path.
Bring the invoice, contract, payment reconciliation, or customer finance workflow you have to defend at audit. Loopfour can map the trigger, controls, integrations, and approval loop.
Worked example
A 4-second approval-permission check that breaks the modal
An approver clicks 'Review invoice' on a Slack message. The workflow receives the interaction payload with trigger_id=12345.6789 at T+0ms, then calls an internal permissions API to confirm the clicking user is an authorized AP approver for that vendor's spend threshold — a call that, under normal load, takes 400ms but occasionally takes 3.5 seconds under load. On a slow run, by the time the permissions check returns and the workflow calls views.open with trigger_id=12345.6789, Slack has already expired it and returns expired_trigger_id. The approver sees the button click do nothing and, with no error surfaced to them, often clicks it again — which either repeats the same race or, if the first request eventually did complete server-side, returns exchanged_trigger_id on the second attempt instead. The fix: open a placeholder modal immediately using the trigger_id, run the permissions check after, and use views.update to either populate the real approval form or replace it with an access-denied message.
Frequently Asked Questions
Sources
Related
Diagnostic
How do I shorten the close when AP reconciliation is the bottleneck?
Move AP-to-bank reconciliation to a rolling, weekly cadence instead of a single close-week event, so most of the matching work is already done by the time the period ends. Most AP bottlenecks at close aren't caused by close-week work itself — they're a backlog of sync gaps and unmatched payments that accumulated all month and only get discovered when someone finally looks.
Read moreTopic
AP & Invoice Processing
Accounts payable and invoice processing is the set of steps a vendor bill goes through between arriving at a company and turning into a payment: capturing what the vendor sent, checking it against wha…
Read moreDiagnostic
What causes a three-way match failure between the PO, receipt, and invoice?
A three-way match compares the PO, the receipt (what was actually delivered), and the invoice on quantity, price, and charges. A failure means one of those three disagrees beyond tolerance — usually because the invoice bills a quantity that isn't fully receipted yet, the unit price differs from the PO, or the invoice adds a charge, like freight, the PO never included.
Read moreTopic
Integrations
Every finance automation vendor publishes an integrations page: a grid of logos, a claim of "seamless" connectivity, and not much else. That page answers a marketing question — does this vendor touch…
Read more