Paid add-on. Service is licensed separately from the open-source packages. See pricing
Configuration reference
Every odden-service config key and environment variable, the full route list, and how to replace the package routes.
The service module reads its settings from config/odden-service.php. Publish the file only if you need to change something that has no environment variable:
php artisan vendor:publish --tag=odden-service-configSettings shared by all Odden modules, such as the user model and rate limits, are covered in Configuration.
Environment variables
| Variable | Config key | Default |
|---|---|---|
ODDEN_SERVICE_ROUTES_ENABLED |
odden-service.routes.enabled |
true |
ODDEN_SERVICE_DOMAIN |
odden-service.routes.web.domain and odden-service.routes.api.domain |
null (any domain) |
ODDEN_SERVICE_PREFIX |
odden-service.routes.web.prefix |
'' (no prefix) |
ODDEN_SERVICE_API_PREFIX |
odden-service.routes.api.prefix |
api/service |
ODDEN_SERVICE_API_TOKEN |
odden-service.api.token |
null (token endpoints disabled) |
ODDEN_SERVICE_INBOUND_REQUIRE_AUTH |
odden-service.inbound_email.require_authenticated_sender |
false |
ODDEN_SERVICE_INBOUND_OWN_ADDRESSES |
odden-service.inbound_email.own_addresses (comma-separated) |
[] |
ODDEN_SERVICE_CONFIRMATIONS_PER_HOUR |
odden-service.confirmations.per_address_per_hour |
3 |
ODDEN_SERVICE_REOPEN_ON_CUSTOMER_REPLY |
odden-service.reopen_on_customer_reply |
true |
ODDEN_SERVICE_PORTAL_CONFIRMATION_EMAIL |
odden-service.portal.confirmation_email |
false |
ODDEN_SERVICE_CHAT_CONFIRMATION_EMAIL |
odden-service.chat.confirmation_email |
false |
ODDEN_SERVICE_NOTIFICATIONS_CONNECTION |
odden-service.notifications.connection |
null (default queue connection) |
ODDEN_SERVICE_NOTIFICATIONS_QUEUE |
odden-service.notifications.queue |
null (the connection's default queue) |
ODDEN_SERVICE_SKIP_BOUNCED |
odden-service.deliverability.skip_bounced |
true |
ODDEN_SERVICE_SKIP_INVALID |
odden-service.deliverability.skip_invalid |
false |
The rate limits come from Core: ODDEN_PUBLIC_RATE_LIMIT (default 30 per minute) and ODDEN_API_RATE_LIMIT (default 600 per minute). See Rate limits.
Config keys
Tables
'tables' => [
'tickets' => 'odden_service_tickets',
'messages' => 'odden_service_ticket_messages',
'sla_policies' => 'odden_service_sla_policies',
'articles' => 'odden_service_articles',
'canned_responses' => 'odden_service_canned_responses',
'routing_rules' => 'odden_service_routing_rules',
],The models and migrations both read these names. Change them before you run the migrations. The migrations also read odden-core.tables.contacts and odden-core.tables.companies for their foreign keys.
Ticket defaults
'defaults' => [
'priority' => 'medium',
'source' => 'web_portal',
'prefix' => 'TICK',
],priority and source are the defaults for new Ticket models (TicketPriority and TicketSource values) when you don't set them. prefix is used for generated ticket numbers (TICK-2026-7WBPJ). Generated numbers keep the prefix as written. Inbound email doesn't use ticket numbers to thread replies.
CreateTicketAction uses priority and source when you don't pass them, and so do the inbound email webhook and the chat widget for the priority (the portal form asks the customer). The panel's ticket form starts with them selected.
Routes
'routes' => [
'enabled' => (bool) env('ODDEN_SERVICE_ROUTES_ENABLED', true),
'web' => [
'domain' => env('ODDEN_SERVICE_DOMAIN'),
'prefix' => env('ODDEN_SERVICE_PREFIX', ''),
'middleware' => ['web'],
],
'api' => [
'domain' => env('ODDEN_SERVICE_DOMAIN'),
'prefix' => env('ODDEN_SERVICE_API_PREFIX', 'api/service'),
'middleware' => ['api'],
],
],Each group's domain, prefix, and middleware are passed to Route::group(). Empty values are dropped. For example, ODDEN_SERVICE_PREFIX=care serves the help center at /care/help, and ODDEN_SERVICE_DOMAIN=support.example.com serves both groups only on that host.
The web group uses Laravel's web middleware group. The api group uses Laravel's stateless api middleware group, so the chat widget's polling, article suggestions and the email webhook never start a session or set a cookie; nothing in those endpoints uses the session. Earlier versions put the api group in web: if you published the config file, change it to ['api'] there too. The browser-facing chat and deflection endpoints and the email webhook also remove Laravel's CSRF middleware themselves, so they keep working if you put them back in web.
Generate links with route() and the route names below, so prefix and domain changes are picked up.
API token
'api' => [
'token' => env('ODDEN_SERVICE_API_TOKEN'),
],Protects the inbound email webhook. Until it is set, that endpoint returns 403.
Inbound email
'inbound_email' => [
'require_authenticated_sender' => (bool) env('ODDEN_SERVICE_INBOUND_REQUIRE_AUTH', false),
'own_addresses' => array_values(array_filter(explode(',', (string) env('ODDEN_SERVICE_INBOUND_OWN_ADDRESSES', '')))),
],When require_authenticated_sender is true, the inbound email webhook only threads a reply onto an existing ticket if the request has sender_authenticated set to a true value or dmarc set to pass. Otherwise the email opens a new ticket.
Mail from own_addresses (and from mail.from.address) is ignored, so your own notifications can't loop back in. Auto-replies, bounces and bulk mail are always ignored.
Confirmation emails
'confirmations' => [
'per_address_per_hour' => (int) env('ODDEN_SERVICE_CONFIRMATIONS_PER_HOUR', 3),
],The most "request received" emails (TicketCreatedNotification) one address gets in an hour, from any channel. The tickets are still created. 0 turns confirmation emails off.
Customer replies
'reopen_on_customer_reply' => (bool) env('ODDEN_SERVICE_REOPEN_ON_CUSTOMER_REPLY', true),When true (the default), a customer reply by email, on the portal, or in the chat widget reopens a Resolved or Closed ticket: the status becomes WaitingOnAgent, resolved_at and closed_at are cleared, and the SLA clock restarts. When false, the reply is added and the status is left alone. Either way, a reply by email to a merged ticket is posted on its primary ticket; see Replies to merged tickets for the portal and chat rules. See Statuses.
Support portal
'portal' => [
'confirmation_email' => (bool) env('ODDEN_SERVICE_PORTAL_CONFIRMATION_EMAIL', false),
],Whether a ticket submitted on the support portal emails the submitter TicketCreatedNotification. Off by default, because the form is public and doesn't verify the email address. A submission whose address matches an existing contact is never linked to that contact or emailed; agents get an internal note to confirm who they are talking to.
Chat widget
'chat' => [
'confirmation_email' => (bool) env('ODDEN_SERVICE_CHAT_CONFIRMATION_EMAIL', false),
],Whether a ticket started from the chat widget emails the visitor TicketCreatedNotification. Off by default, because the chat endpoint is public and doesn't verify the email address. Chat tickets are routed and logged on the contact's timeline either way.
Notifications
'notifications' => [
'connection' => env('ODDEN_SERVICE_NOTIFICATIONS_CONNECTION'),
'queue' => env('ODDEN_SERVICE_NOTIFICATIONS_QUEUE'),
],The ticket notifications are queued (ShouldQueue) on this connection and queue. null uses your default queue connection (QUEUE_CONNECTION) and its default queue. Run a worker for the queue, for example php artisan queue:work --queue=support-mail,default when ODDEN_SERVICE_NOTIFICATIONS_QUEUE=support-mail. With the sync connection, notifications are sent during the request.
Deliverability
'deliverability' => [
'skip_bounced' => env('ODDEN_SERVICE_SKIP_BOUNCED', true),
'skip_invalid' => env('ODDEN_SERVICE_SKIP_INVALID', false),
],Customer ticket email (the confirmation, agent replies and the satisfaction survey) is not sent to an address a message has bounced for good to. The ticket and the reply are still saved, and an internal note on the ticket says the email was not sent and why, so an agent can reach the customer another way. skip_bounced turns this off. skip_invalid also stops email to an address Core found invalid without a bounce (no mail server, not an address); it is off because that is a guess, and ticket email is something the customer asked for. An unsubscribe from marketing never stops ticket email.
Service only reads Core's email status. With Marketing installed (through the Pro package), the suppression list is honoured too: a hard bounce, a run of soft bounces, or a spam complaint stops ticket email. To use another source, rebind Odden\Service\Contracts\ChecksRecipientDeliverability.
Routes
The web group (no prefix by default):
| Method | URI | Name | Throttle | Page |
|---|---|---|---|---|
| GET | /help |
odden.help.index |
Knowledge base | |
| GET | /help/{slug} |
odden.help.show |
||
| POST | /help/{slug}/vote |
odden.help.vote |
odden-public |
|
| GET | /support |
odden.support.create |
Support portal | |
| 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 |
CSAT | |
| POST | /support/rate/{token} |
odden.support.submitRating |
odden-public |
The api group (prefix api/service by default):
| Method | URI | Name | Throttle | CSRF | Auth |
|---|---|---|---|---|---|
| POST | /inbound-email |
odden.service.inbound-email |
odden-api |
Exempt | API token |
| GET | /knowledge/suggest |
odden.service.knowledge.suggest |
|||
| POST | /knowledge/deflect |
odden.service.knowledge.deflect |
odden-public |
Exempt | |
| POST | /chat/start |
odden.service.chat.start |
odden-public |
Exempt | |
| POST | /chat/{token}/message |
odden.service.chat.message |
odden-public |
Exempt | |
| GET | /chat/{token}/messages |
odden.service.chat.messages |
POST routes in the web group keep CSRF protection; the bundled forms include the token.
Rate limits
The throttled routes use Core's limiters, keyed by IP address and module:
odden-public:odden-core.rate_limits.public, default 30 requests per minute.odden-poll:odden-core.rate_limits.poll, default 120 requests per minute.odden-api:odden-core.rate_limits.api, default 600 requests per minute.
Each limiter keeps one counter per IP for each Odden module, shared by that module's routes that use it. A visitor who sends chat messages, votes on articles, and submits the support form is counted once against the same 30 per minute for the service module, but marketing or sales routes have their own counters. If many customers reach your app through one proxy address, configure trusted proxies as described in Rate limits.
GET /chat/{token}/messages, which the chat widget polls every 4 seconds, and GET /knowledge/suggest use the separate odden-poll limiter (120 per minute by default).
Using your own routes
Set ODDEN_SERVICE_ROUTES_ENABLED=false to stop the package from registering any routes, then register the ones you want in your app. Keep the route names: the models, notifications, actions, and bundled views generate links with them.
| Name | Used by |
|---|---|
odden.support.show |
Ticket::getPortalUrl(), customer emails, portal redirects |
odden.support.rate |
Ticket::getCsatUrl(), the resolution email |
odden.help.show |
DeflectTicketAction result URLs |
odden.help.index, odden.help.vote, odden.support.create, odden.support.store, odden.support.reply, odden.support.submitRating |
The bundled views |
odden.service.knowledge.suggest, odden.service.knowledge.deflect |
The support form's suggestion script |
The simplest starting point is the package's own routes/web.php. This example puts the help center behind your app's login and moves the email webhook to a different path:
// routes/web.php
use Odden\Core\Http\Middleware\RequireApiToken;
use Odden\Core\Support\CsrfExemption;
use Odden\Service\Http\Controllers\HelpCenterController;
use Odden\Service\Http\Controllers\InboundEmailWebhookController;
use Illuminate\Support\Facades\Route;
Route::middleware('auth')->group(function (): void {
Route::get('/help', [HelpCenterController::class, 'index'])->name('odden.help.index');
Route::get('/help/{slug}', [HelpCenterController::class, 'show'])->name('odden.help.show');
Route::post('/help/{slug}/vote', [HelpCenterController::class, 'vote'])
->middleware('throttle:odden-public')
->name('odden.help.vote');
});
Route::post('/webhooks/support-email', InboundEmailWebhookController::class)
->withoutMiddleware(CsrfExemption::middleware())
->middleware([RequireApiToken::class.':odden-service.api.token,query', 'throttle:odden-api'])
->name('odden.service.inbound-email');Routes in routes/web.php already run the web middleware group. CsrfExemption::middleware() returns the CSRF middleware classes that exist in your Laravel version (ValidateCsrfToken, and PreventRequestForgery on Laravel 13), so the webhook is exempt on both. You still need to register the support portal routes if customers will follow the links in their emails.
Migrations
The migrations load automatically. To edit them before running, publish them:
php artisan vendor:publish --tag=odden-service-migrationsThey create the six tables above. They require Core's contacts and companies tables and your users table, so run them after Core's migrations.
Demo data
Odden\Service\Database\Seeders\ServiceDatabaseSeeder creates sample SLA policies, articles, canned responses, routing rules, and tickets. It also creates three users (admin@odden.test, alex.mercer@odden.test, beth.caldwell@odden.test) with the password password, and attaches tickets to existing contacts and companies. Use it only in local environments.
php artisan db:seed --class="Odden\Service\Database\Seeders\ServiceDatabaseSeeder"