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 Package

helpscout/api

A PHP SDK for the Help Scout API, offering a clean, object-oriented client for mailboxes, conversations, customers, tags, and webhooks. Easily integrate Help Scout support data into PHP or Laravel applications with an expressive, framework-friendly API.

View on GitHub
Deep Wiki
Context7

Getting Started

Require the package and create a Help Scout OAuth app (Your Apps → Create App) to get a client ID and secret. For an internal/single-account integration use the client-credentials flow; for a distributed app use the authorization-code flow.

composer require helpscout/api
// config/services.php
'helpscout' => [
    'client_id'     => env('HELPSCOUT_CLIENT_ID'),
    'client_secret' => env('HELPSCOUT_CLIENT_SECRET'),
    'mailbox_id'    => env('HELPSCOUT_MAILBOX_ID'),
],
// app/Providers/AppServiceProvider.php
use HelpScout\Api\ApiClient;
use HelpScout\Api\Auth\Token\FileTokenStore;

$this->app->singleton(ApiClient::class, fn () => ApiClient::forClientCredentials(
    config('services.helpscout.client_id'),
    config('services.helpscout.client_secret'),
    new FileTokenStore(storage_path('framework/helpscout-token.json'))
));

First use case: confirm credentials with a cheap read, then create a conversation.

$client = app(ApiClient::class);

$mailboxes = $client->mailboxes()->list(); // smoke test

$customer = (new \HelpScout\Api\Customers\Customer())
    ->setFirstName('Ada')
    ->setLastName('Lovelace');

$thread = (new \HelpScout\Api\Conversations\Thread())
    ->setType(\HelpScout\Api\Conversations\Thread::TYPE_CUSTOMER)
    ->setCustomer($customer)
    ->setText('My order never arrived.');

$conversation = (new \HelpScout\Api\Conversations\Conversation())
    ->setSubject('Where is my order?')
    ->setMailboxId((int) config('services.helpscout.mailbox_id'))
    ->setType(\HelpScout\Api\Conversations\Conversation::TYPE_EMAIL)
    ->setCustomer($customer)
    ->setStatus(\HelpScout\Api\Conversations\Conversation::STATUS_ACTIVE)
    ->addThread($thread);

$created = $client->conversations()->create($conversation);

Look first at the service methods on ApiClient (conversations(), customers(), mailboxes(), tags(), workflows(), users(), webhooks(), plus Docs entities) and the entity classes under HelpScout\Api\*\ — they are the source of truth for allowed fields and status/type constants. The README covers the two grant types and token stores; the class constants cover the API's enums.

Implementation Patterns

Bind the client as a singleton and wrap it. Constructing ApiClient performs auth/token work, so never new it per job. Put a thin HelpScoutService around it exposing your domain verbs (openConversation, addNote, tagConversation). That gives you one place to fake in tests and one place to absorb breaking changes from this small package.

Entities are mutable DTOs. Build Customer, Thread, Conversation, Tag, etc., set fields, and pass them to the service. Re-fetch before updating: the API's updates are effectively full-resource writes, so get() → mutate → update() is the safe loop.

$conversation = $client->conversations()->get($id);
$conversation->setStatus(Conversation::STATUS_CLOSED);
$client->conversations()->update($conversation);

Adding replies/notes follows the same entity pattern against the conversation's thread endpoint — create a Thread with TYPE_NOTE or TYPE_REPLY, set createdBy/customer, and post it. Attachments are uploaded first (separate attachment call, returns an ID) and then referenced by ID on the thread.

Paginate explicitly. List calls return one page plus HAL navigation links; loop over the collection and follow the next-page link until null. Never assume list() returned everything — cap iterations and log when you hit the cap.

$page = $client->conversations()->list(['status' => 'active', 'mailbox' => $mailboxId]);
while ($page !== null) {
    foreach ($page as $conversation) { /* ... */ }
    $page = $page->getNextPage(); // null on the last page
}

Webhook ingestion. Receive the POST, persist the raw payload, and return 204 immediately; process in a queued job. Treat webhooks as notifications, not data — use the IDs in the payload to get() the fresh conversation/customer. Make the job idempotent on conversation/thread ID because retries and duplicate deliveries are normal.

Rate-limit-aware jobs. Wrap outbound calls in a job middleware or a retry helper that releases with backoff on 429 (honor Retry-After). Batch tagging, backfills, and report pulls should be chunked artisan commands, not synchronous request-path work.

Testing. Mock your own HelpScoutService in feature tests. For the wrapper itself, inject a Guzzle handler with MockHandler into the underlying RestClient (check the client's constructor/factory for the injection point) and assert on the hydrated entities. Mocking ApiClient's fluent service chain directly is brittle.

Configuration/env. Keep mailbox IDs, tags, and workflow IDs in config, not in code — they differ per Help Scout account and per environment.

Gotchas and Tips

  • No Laravel auto-discovery. There is no facade or service provider; all wiring is manual. Add the binding and publish nothing.
  • FileTokenStore is per-server. It writes a token file to local disk. On multi-instance deployments each node will re-authenticate and potentially thrash tokens. Implement the package's TokenStore interface against cache/DB, or store tokens in a shared volume. This is the single most common production bug with this package.
  • Authorization-code flow needs refresh handling. The access token expires; the grant must refresh it. Verify your token store persists the refresh token, and test expiry by forcing a stale token.
  • Client-credentials tokens are short-lived. They are cached by the token store, so a new token per request usually means the store is misconfigured (unwritable path, wrong cache key, container recreated each request).
  • 429s are silent killers in bulk jobs. Help Scout enforces per-minute limits and returns Retry-After. Without retry/backoff you will see sporadic failures in backfills and webhook fan-out.
  • Updates can clobber data. Because PUT-style updates send the whole resource, updating a stale entity can overwrite fields set elsewhere (e.g., an agent changing status in the UI). Re-fetch inside the same job, keep the mutation window small.
  • Threads are append-only. You cannot edit or delete an individual thread; mistakes are visible in the conversation forever. Staging mailbox or a dry-run mode is worth the setup.
  • Attachments are two-step and size-limited. Upload the file, capture the returned attachment ID, then reference it on the thread. Do not pass file paths or base64 blobs into thread payloads.
  • Dates: entities use DateTime objects and the API expects ISO-8601. Timezone drift between app and Help Scout is a regular source of "wrong day" bugs in report filters.
  • HAL/hydration surprises. The API returns HAL+JSON with _embedded; unknown or newly added fields may be dropped on hydration. If a field silently disappears, check whether the entity has a setter — upgrade or subclass.
  • Error surfacing is thin. Catch the package's HTTP exceptions, log the response body and status, and map 401 (bad/expired token), 403 (app not authorized for mailbox), 404 (deleted conversation), and 422 (validation) distinctly. Don't let a generic exception hide a 422 payload that tells you which field is wrong.
  • Webhook verification. If you configure a secret, verify the signature header over the raw request body before parsing JSON; Laravel's $request->getContent() is the raw body to use.
  • Small ecosystem, few dependents (0). Pin an exact version in composer.json, read the source when behavior is unclear, and keep the wrapper interface small. You are effectively the maintainer of the integration layer.
  • Extension points worth knowing: the TokenStore interface (shared/Redis tokens), the underlying Guzzle client (logging middleware, retries, MockHandler in tests), and entity subclasses (for fields the package hasn't exposed yet). Debugging is easiest by adding a Guzzle log middleware and dumping the last request/response around a single API call.
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.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource