sun-asterisk/chatwork-php
A PHP client for the Chatwork API that lets PHP and Laravel applications send messages, manage rooms, files, tasks, and contacts through simple, typed methods. Lightweight and easy to configure, it is ideal for automating team notifications and Chatwork workflows.
Install and wire it up. The package is a thin Guzzle-based wrapper around the Chatwork REST API v2 — no Laravel service provider, no config file, no facade ships with it. Expect to spend ten minutes on glue code:
composer require sun-asterisk/chatwork-php
// config/services.php
'chatwork' => [
'token' => env('CHATWORK_API_TOKEN'),
'room' => env('CHATWORK_DEFAULT_ROOM_ID'),
],
Bind it as a singleton so you're not re-instantiating a Guzzle stack on every resolution:
// app/Providers/AppServiceProvider.php
$this->app->singleton(\App\Services\Chatwork\Client::class, fn () => new \App\Services\Chatwork\Client(
config('services.chatwork.token')
));
Where to look first. Read src/ before the README. The surface area is small (a client class plus resource helpers), and the method names are the only documentation you'll get for less-travelled endpoints. Then keep the Chatwork API v2 reference open — the package mirrors the endpoints one-to-one, so the official docs are the package docs. Method names and namespaces vary between minor versions, so confirm the entry-point class in vendor/sun-asterisk/chatwork-php/src before you write call sites.
Get a token. Chatwork → Account Settings → API Token. Critical constraint up front: the token acts as the user who issued it. GET /rooms and GET /contacts only return rooms and people that user can see. A bot account is usually the right call — create a dedicated Chatwork user, add it to the rooms you care about, issue the token there.
First use case: post to a room. This is 90% of what anyone does with this package — deploy notices, error alerts, form submissions:
$client->post("rooms/{$roomId}/messages", [
'body' => "[info][title]Deploy[/title]v1.4.2 shipped to production[/info]",
]);
If that works, you're done with the hard part. Everything below is about not shooting yourself in the foot.
Wrap it, don't sprinkle it. Because the package is untyped and unopinionated, define your own narrow interface and let the package be an implementation detail. This is what makes the thing testable and swappable (important for a 22-star package with zero dependents — you may end up forking it):
interface ChatworkNotifier
{
public function message(string $roomId, string $body): void;
public function members(string $roomId): array;
}
Custom notification channel. If you already use Laravel notifications, a chatwork channel is the cleanest integration and keeps message formatting out of your domain code:
public function via($notifiable) { return [ChatworkChannel::class]; }
public function toChatwork($notifiable): array
{
return ['room_id' => $notifiable->chatwork_room_id, 'body' => "Build #{$this->build} failed"];
}
Always queue outbound messages. HTTP calls to Chatwork from a request cycle will eventually bite you — the API is slow-ish and rate-limited. ShouldQueue on the notification/job, plus retryUntil/backoff, gives you resilience for free:
public int $tries = 3;
public array $backoff = [5, 30, 120]; // respect 429s
Error reporting hook. The highest-leverage integration for a Laravel team is routing exception reports to an ops room. In bootstrap/app.php (Laravel 11+) or App\Exceptions\Handler, call your notifier inside report() — guarded with app()->bound() so it never cascades during a boot failure.
Formatting helper. Chatwork has its own markup, and hand-rolling it inline gets ugly fast. Centralize it:
final class ChatworkMarkup
{
public static function mention(int $accountId): string { return "[To:{$accountId}]"; }
public static function info(string $title, string $body): string { return "[info][title]{$title}[/title]{$body}[/info]"; }
}
Resolve mention IDs from GET /rooms/{room_id}/members once and cache them (Cache::remember("chatwork.members.{$roomId}", 3600, ...)) — members change rarely and the endpoint consumes rate limit.
Polling, if you need inbound. Chatwork has no webhooks in API v2. If you must react to incoming messages, a scheduled command hitting GET /rooms/{id}/messages is the only option — store the last processed message_id per room in the database or cache and process strictly forward. Never poll more often than your rate budget allows.
Multi-tenant / multi-room. Don't fight the singleton. Bind a factory that returns a client per token, and key config by tenant. The package holds no global state, so multiple instances are safe.
Testing. Point your wrapper's interface at a fake in tests and keep the package out of the test suite entirely. If you genuinely need to test the HTTP layer, inject a Guzzle MockHandler into the underlying client (the package accepts a Guzzle client/handler stack in most versions) — far more reliable than hitting the network in CI.
Form-encoded, not JSON. Chatwork's API expects application/x-www-form-urlencoded bodies (multipart for file uploads). If you pass an array and get a 400, check whether the package is encoding it correctly — some versions require you to pass a pre-built query string. This is the single most common source of confusion.
204 means empty body. Many mutating endpoints return 204 No Content. Naive json_decode($response->getBody()) returns null, and if the package doesn't guard for it you'll get type errors or warnings. Check the status code before parsing.
Rate limits are real and low. Budget roughly 100 requests/minute per token and handle 429 explicitly — exponential backoff, not immediate retry. Chatwork returns rate-limit headers; log them so you can see throttling before users do. Fan-out to many rooms in one job is a classic way to trip this.
Token scope surprises. "Room not found" almost always means the token's owner isn't in that room, not that the ID is wrong. Verify with GET /rooms when debugging 404s.
self_unread. By default, messages your token posts are marked read for the poster. Pass self_unread=1 if you want the sender (a human account) to actually see the notification.
Mention syntax needs account IDs, not names. [To:123] is an account ID from /contacts or /rooms/{id}/members. Get it wrong and you get a silent, non-notifying message — no error.
Message ordering and pagination. GET /rooms/{id}/messages returns a time-ordered slice, not a cursor-paginated feed. For polling, use the force parameter and dedupe on message_id; for backfill, page backwards by ID yourself.
Logging leaks tokens. If you enable Guzzle request logging or dump() a client instance, the X-ChatWorkToken header goes with it. Add a scrubber or log only method + path.
Duplicate messages on retry. Queued jobs retry; your notifier doesn't know it already sent. For anything user-visible, persist an idempotency marker (job UUID → message ID) before the HTTP call and check it on retry.
Composer conflicts. It's a small package with a fixed Guzzle constraint. If Laravel's HTTP client or another SDK pins Guzzle differently, you'll get resolution failures. composer why-not first, and be prepared to fork — the repo is small enough that patching a namespace or a header is a 15-minute job.
Extension points worth knowing. Because the client ultimately hands off to Guzzle, a middleware on the handler stack is the cleanest place to add retry-on-429, request logging, or a metrics timer — better than wrapping every call site. If the version you install doesn't expose the stack, subclass the client or fork; don't reimplement the API surface yourself.
How can I help you explore Laravel packages today?