Public Ticket Portal
The Public Ticket Portal gives each customer a unique, secure URL where they can view their support conversation in a browser and post a reply β without needing email at all.
When enabled, every outbound email gets a small βView this conversationβ footer linking to a per-customer, per-ticket URL like:
https://api.scitor.io/portal/9c0aβ¦b3f
Customers donβt need an account. The token is the credential β long, random, and unguessable.
Why use it
- Mobile-friendly replies. Customers can answer from a browser without hunting through their inbox.
- Lost emails are no problem. If the original message was deleted or filtered to spam, the link still works.
- CAPTCHA-protected. Replies are gated by Cloudflare Turnstile to keep bots out.
- Auto-expires after close. Once an issue is closed, the link remains valid for a configurable retention window, then returns
410 Gone.
How it works
- Outbound email. When
/send,/sendall, or/replyproduces an email, Scitor finds-or-creates a portal ticket for that(issue, customer email)pair. - Footer added. A short footer with the portal URL is appended to both the HTML and plain-text bodies of the outbound email.
- Customer clicks the link. They see the public timeline of inbound + outbound messages and a reply form (if
allow_replyis on). - Customer replies. The reply is posted as a comment on the GitHub issue / discussion, attributed as
π¨ Reply via portal β from name <email>, and recorded on the timeline. - Issue closes. The retention timer starts. After it elapses the page returns
410 Gone. - Issue reopens. The expiry is cleared and the link works again.
Configuration
Add a portal: block to .github/scitor.yaml:
portal:
enabled: true # Required to opt in. Default: false.
allow_reply: true # Allow customers to reply via the portal. Default: true.
retention_after_close: 30d # How long the link stays valid after the issue closes. Default: 30d.
Options
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | false |
Master switch. The feature is fully off when not set to true. |
allow_reply |
boolean | true |
If false, the timeline is read-only. |
retention_after_close |
duration | 30d |
Time the link remains valid after issues.closed. Accepts 30m, 2h, 7d, 4w. |
Spam protection (CAPTCHA) on the reply form is built-in and managed by Scitor β no setup required.
Privacy & retention
- Tokens are unguessable β no enumeration possible.
- Per-ticket scope. A token only ever exposes the messages on a single ticket for a single customer.
- No internal comments. The timeline contains only the messages the customer themselves sent or received via email; team-only GitHub comments are never shown.
- Replies are sanitized. HTML pasted into the reply box is stripped before being stored or posted to GitHub.
- Hard expiry. After
retention_after_closeelapses post-close, the page returns410 Goneand the timeline is no longer accessible. - Revocation. A ticket can be revoked at any time by an admin; revoked links return
404.
Security
- Tokens are stored as the primary key β no separate signature is needed because the token itself has 256 bits of entropy.
- Replies require a Cloudflare Turnstile challenge. If
TURNSTILE_SECRET_KEYisnβt configured on the worker, replies are accepted with a warning logged. - Reply bodies are capped at 10,000 characters.
Limitations (v1)
- Attachments are not yet supported on portal replies.
- The portal is served from the API worker (
api.scitor.io/portal/:token); a custom-domain version may come later. - Inbound replies that arrive via email after the original ticket is created arenβt yet mirrored onto the portal timeline (the initial email and all outbound replies are).
Was this article helpful?