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

Zendesk Api Client Php Laravel Package

zendesk/zendesk_api_client_php

A PHP client for the Zendesk API that simplifies authentication and CRUD operations for tickets, users, organizations, and other support resources. It offers an object-oriented wrapper with pagination support for integrating Zendesk workflows into PHP applications.

Deep Wiki
Context7

Getting Started

Install

composer require zendesk/zendesk_api_client_php

First contact — the client is a thin, resource-oriented wrapper over https://{subdomain}.zendesk.com/api/v2/. You construct one client and then call resource methods on it:

use Zendesk\API\HttpClient as ZendeskAPI; // alias: collides with Guzzle's HttpClient

$client = new ZendeskAPI('your-subdomain');

// API token (email + token) — the library appends the required "/token" suffix for you
$client->setAuth('basic', [
    'username' => config('services.zendesk.email'),
    'token'    => config('services.zendesk.token'),
]);

// or OAuth
$client->setAuth('oauth', ['token' => $accessToken]);

Prove it works with one read:

$tickets = $client->tickets()->findAll(['per_page' => 5]);

foreach ($tickets as $ticket) {
    dump($ticket->id, $ticket->subject, $ticket->status);
}

Where to look first

  1. src/Zendesk/API/Resources/Core/ — one class per endpoint family (Tickets, Users, Organizations, Attachments, Search, Views, Macros, Triggers…). This is the API surface; read it before reading docs.
  2. src/Zendesk/API/Traits/Resource/ — Find, FindAll, Create, Update, Delete, CreateMany, UpdateMany, DeleteMany. Knowing which traits a resource uses tells you exactly which verbs and payload shapes it supports.
  3. src/Zendesk/API/HttpClient.php — how the base URL, auth headers and Guzzle client are assembled.

First real use case: inbound support form → $client->tickets()->create([...]), then attach the ticket ID to your local record. That single flow exercises auth, error handling, custom fields and comments, which is 80% of what you'll ever do with this package.

Implementation Patterns

1. Bind it once (Laravel service provider)

There is no service provider — you write it. Bind a singleton so the Guzzle connection and auth setup are reused per worker:

// config/services.php
'zendesk' => [
    'subdomain' => env('ZENDESK_SUBDOMAIN'),
    'email'     => env('ZENDESK_EMAIL'),
    'token'     => env('ZENDESK_TOKEN'),
],

// AppServiceProvider::register()
$this->app->singleton(\Zendesk\API\HttpClient::class, function () {
    $c = new \Zendesk\API\HttpClient(config('services.zendesk.subdomain'));
    $c->setAuth('basic', [
        'username' => config('services.zendesk.email'),
        'token'    => config('services.zendesk.token'),
    ]);
    return $c;
});

Then inject the interface you actually depend on in your own code (ZendeskGateway), not the vendor client directly — see testing below.

2. Ticket creation — the canonical payload

$ticket = $client->tickets()->create([
    'subject'  => $subject,
    'requester' => ['name' => $name, 'email' => $email],  // or 'requester_id'
    'priority' => 'normal',
    'type'     => 'question',
    'tags'     => ['web-form', 'billing'],
    'custom_fields' => [
        ['id' => 12345678, 'value' => 'enterprise'],
    ],
    'comment' => [
        'body'     => $body,
        'public'   => true,          // false => internal note
        'uploads'  => [$token],      // see attachments below
    ],
]);

Resolve custom-field keys to IDs once via $client->ticketFields()->findAll() and cache them — Zendesk will silently ignore unknown keys and you'll spend an afternoon debugging "why isn't my field set".

3. Attachments: two-step, token-based

$upload = $client->attachments()->upload([
    'file'     => storage_path("app/{$path}"),   // path or SplFileInfo
    'filename' => 'invoice.pdf',
]);

$client->tickets()->update($ticketId, [
    'ticket' => [
        'comment' => [
            'body'    => 'Invoice attached.',
            'uploads' => [$upload->upload->token],
        ],
    ],
]);

Uploads are a separate host (*.zendesk.com/api/v2/uploads.json), return a short-lived single-use token, and must be attached to a comment or they evaporate. Upload first, then create the ticket/comment in the same job — don't persist tokens for later.

4. Bulk operations

$client->tickets()->createMany([ /* up to 100 ticket arrays */ ]);

$client->tickets()->deleteMany(['ids' => [1, 2, 3]]);

createMany is capped at 100 objects per request and is not atomic — a partial success returns a job status. For anything larger, chunk in your own code and inspect per-item results. Check Traits/Resource/UpdateMany.php for the exact ids + attribute payload shape before shipping; it differs subtly from create/update.

5. Reading efficiently: side-loading and pagination

$tickets = $client->tickets()->findAll([
    'include'  => 'users,organizations', // avoids N+1 lookups
    'per_page' => 100,                   // 100 is the hard max
]);

foreach ($tickets as $ticket) { /* ... */ }

For sync jobs and reports, use cursor pagination instead of page — page-based pagination degrades badly and the offset caps out. Pass raw query params and follow the cursor yourself:

$params = ['page[size]' => 100, 'sort_by' => 'id', 'sort_order' => 'asc'];
$params['page[after]'] = $cursor; // from the previous response's meta/links

