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

Zoho Laravel Package

asciisd/zoho

A Laravel package for integrating Zoho Books into PHP applications, offering a fluent API wrapper, OAuth token handling, configuration, and convenient access to Zoho Books resources from your Laravel app.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install and publish config

    composer require asciisd/zoho
    php artisan vendor:publish --provider="Asciisd\Zoho\ZohoServiceProvider"
    

    This drops config/zoho.php into your project. Open it first — everything else keys off these values.

  2. Create a Zoho OAuth client in the Zoho API Console. Pick "Server-based Applications" and set the redirect URI to whatever you configure as ZOHO_REDIRECT_URI (commonly https://your-app.test/zoho/callback). Grant at minimum: ZohoCRM.modules.ALL, ZohoCRM.users.READ, ZohoCRM.settings.READ.

  3. Populate .env

    ZOHO_CLIENT_ID=
    ZOHO_CLIENT_SECRET=
    ZOHO_REDIRECT_URI="${APP_URL}/zoho/callback"
    [email protected]
    

    The ZOHO_CURRENT_USER_EMAIL value is what the package uses to persist a token per user — don't skip it, or auth will "succeed" and then every call returns 401.

  4. Authorize once. Visit the package-provided authorization route, complete the Zoho consent screen, and let the callback persist the refresh token. Tokens are stored via the configured ZohoStore (a CacheStore or DB-backed store — check config('zoho.storage')). From here on, the package refreshes access tokens automatically.

First use case: the canonical one is lead capture. After auth, pushing a form submission into CRM is a two-liner:

use Asciisd\Zoho\Facades\Zoho;

Zoho::leads()->create([
    'Last_Name'   => $request->last_name,
    'Company'     => $request->company,
    'Email'       => $request->email,
    'Lead_Source' => 'Web Form',
]);

If that returns a record ID, you're wired up correctly and can start building on top of it.

Implementation Patterns

Module facades — the primary API surface. Every Zoho module is reachable as Zoho::leads(), Zoho::contacts(), Zoho::accounts(), Zoho::deals(), Zoho::tasks(), etc. Each module object exposes the same verb set: create(array), update($id, array), get($id), search($criteria), delete($id), all(). Learn one, you know them all.

Read-back after create. Zoho's create response is thin (mostly id + code). If you need the full record, follow up with ->get($id). It's a second HTTP call, but it's usually cleaner than reading fields off the create payload.

Search syntax is Zoho's, not Eloquent's. Zoho::contacts()->search('Email:equals:[email protected]') — the three-part field:operator:value string is the CRM search API, not a fluent builder. Wrap this in your own repository/action class so the rest of your code doesn't speak Zoho.

Wrapping in a service layer. Don't call the facade from controllers on anything bigger than a demo. A ZohoLeadService with createFromRequest(), updateFromModel(), and mapping methods lets you swap the transport (or mock it) later:

class ZohoLeadService {
    public function push(Lead $lead): string {
        return Zoho::leads()->create($this->map($lead))['id'];
    }
}

Syncing Laravel models to CRM. Common pattern: keep a zoho_id column on your Eloquent models, use an observer (created → create in Zoho, updated → update) with a withoutEvents guard to avoid recursion. Store the returned ID before dispatching anything downstream.

Jobs over inline calls. Every Zoho call is a network round-trip with rate limits. Wrap push/pull operations in queued jobs (ShouldQueue, sensible tries and backoff) so a CRM outage doesn't take down a webhook or registration flow.

Inbound sync via webhooks. Zoho sends webhook payloads as form-encoded application/x-www-form-urlencoded POSTs, often with a ?secret= query param you set on the Zoho side. Add a route, validate the secret in middleware, and dispatch a job to reconcile into your local tables.

Bulk-ish workflows. There's no batch endpoint exposed here — loop and queue. Rate limits are per-user and per-org; staggering jobs with delay() per item is more reliable than one big job that sleeps between calls (which eats a worker).

Caching user/module metadata. Zoho::users() and module metadata change rarely. Cache them for the day; you'll save dozens of calls an hour on a busy app.

Gotchas and Tips

OAuth is per-user, not global. The package expects ZOHO_CURRENT_USER_EMAIL (or an equivalent context) to resolve which token to use. In multi-tenant apps you'll want to set this per-request — often via a Zoho::withUser($email) call or by swapping the config at runtime. Forgetting this is the #1 cause of "works in tinker, 401 in production."

Refresh tokens expire. Zoho invalidates refresh tokens that go unused for ~60 days and revokes them on certain admin actions ("revoke all tokens" in the console, password change, org policy). Build a health check — a scheduled job that hits Zoho::users()->get(...) and alerts on 401 — rather than discovering the failure when a customer's lead doesn't appear.

Redirect URI must match exactly. Trailing slash, http vs https, localhost vs 127.0.0.1 — all matter. Use the fully-qualified APP_URL in your env, not a hardcoded string.

Data Center is not automatic. Zoho has regional data centers (.com, .eu, .in, .com.au, .jp). If your org is in the EU and the package defaults to .com, tokens are issued but every API call 400s with INVALID_URL_PATTERN. Check what config('zoho.accounts_url') / api_url are set to, and adjust. The README does not always make this obvious.

Fields must match the CRM layout verbatim. Last_Name with an underscore and capital N is not the same as last_name. Every Zoho field also has an API name that may differ from the label your admin sees. Pull the layout (Zoho::settings()->fields(...) or just the module metadata) during development and keep a mapping constant — don't discover typos through 400s in staging.

Required-field validation is Zoho-side. The package passes your array straight through. Missing a mandatory field returns a MANDATORY_NOT_FOUND error with a details array; log that payload, don't just log $e->getMessage(), or you'll be guessing which field was missing.

Update semantics are partial-merge by default. ->update($id, ['Phone' => '...']) only touches Phone. That's usually what you want, but be careful when you intend to clear a field — send null for text fields, and know that CRM may ignore nulls for some field types (picklists especially).

Duplicate checks are enforced by CRM rules. If your org has duplicate-check enabled, create can return DUPLICATE_DATA with the existing record's ID inside the error payload. Handle this branch explicitly — treat it as "already exists" rather than a hard failure, and update that record instead.

search returns a maximum of 200 records. No pagination in the wrapped call. For anything bigger you'll need page/per_page params (if the SDK version supports it) or to loop by modified_time ranges yourself. Don't assume "search returns everything."

Rate limits are real. Zoho CRM allows on the order of ~10 requests/second per user and a daily cap tied to your plan. A naive foreach ($leads as $lead) Zoho::leads()->create(...) in a seeder will hit TOO_MANY_REQUESTS fast. Queue + throttle (RateLimiter::for('zoho')) or you'll spend an afternoon wondering why half your records are missing.

Testing. The package hits a real HTTP endpoint; there's no built-in fake. Bind a fake Zoho class in a service container for tests, or use Http::fake() scoped to *.zohoapis.* if the package routes through Laravel's HTTP client (varies by version). Wrap the facade behind your own interface so tests never touch the package directly.

Package is quiet. Last release was March 2022, and the underlying Zoho CRM API has moved on in places (v2.1 nuances, new modules). Pin your Laravel version, read the actual SDK source under vendor/asciisd/zoho when behavior surprises you, and be prepared to fork or wrap. Treat it as a thin SDK shim, not a maintained integration — your own abstraction layer pays for itself the first time you hit a missing endpoint.

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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package