Helpora Manual, as shipped in the download
Helpora — User Manual
A support desk you can be honest about: requests, threads, response and resolution targets, and a clock that cannot be improved without answering somebody. Version 1.0.3 [src: app/controllers/api.php:25].
About this manual
Every statement here was written by reading Helpora's own source, and each one carries the file it came from in a bracketed src marker. Paths are relative to the folder this docs directory sits in. If a sentence disagrees with the application, the application is right and this manual has a bug.
Nothing is described that the code does not do. Where something is deliberately limited, or currently wrong, that is said plainly rather than left out.
⭐ The clock is the product
The source opens with the argument the whole design rests on [src: app/src/Request.php:5]:
Every help desk reports response times. Almost every one of them can be made to report better ones than it earned, because the timer is reset by things that are not an answer: an internal note, a reassignment, a status change, an auto-reply. A desk whose numbers can be improved without answering anybody is not measuring service, it is measuring how well its agents know the tool.
Four rules follow, and all four are enforced in one file.
1. The first response happens once
first_response_at is stamped by the first message that actually goes to the requester, and is never written again [src: app/src/Request.php:13]. The write is guarded — WHERE first_response_at IS NULL — so two agents replying in the same second cannot both claim it: the loser matches zero rows and the earlier stamp stands [src: app/src/Request.php:14].
Nothing else touches it: no note, no reassignment, no status change, no reopening [src: app/src/Request.php:16].
2. You cannot close what you never answered
Resolving or closing a request that has never had an outbound message is refused, because a desk that can close in silence will, and its numbers will look excellent [src: app/src/Request.php:19]. Executed, with the refusal it actually produces [src: app/src/Request.php:446]:
status new, never answered -> "This request has never been answered. Write to the person who
raised it before resolving or closing it — a desk that can settle
in silence will, and its response times will look excellent."
status open, never answered -> same refusal
status open, answered -> allowed
3. The only pause is waiting on the requester, and it costs the first response nothing
You may stop the resolution clock while genuinely waiting for the person who asked — but only after you have written to them, because otherwise "waiting on the requester" means "waiting for them to guess we need something" [src: app/src/Request.php:23]. The pause ends by itself the moment they reply.
And the first-response clock cannot be paused at all: there is no state of the world in which you are waiting on somebody to hear back from you for the first time [src: app/src/Request.php:27].
Executed [src: app/src/Request.php:461]:
open, nothing sent yet -> "Nothing has been sent to the requester yet, so they are not the ones
being waited on. Ask them the question first, then mark it waiting."
open, already answered -> allowed
already resolved -> "That request is already settled."
4. A deadline only ever moves later
Targets land on working days; a weekend or one of your own non-working days rolls the observed date forward. The policy date itself is never rewritten, and both are shown, so nothing becomes breached sooner because a holiday was added [src: app/src/Request.php:30].
Executed with Monday 7 September marked as a company non-working day [src: app/src/Request.php:198]:
policy 2026-09-04 (Fri) -> observed 2026-09-04
policy 2026-09-05 (Sat) -> observed 2026-09-08
policy 2026-09-06 (Sun) -> observed 2026-09-08
policy 2026-09-07 (Mon, non-working) -> observed 2026-09-08
The same arithmetic applies to the hour, not just the day. A four-hour response target set at five o'clock on that Friday lands at one o'clock on the Tuesday once the weekend and the closed Monday are stepped over [src: app/src/Request.php:135]:
response, 4h from 2026-09-04 17:00 -> 2026-09-08 13:00
resolution, 2 days from the same -> 2026-09-09
Targets per priority
Four priorities, each with a response target in hours and a resolution target in days [src: app/src/Request.php:61]. Executed:
urgent 1 hour 1 day normal 8 hours 5 days
high 4 hours 2 days low 24 hours 10 days
Requesters do not need an account
A request is identified by an unguessable code [src: app/src/Request.php:88] and followed up with a passphrase, of which only the hash is kept [src: app/src/Request.php:324]. There is no account to create and no password to reset.
An email address is optional. Replies always reach the requester on their request page, behind the reference and passphrase. With Settings → Email the requester switched on (it is off by default), a requester who gave an address is also emailed when you reply and when you resolve or close the request [src: app/src/helpers.php:174]. The email carries the reference, their own summary line and the address of the request page — never your reply, an internal note or their passphrase — and each send, or the reason it did not send, is written on the request's log. With no address, nothing can be emailed, which the interface says rather than hides [src: app/src/Request.php:36].
Mail goes through your own SMTP server; the Security setting offers STARTTLS (usually port 587), SSL/TLS (usually port 465) or none for a local relay.
Month arithmetic
Where a month has to be added, it clamps to the end of the target month rather than overflowing [src: app/src/Request.php:97]. Executed against PHP's own:
2026-01-31 + 1 month PHP: 2026-03-03 Helpora: 2026-02-28
2026-08-31 + 6 months PHP: 2027-03-03 Helpora: 2027-02-28
Users and roles
Three roles [src: app/controllers/v3.php:60]. A member can handle requests and read the thread [src: app/controllers/v3.php:61]; admin holds everything.
A viewer is granted audit.view explicitly, written into the role by name rather than inherited from a wildcard [src: app/controllers/v3.php:60], and the audit page requires exactly that [src: app/controllers/v3.php:659]. On a desk whose value is the honesty of its numbers, letting a read-only account see who changed what reads as deliberate — but if you expected otherwise, change the role.
Backups are gated on settings.edit [src: app/controllers/api.php:238], which no role below admin holds [src: app/controllers/api.php:246].
Webhooks
The events you can subscribe to are the events the app sends [src: app/src/Webhook.php:22]:
| event | sent when |
|---|---|
request.created | a request arrives through the form |
request.first_response | the first reply to a request is sent — once per request |
request.message | the requester writes back |
request.status_changed | a request moves, on the board, the request screen, in bulk or through the API |
request.settled | a request is resolved or closed |
An endpoint with no events ticked receives all of them. An endpoint saved before this release under the older names case.created, case.message or case.status_changed receives the matching request.* event [src: app/src/Webhook.php:25]. Payloads carry the reference, the queue, the status and the clock facts — never the request text or a message body.
The API and agent access
Five tools [src: app/controllers/v3.php:309]: three read — the request list, one request in detail, and the statistics — and two write, posting a handler message [src: app/controllers/v3.php:368] and settling a request [src: app/controllers/v3.php:397].
The statistics tool reports first-reply performance as met, missed and never answered [src: app/controllers/v3.php:470] — the third figure being the one a desk with a silent-close habit would rather not publish.
An agent settling a request goes through the same refusal as a person: it cannot close what was never answered.
What Helpora does not do
It does not let a note, a reassignment or a status change stamp a first response [src: app/src/Request.php:16].
It does not let two agents both claim the first reply [src: app/src/Request.php:14].
It does not allow a request to be settled in silence [src: app/src/Request.php:19].
It does not let you pause a clock before you have written to anybody [src: app/src/Request.php:23].
It does not pause the first-response clock at all [src: app/src/Request.php:27].
It does not move a deadline earlier [src: app/src/Request.php:30].
It does not store a requester's passphrase [src: app/src/Request.php:324].