Skip to content

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:

bash
php artisan vendor:publish --tag=odden-service-config

Settings 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_REOPEN_ON_CUSTOMER_REPLY odden-service.reopen_on_customer_reply true
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)

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

php
'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

php
'defaults' => [
    'priority' => 'medium',
    'source' => 'web_portal',
    'prefix' => 'TICK',
],

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.

priority and source are not read by the package. A new ticket's defaults come from the model and the database columns (medium and web_portal) and from the arguments you pass to CreateTicketAction.

Routes

php
'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' => ['web'],
    ],
],

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.

Both groups use the web middleware group by default. The browser-facing chat and deflection endpoints and the email webhook remove Laravel's CSRF middleware themselves, so they work from other sites and servers. If you set the api group's middleware to ['api'], sessions are no longer started for widget requests; nothing in the API endpoints depends on the session.

Generate links with route() and the route names below, so prefix and domain changes are picked up.

API token

php
'api' => [
    'token' => env('ODDEN_SERVICE_API_TOKEN'),
],

Protects the inbound email webhook. Until it is set, that endpoint returns 403.

Inbound email

php
'inbound_email' => [
    'require_authenticated_sender' => (bool) env('ODDEN_SERVICE_INBOUND_REQUIRE_AUTH', false),
],

When 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.

Customer replies

php
'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 Open and resolved_at and closed_at are cleared. 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.

Chat widget

php
'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

php
'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.

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 two limiters, both keyed by IP address:

  • odden-public: odden-core.rate_limits.public, default 30 requests per minute.
  • odden-api: odden-core.rate_limits.api, default 600 requests per minute.

Each limiter has one counter per IP shared by every route that uses it, including routes in other Odden modules. A visitor who sends chat messages, votes on articles, and submits the support form is counted once against the same 30 per minute. 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 are not throttled.

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:

php
// 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', '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:

bash
php artisan vendor:publish --tag=odden-service-migrations

They 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.

bash
php artisan db:seed --class="Odden\Service\Database\Seeders\ServiceDatabaseSeeder"