Support portal and CSAT
The public ticket form, token-based ticket pages where customers follow and reply to tickets, and the CSAT satisfaction survey.
The support portal lets customers submit tickets, follow the conversation, reply, and rate their experience without an account. Each ticket's pages are addressed by its 40-character portal_token; whoever has the link can read the public thread and reply.
Routes
These routes are in the web route group (odden-service.routes.web), with no prefix by default. See Configuration reference.
| Method | URI | Route name | Rate limit |
|---|---|---|---|
| GET | /support |
odden.support.create |
|
| POST | /support |
odden.support.store |
odden-public |
| GET | /support/tickets/{token} |
odden.support.show |
|
| POST | /support/tickets/{token}/reply |
odden.support.reply |
odden-public |
| GET | /support/rate/{token} |
odden.support.rate |
|
| POST | /support/rate/{token} |
odden.support.submitRating |
odden-public |
The POST routes use the web group's normal CSRF protection; the bundled forms include @csrf. An unknown token returns 404.
Link to the form from your site with the route name, so a changed prefix or domain is picked up:
<a href="{{ route('odden.support.create') }}">Contact support</a>Submitting a ticket
GET /support shows a form. POST /support validates:
| Field | Rules |
|---|---|
name |
required, string, max 255 |
email |
required, email, max 255 |
subject |
required, string, max 255 |
priority |
required, one of low, medium, high, urgent |
description |
required, string |
The customer chooses the priority, and it sets the ticket's SLA targets. The controller then:
- Finds the Core
Contactwith that email, ignoring case and surrounding whitespace (soDana@Example.comfindsdana@example.com), or creates one with the email lowercased and trimmed, splittingnameinto first and last name at the first space. - Creates the ticket with
CreateTicketActionand sourceWebPortal. The ticket is routed, a task is logged on the contact's timeline, andTicketCreatedNotificationis queued for the customer. - Redirects to the ticket's page with the flash message "Your support ticket has been received. Our team will review it shortly." under the
statussession key.
While the customer types a subject, the form calls the knowledge suggestion endpoint and offers matching articles. Article titles, categories, and excerpts are inserted as text, so HTML in an article is shown as typed. If the customer clicks "This solved it", the form records a deflection and disables the ticket form.
The ticket page
GET /support/tickets/{token} shows the ticket number, status, priority, contact name, owner name, creation date, channel, and every message that is not an internal note. getPortalUrl() on the ticket returns this URL, and every customer email links to it.
POST /support/tickets/{token}/reply takes a required body and adds it as a Customer message from the ticket's contact through ReplyTicketAction, so the status changes as described in Statuses: a reply reopens a Resolved or Closed ticket unless odden-service.reopen_on_customer_reply is false. It redirects back with the flash message "Your reply has been posted to the ticket." If the ticket was merged into another, what happens depends on where the ticket came from, see Replies to merged tickets: for a ticket created from an email, by phone, or through the API, the reply is posted on the primary ticket and the flash message says so instead. For a ticket created on the portal or in the chat widget, the reply is refused with a body validation error asking the customer to reply to the latest email from your team. The page itself always shows the token's own ticket, never the primary. The bundled page hides the reply form once the ticket is Closed (merged tickets are closed), but the endpoint still accepts replies, so customers can reopen a closed ticket by email or from your own view.
CSAT surveys
When a ticket is resolved with ResolveTicketAction, the customer receives TicketResolvedCsatNotification with a "Rate Your Support Experience" button that links to getCsatUrl(). The ticket page also shows a rating button once the ticket is Resolved or Closed and has no rating yet.
GET /support/rate/{token} shows a 1 to 5 star form with an optional comment. POST /support/rate/{token} validates:
| Field | Rules |
|---|---|
rating |
required, integer, 1 to 5 |
comment |
optional, string, max 1000 |
It saves csat_rating and csat_comment on the ticket and redirects to the ticket page with "Thank you! Your feedback has been recorded." The survey accepts a rating at any status, and a new submission replaces the previous one.
A rating of 1 or 2 also triggers service recovery:
- An internal
Systemnote is added to the ticket with the rating, the comment, and "Supervisor review recommended." - If the ticket has a contact, a pending task titled
CSAT Service Recovery: Ticket #{number} ({rating}/5 stars)is logged on the contact's timeline, due in 24 hours.
You can also record a rating from your own code with ResolveTicketAction's $csatRating and $csatComment arguments. That path doesn't trigger service recovery.
Customizing the pages
The pages are Blade views in the odden-service namespace: odden-service::portal.create, odden-service::portal.show, and odden-service::portal.rate. They are standalone HTML pages that load Tailwind CSS from cdn.tailwindcss.com and the Plus Jakarta Sans font from Google Fonts.
The package has no publish tag for its views. To change one, copy it from vendor/getodden/crm-service/resources/views into resources/views/vendor/odden-service in your app, keeping the same relative path (for example resources/views/vendor/odden-service/portal/show.blade.php). Laravel uses your copy instead of the package's.
To replace the routes and controllers entirely, see Using your own routes.