A PHP SDK for the Gripp (now Exact Gripp) CRM/ERP API. Manage companies, contacts, projects, invoices, time tracking, and 50+ other resources through a fluent query builder with batch operations, auto-pagination, and automatic retries. Works with any PHP 8.1+ application, including Laravel.
- Fluent query builder with 14 filter operators
- Full CRUD support on 54 Gripp resources
- Batch operations (multiple API calls in a single HTTP request)
- Auto-pagination for large datasets
- Automatic retries on server errors and connection failures
- Typed exceptions for authentication, rate limiting, and API errors
- Laravel Collection responses out of the box
- Self-documenting resources with field types, required fields, and relationship metadata
- PHP 8.1+
- A Gripp API token and API URL
composer require codebes/gripp-sdkSet GRIPP_API_TOKEN in your .env file or environment:
GRIPP_API_TOKEN=your-api-tokenThen call configure without arguments:
use CodeBes\GrippSdk\GrippClient;
GrippClient::configure();The SDK uses https://api.gripp.com by default. To override, set GRIPP_API_URL in your environment.
use CodeBes\GrippSdk\GrippClient;
GrippClient::configure(token: 'your-api-token');vendor/bin/gripp-setupThis creates a .env file with your credentials.
use CodeBes\GrippSdk\GrippClient;
use CodeBes\GrippSdk\Resources\Company;
use CodeBes\GrippSdk\Resources\Contact;
use CodeBes\GrippSdk\Resources\Project;
// Configure once at application boot
GrippClient::configure();
// Find a record by ID
$company = Company::find(123);
// Get all records (auto-paginated)
$allCompanies = Company::all();
// Query with filters
$activeCompanies = Company::where('active', true)
->orderBy('companyname', 'asc')
->limit(50)
->get();
// Create a record
$result = Company::create([
'companyname' => 'Acme Corp',
'relationtype' => 'COMPANY',
'email' => 'info@acme.com',
]);
// Update a record
Company::update(123, [
'phone' => '+31 20 123 4567',
]);
// Delete a record
Company::delete(123);The query builder provides a fluent interface for filtering, ordering, and paginating results.
use CodeBes\GrippSdk\Resources\Project;
// Simple equality filter (two-argument form)
$projects = Project::where('company', 42)->get();
// With operator (three-argument form)
$projects = Project::where('name', 'contains', 'Website')->get();
// Chain multiple filters
$results = Project::where('company', 42)
->where('archived', false)
->orderBy('createdon', 'desc')
->limit(25)
->offset(0)
->get();
// Get just the first match
$project = Project::where('name', 'contains', 'Redesign')->first();
// Count matching records
$count = Project::where('archived', false)->count();| Operator | Description |
|---|---|
equals |
Exact match (default when using two-argument where) |
notequals |
Not equal to |
contains |
String contains |
notcontains |
String does not contain |
startswith |
String starts with |
endswith |
String ends with |
greaterthan |
Greater than |
lessthan |
Less than |
greaterequals |
Greater than or equal to |
lessequals |
Less than or equal to |
in |
Value is in array |
notin |
Value is not in array |
isnull |
Field is null (pass true as value) |
isnotnull |
Field is not null (pass true as value) |
Common date filtering patterns are built into the query builder:
use CodeBes\GrippSdk\Resources\Project;
use CodeBes\GrippSdk\Resources\Hour;
// Filter by year
$projects = Project::where('archived', false)
->whereYear('createdon', 2026)
->get();
// Filter by month
$hours = Hour::where('employee', 42)
->whereMonth('date', 2026, 3)
->get();
// Filter by date range
$invoices = Invoice::where('company', 10)
->whereDateBetween('date', '2026-01-01', '2026-03-31')
->get();
// Modified since (for incremental syncing)
$updated = Project::where('archived', false)
->whereModifiedSince(new DateTime('2026-03-01 00:00:00'))
->get();Field names are automatically prefixed with the entity name when using the query builder. Writing Project::where('createdon', ...) produces the filter field project.createdon. Fully qualified fields like project.createdon are left as-is.
Both get() and the query builder's get() automatically paginate through all results. Use limit() when you only want a specific number of results (single API call):
// Fetches ALL matching projects across all pages
$all = Project::where('archived', false)->get();
// Fetches only the first 25 (single page)
$page = Project::where('archived', false)->limit(25)->get();Group multiple API calls into a single HTTP request for better performance:
use CodeBes\GrippSdk\GrippClient;
use CodeBes\GrippSdk\Resources\Company;
use CodeBes\GrippSdk\Resources\Contact;
$transport = GrippClient::getTransport();
$transport->startBatch();
// Queue multiple calls (these don't execute yet)
Company::find(1);
Company::find(2);
Contact::find(10);
// Execute all queued calls in a single HTTP request
$responses = $transport->executeBatch();
foreach ($responses as $response) {
$rows = $response->rows();
// Process each response...
}The transport tracks rate limit headers from API responses and provides hooks for proactive budget management:
$transport = GrippClient::getTransport();
// Check current rate limit state (from most recent response headers)
$transport->getRateLimitRemaining(); // e.g. 847
$transport->getRateLimitLimit(); // e.g. 1000
// Abort requests when budget is low
$transport->beforeRequest(function (int $requestCount, ?int $remaining, ?int $limit) {
if ($remaining !== null && $remaining <= 5) {
throw new \RuntimeException("Only {$remaining} API calls left!");
}
});
// React when a 429 or 503 rate limit hits
$transport->onRateLimitExceeded(function (?int $retryAfter, ?int $remaining) {
// Set a flag, notify monitoring, etc.
Log::warning("Gripp rate limit hit, retry after {$retryAfter}s");
});The SDK treats both HTTP 429 and HTTP 503 with Gripp error code 1004 (short-burst throttle) as rate limit errors.
The SDK throws specific exceptions for different error types:
use CodeBes\GrippSdk\Exceptions\AuthenticationException;
use CodeBes\GrippSdk\Exceptions\RateLimitException;
use CodeBes\GrippSdk\Exceptions\RequestException;
use CodeBes\GrippSdk\Exceptions\GrippException;
try {
$company = Company::find(123);
} catch (AuthenticationException $e) {
// 401 or 403 - invalid token or forbidden
if ($e->isTokenInvalid()) {
// Handle invalid/expired token
}
if ($e->isForbidden()) {
// Handle insufficient permissions
}
} catch (RateLimitException $e) {
// 429 - too many requests
$retryAfter = $e->getRetryAfter(); // seconds to wait
$remaining = $e->getRemaining(); // remaining requests
} catch (RequestException $e) {
// Other API errors
$data = $e->getResponseData(); // raw error response
} catch (GrippException $e) {
// Base exception for all SDK errors (e.g. not configured)
}All resources support read operations. Resources that also support create, update, and/or delete are indicated below.
| Resource | Create | Read | Update | Delete |
|---|---|---|---|---|
AbsenceRequest |
x | x | x | x |
AbsenceRequestLine |
x | x | x | x |
BulkPrice |
x | x | x | x |
CalendarItem |
x | x | x | x |
Company |
x | x | x | x |
CompanyDossier |
x | x | x | x |
Contact |
x | x | x | x |
Contract |
x | x | x | x |
ContractLine |
x | x | x | x |
Cost |
x | |||
CostHeading |
x | x | x | x |
Department |
x | x | x | x |
Employee |
x | x | x | x |
EmployeeFamily |
x | x | x | x |
EmployeeTarget |
x | |||
EmployeeYearlyLeaveBudget |
x | x | x | |
EmploymentContract |
x | x | x | x |
ExternalLink |
x | x | x | x |
File |
x | |||
Hour |
x | x | x | x |
Invoice |
x | x | x | x |
InvoiceLine |
x | x | x | x |
Ledger |
x | x | x | x |
Memorial |
x | |||
MemorialLine |
x | |||
Notification |
||||
Offer |
x | x | x | x |
OfferPhase |
x | x | x | x |
OfferProjectLine |
x | x | x | x |
Packet |
x | x | x | x |
PacketLine |
x | x | x | x |
Payment |
x | x | x | x |
PriceException |
x | x | x | x |
Product |
x | x | x | x |
Project |
x | x | x | x |
ProjectPhase |
x | x | x | x |
PurchaseInvoice |
x | x | x | x |
PurchaseInvoiceLine |
x | x | x | x |
PurchaseOrder |
x | x | x | x |
PurchaseOrderLine |
x | x | x | x |
PurchasePayment |
x | x | x | x |
RejectionReason |
x | x | x | x |
RevenueTarget |
x | |||
Tag |
x | x | x | x |
Task |
x | x | x | x |
TaskPhase |
x | x | x | x |
TaskType |
x | x | x | x |
TimelineEntry |
x | x | x | x |
UmbrellaProject |
x | x | x | |
Unit |
x | x | x | x |
Webhook |
x | x | x | x |
YearTarget |
x | |||
YearTargetType |
x |
Special resources:
Notificationhas customemit()andemitall()methods instead of CRUD.Companyhas additionalgetCompanyByCOC(),addInteractionByCompanyId(), andaddInteractionByCompanyCOC()methods.
Every resource class exposes constants that describe its schema:
use CodeBes\GrippSdk\Resources\Company;
Company::FIELDS; // ['id' => 'int', 'companyname' => 'string', ...]
Company::READONLY; // ['createdon', 'updatedon', 'id', 'searchname', 'files']
Company::REQUIRED; // ['relationtype']
Company::RELATIONS; // ['accountmanager' => Employee::class, 'tags' => Tag::class, ...]FIELDSmaps field names to their types (string,int,float,boolean,datetime,date,array,customfields,color)READONLYlists fields that cannot be written toREQUIREDlists fields that must be provided when creating/updatingRELATIONSmaps foreign key fields to their related resource classes
Both all() and get() automatically handle pagination, fetching all matching records transparently:
// Fetches all companies, regardless of how many pages it takes
$companies = Company::all(); // Returns Illuminate\Support\Collection
// Filtered queries also auto-paginate
$active = Company::where('active', true)->get(); // All pagesAll collection methods return Illuminate\Support\Collection instances. Single-record methods return associative arrays or null.
$companies = Company::where('active', true)->get();
// Use Collection methods
$names = $companies->pluck('companyname');
$grouped = $companies->groupBy('visitingaddress_city');
$first = $companies->first();composer testOr directly:
vendor/bin/phpunitPlease see the GitHub Releases page for more information on what has changed recently.
Contributions are welcome! Please open a pull request against the main branch. All PRs require:
- Passing tests (
composer test) - Code style compliance (
composer cs) - Static analysis passing (
composer analyse) - Code owner approval
MIT - see LICENSE for details.