Store the highest seen id / updated_at in your DB so the next run is incremental. For very large accounts, prefer the export endpoints over tickets()->findAll().

6. Search as your lookup primitive

$client->search()->find('type:user email:[email protected]');
$client->search()->find('type:ticket status<solved tags:billing');

Search caps at ~1000 results per query — it's for lookup, not for export. Also remember search indexing lags writes by seconds: don't create-then-immediately-search to confirm.

7. Views, macros, Help Center

  • $client->helpCenter()->articles()->findAll(), ->create([...]) — note the extra helpCenter() namespace; Help Center models require permission_group_id and a locale.
  • $client->views()->findAll() / $client->macros()->findAll() to enumerate automation rather than hard-coding IDs.
  • Applying a macro: POST to /macros/{id}/apply — if the resource method isn't exposed, see custom resources below.

8. Custom resources for unwrapped endpoints

The resource classes are small and composable. When the client lacks a method, don't drop to raw curl:

use Zendesk\API\Resources\ResourceAbstract;
use Zendesk\API\Traits\Resource\FindAll;

class MyReports extends ResourceAbstract
{
    use FindAll;

    protected $resourceName = 'reports';
    protected $objectName   = 'reports';
}

Instantiate with the client and reuse the built-in _get/_post/_put helpers so auth, JSON encoding and error handling stay consistent.

9. Job design for rate limits and retries

Zendesk rate-limits per user/bucket and returns 429 with a Retry-After header. The package does not retry for you. Wrap calls in a gateway with backoff, and in Laravel put a Redis::throttle('zendesk') middleware (or RateLimited job middleware) in front of the queue worker:

try {
    $client->tickets()->create($payload);
} catch (\Zendesk\API\Exceptions\ApiResponseException $e) {
    if ($e->getCode() === 429) {
        $this->release($retryAfter ?? 60);
        return;
    }
    throw $e;
}

Make jobs idempotent by storing the Zendesk ticket ID locally as soon as it's returned — retries after a timeout can otherwise duplicate tickets.

10. Testing

The client builds its own Guzzle instance, so the cleanest seam is your own ZendeskGateway interface, faked in tests. For a genuine integration test, point the client at a Guzzle MockHandler if your version exposes the handler stack; otherwise hit a sandbox subdomain from a dedicated test suite, never from unit tests.

Gotchas and Tips

Auth

  • With API tokens, the classic mistake is passing password instead of token to setAuth('basic', [...]) — the library then base64-encodes email:password and you get a 401 with a cryptic body. Use the token key.
  • OAuth access tokens are not refreshed automatically. Persist refresh tokens and rebuild the client (or re-call setAuth) when you rotate.
  • A wrong subdomain often returns a redirect/HTML page rather than a JSON error; you'll see a JSON parse failure, not an auth failure. Verify the subdomain first.

Errors

  • Everything non-2xx throws Zendesk\API\Exceptions\ApiResponseException. $e->getCode() carries the HTTP status; $e->getErrorDetails() carries Zendesk's structured error / description / details — log that, not just getMessage(), or you'll be missing the field-level reasons.
  • 422 bodies contain details keyed by field ({"requester": [{"description": "..."}]}). Surface those to users or you'll get "something went wrong" bug reports.
  • Validation errors on update are per-field but the top-level message is generic — always dig into details.

Data shape

  • Empty collections may come back as a missing property rather than []. Always guard $response->tickets ?? [] or go through toArray().
  • Responses are iterable and property-accessible, but don't assume count() reflects the dataset — Zendesk's count is the total across pages, not the page size.
  • via, created_at, updated_at, and id on a create are server-set; sending them is ignored at best.
  • Timestamps must be ISO 8601 UTC. Local timezones silently shift or 422.
  • To clear a custom field, send null — omitting it means "unchanged".
  • tags is an array in the API but a comma string in some responses/webhooks; normalise on ingest.

Business rules that bite

  • A comment (or at least a comment.body) is effectively required on ticket create; attachments-only comments are finicky — include a body.
  • Closed tickets are immutable. Reopen requires a new ticket with via_followup_source_id, or an update that sets status before it locks.
  • users()->delete() is a soft delete; hard deletion needs the permanent-delete path and fails if the user has tickets. Same for organizations.
  • Attachments have a max size (typically 20 MB on most plans) and are rejected server-side with a 422 you'll only see if you log details.
  • Search index lag (seconds to minutes) makes "create then search" a flaky test pattern. Query by the returned ID instead.

Operational

  • Set an explicit timeout — Guzzle's default is no timeout and a hung Zendesk call will stall a queue worker indefinitely.
  • Log X-Zendesk-Request-Id from responses/errors; Zendesk support will ask for it.
  • Enable the client's debug mode in local/staging only; it dumps full request and response bodies including tokens and PII.
  • Cache ticketFields(), groups(), organizations() lookups — they're static-ish and easy to hammer.
  • include= is your cheapest performance win; N+1 users()->find() calls are the #1 cause of hitting rate limits.
  • The class is Zendesk\API\HttpClient, which shadows GuzzleHttp\Client in autocompletion — always alias on import.
  • If Composer resolves an old major, the namespace shifts (Zendesk\API\Client vs HttpClient) and setAuth signatures differ. Pin the version and read the CHANGELOG before upgrading across majors — this library has made breaking changes in minor releases historically.
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