Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Api Laravel Laravel Package

helpscout/api-laravel

A Laravel integration for the Help Scout API, providing configuration, a service provider, and a facade for working with mailboxes, conversations, customers, and other Help Scout resources from your PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

This package is a thin Laravel bridge over the official helpscout/api PHP SDK. It does three things and nothing more: boots a service provider, binds HelpScout\Api\ApiClient into the container, and exposes a facade. Everything you can do with it you can do with the SDK — so the first place to look is the SDK's docs, not this package's.

1. Install and publish

composer require helpscout/api-laravel
php artisan vendor:publish --provider="HelpScout\Laravel\HelpScoutServiceProvider"

(Confirm the exact provider FQCN from the package's composer.json — the package predates Laravel's auto-discovery conventions in places, and with ~9 stars you should not assume the README is current. If auto-discovery doesn't fire, register the provider manually.)

2. Credentials

Create an app in Help Scout under Your Profile → My Apps and take the client ID/secret. Then:

HELPSCOUT_CLIENT_ID=xxxxxxxx
HELPSCOUT_CLIENT_SECRET=xxxxxxxx
// config/helpscout.php
return [
    'client_id'     => env('HELPSCOUT_CLIENT_ID'),
    'client_secret' => env('HELPSCOUT_CLIENT_SECRET'),
];

3. First call — prove auth works

php artisan tinker
>>> app(HelpScout\Api\ApiClient::class)->mailboxes()->list();

If that returns mailboxes, you're done. If it throws an auth error, the problem is the token exchange (client credentials grant against https://api.helpscout.net/v2/oauth2/token), not Laravel.

First real use case: pull a mailbox's open conversations and dump them to a command.

$client = app(\HelpScout\Api\ApiClient::class);
$page = $client->conversations()->list(['mailbox' => 12345, 'status' => 'open']);
foreach ($page->getItems() as $conversation) {
    dump($conversation->getId(), $conversation->getSubject());
}

Note the mailbox filter is required for most conversation queries — Help Scout will reject unscoped list calls.

Implementation Patterns

Wrap the client in your own service

The container binding is a singleton around a stateful HTTP client with an expiring token. Don't spray app(ApiClient::class) across controllers; wrap it, and give yourself a seam to fake later.

namespace App\Services;

use HelpScout\Api\ApiClient;

class HelpScoutService
{
    public function __construct(private ApiClient $client) {}

    public function createTicket(string $email, string $subject, string $body): int
    {
        $customer = $this->client->customers()->create(
            (new \HelpScout\Api\Customers\Customer())
                ->setFirstName('Website')
                ->setLastName('Visitor')
                ->setEmails([['type' => 'work', 'value' => $email]])
        );

        $conversation = (new \HelpScout\Api\Conversations\Conversation())
            ->setMailboxId((int) config('helpscout.mailbox_id'))
            ->setType('email')
            ->setSubject($subject)
            ->setCustomer($customer);

        $thread = (new \HelpScout\Api\Conversations\Threads\CustomerThread())
            ->setCustomer($customer)
            ->setText($body);

        $conversation->setThreads([$thread]);

        return $this->client->conversations()->create($conversation)->getId();
    }
}

Register it as a singleton alongside the binding, then type-hint HelpScoutService in controllers/jobs.

Always go through the queue for writes

Conversation creation touches customer lookup, conversation create, and thread create — three round trips that can partially fail. Push it onto a job with $tries = 3 and backoff(); a duplicate ticket is far better than a 500 in the user's face after a network blip.

class SyncTicketToHelpScout implements ShouldQueue
{
    use InteractsWithQueue, Queueable;
    public int $tries = 3;
    public function backoff(): array { return [10, 60, 300]; }
}

Use the facade for terse one-liners

Where the binding is registered you can do HelpScout::conversations()->get($id) if the alias ships in config/app.php. It's the same singleton underneath, so there's no behavioural difference — but be consistent. Mixing HelpScout:: and app(ApiClient::class) in one codebase makes grepping impossible.

Cache the reference data

Mailboxes and users change almost never. Cache::remember('helpscout.mailboxes', 3600, fn () => $client->mailboxes()->list()) will remove an API call from nearly every request path and, more importantly, remove it from your rate budget.

Paginate deliberately

Help Scout list endpoints are page-numbered, not cursor-based. A naive while (true) loop will page forever if you aren't reading the returned pagination metadata:

$page = 1;
do {
    $result = $client->conversations()->list($filters + ['page' => $page]);
    foreach ($result->getItems() as $c) { /* ... */ }
    $page++;
} while ($result->getPagination() && $page <= $result->getPagination()->getTotalPages());

For a full backfill, drive it from a scheduled command with a stored high-water mark, not from a request.

Receiving webhooks

Help Scout posts to a URL you register in the Help Scout UI — this package gives you nothing here. Verify the X-HelpScout-Signature header (base64 HMAC-SHA1 of the raw body with your webhook secret) before decoding JSON, then dispatch a job:

$expected = base64_encode(hash_hmac('sha1', $request->getContent(), config('helpscout.webhook_secret'), true));
abort_unless(hash_equals($expected, $request->header('X-HelpScout-Signature')), 403);

Webhooks are the right answer for "react to a customer reply" — polling conversations is the wrong one.

Testing

Because the binding is a plain class, you can swap it:

$this->mock(HelpScoutService::class, fn ($m) => $m->shouldReceive('createTicket')->once()->andReturn(99));

If you go through the facade, HelpScout::shouldReceive('conversations->create') works but is brittle; the mocked chain has to match every call. Prefer mocking your own wrapper.

Gotchas and Tips

The singleton holds a token that expires. OAuth client-credentials tokens from Help Scout are short-lived (two hours). A Horizon worker that keeps a resolved ApiClient alive for a long time will eventually hit a 401. Either resolve the client per job (app(ApiClient::class) inside handle() rather than in the constructor), or catch the auth exception at the top of your job and re-resolve. This is the single most common production failure with this wrapper.

env() inside config is fine; env() inside your service is not. If anyone adds env('HELPSCOUT_CLIENT_ID') to a service class, config:cache will silently hand you null. Keep all env reads in config/helpscout.php.

Know the exception taxonomy before you retry anything. The SDK throws HelpScout\Api\Exception\* types — you care about the distinction between a 4xx validation problem (retrying is pointless, alert and drop) and a 429/5xx (retry with backoff). Catch \Throwable and inspect the status code on the response rather than relying on a generic retry. Help Scout returns useful validation detail in the body; log it, because the exception message alone is thin.

Respect the rate limit. The Mailbox API enforces per-app limits and the SDK does not back off for you. When you see X-RateLimit-Remaining dropping near zero, or your first 429, slow down — RateLimiter::for() in Laravel keyed on the mailbox ID is a clean place to enforce it. Bulk imports without throttling will get you temporarily blocked.

mailboxId must be an integer. Passing the string from an env var straight through produces a confusing validation error. Cast at the config boundary.

Attachments are a two-step dance. Upload via the attachments endpoint, get an attachment reference, then include it in the thread. You can't inline base64 in the conversation create call — plan for the extra round trip and handle orphans.

Timestamps are UTC ISO-8601 in and out. If your app stores local time, convert before setting setCreatedAt(); Help Scout will accept the value and you'll spend an afternoon wondering why thread ordering looks wrong.

No sandbox. There's no test tenant. Use a dedicated development mailbox and guard write paths with an if (app()->isProduction()) — or better, a config flag — before you email real customers.

Thin wrapper means no escape hatch. There's no way to configure Guzzle timeouts or middleware through this package. If you need custom HTTP behaviour (timeouts are the usual one — the default is effectively "wait forever"), skip the package entirely and construct ApiClient yourself in a service provider. You lose the facade and gain control; at ~9 stars and zero dependents, that's a reasonable trade, and it means an abandoned package isn't your problem.

Treat the SDK version as the real dependency. Pin helpscout/api in composer.json and read its CHANGELOG when bumping — method names (threads(), create(), the thread subclasses) shift between SDK majors, and this wrapper adds no abstraction to absorb that.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
adachsoft/php-code-style