jcolombo/paymo-api-php

PHP implementation of the Paymo App API

Maintainers

Package info

github.com/jcolombo/paymo-api-php

pkg:composer/jcolombo/paymo-api-php

Statistics

Installs: 41

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.6.1 2025-12-08 05:24 UTC

This package is auto-updated.

Last update: 2026-04-15 22:30:37 UTC


README

A robust, object-oriented PHP SDK for the Paymo project management API.

Latest Version PHP Version License GitHub Issues GitHub Stars

Overview

This independently developed package provides a developer-friendly toolkit to simplify all interactions with the Paymo REST API. It is not affiliated with or endorsed by Paymo.

Official Paymo API Documentation: https://github.com/paymoapp/api

Features

  • Full CRUD Operations - Create, Read, Update, and Delete for all 38 Paymo resource types
  • Fluent Interface - Chainable methods for clean, readable code
  • Smart Query Building - WHERE filters, HAS relationship conditions, and INCLUDE for eager loading
  • JSON-Ready Collections - Collections are directly JSON-serializable for API responses
  • Server-Side Pagination - Limit and paginate large result sets via limit() method
  • Response Caching - Built-in file-based caching to reduce API calls and avoid rate limits
  • Request Logging - Comprehensive logging for debugging and monitoring
  • Type Safety - Property type validation for each resource type
  • Relationship Support - Load related entities in a single call
  • File Uploads - Easy image and file attachment handling

Requirements

  • PHP 7.4 or higher
  • An active Paymo account
  • A Paymo API key (found in your Paymo account settings)
  • Composer

Installation

composer require jcolombo/paymo-api-php

Quick Start

Establishing a Connection

use Jcolombo\PaymoApiPhp\Paymo;
use Jcolombo\PaymoApiPhp\Entity\Resource\Project;

// Connect with your API key
$paymo = Paymo::connect('YOUR_API_KEY');

// Alternative: Username/password authentication (API key recommended; see Session resource for token-based auth)
$paymo = Paymo::connect(['username', 'password']);

Fetching a Single Resource

use Jcolombo\PaymoApiPhp\Entity\Resource\Project;

// Fetch a project by ID
$project = Project::new()->fetch(12345);

// Access properties directly
echo $project->name;
echo $project->description;

// Fetch with related entities included
$project = Project::new()->fetch(12345, ['client', 'tasklists', 'tasks']);
echo $project->client->name; // Access the related client

Fetching Collections (Lists)

use Jcolombo\PaymoApiPhp\Entity\Resource\Project;
use Jcolombo\PaymoApiPhp\Entity\Resource\Task;

// Get all projects
$projects = Project::list()->fetch();

foreach ($projects as $project) {
    echo $project->name . "\n";
}

// Get count directly
echo "Total projects: " . count($projects);

// JSON encode directly for API responses
$json = json_encode($projects);  // Returns array of flattened objects

// Get all tasks with filters
$tasks = Task::list()
    ->where(Task::where('complete', false))
    ->fetch();

// Paginate results (get only first 100)
$invoices = Invoice::list()->limit(100)->fetch();

// Paginate with explicit page (page 2, 50 per page)
$invoices = Invoice::list()->limit(2, 50)->fetch();

Creating Resources

use Jcolombo\PaymoApiPhp\Entity\Resource\Project;
use Jcolombo\PaymoApiPhp\Entity\Resource\Task;

// Create a new project
$project = new Project();
$project->name = "My New Project";
$project->description = "Project description here";
$project->create();

// Fluent style creation
$project = Project::new()
    ->set(['name' => 'Another Project', 'description' => 'Created with chaining'])
    ->create();

// Create a task (requires tasklist_id or project_id)
$task = Task::new()
    ->set([
        'name' => 'My First Task',
        'tasklist_id' => 123,
        'description' => 'Task details here'
    ])
    ->create();

Updating Resources

// Fetch, modify, and update
$project = Project::new()->fetch(12345);
$project->name = "Updated Project Name";
$project->description = "New description";
$project->update();

// Only dirty (changed) fields are sent to the API

Deleting Resources

// Delete via instance
$project = Project::new()->fetch(12345);
$project->delete();

// Delete by ID directly
Project::deleteById(12345);

Supported Resources

The SDK supports all 38 Paymo API resource types:

Category Resources
Projects & Tasks Project, ProjectStatus, ProjectTemplate, Tasklist, Task, Subtask, TaskAssignment
Time Tracking TimeEntry, Booking
Financial Invoice, InvoiceItem, InvoicePayment, InvoiceTemplate, Estimate, EstimateItem, EstimateTemplate, Expense
Recurring RecurringProfile, RecurringProfileItem, TaskRecurringProfile
Users & Clients User, Client, ClientContact, Company
Collaboration Discussion, Comment, CommentThread, Milestone
Workflows Workflow, WorkflowStatus
Files & Sessions File, Session
Reports Report
Templates ProjectTemplateTask, ProjectTemplateTasklist, EstimateTemplateGallery, InvoiceTemplateGallery
Integrations Webhook

Query Building

WHERE Filters

Filter collections using the static where() method on any resource class:

use Jcolombo\PaymoApiPhp\Entity\Resource\Project;
use Jcolombo\PaymoApiPhp\Entity\Resource\Task;

// Simple equality
$projects = Project::list()
    ->where(Project::where('active', true))
    ->fetch();

// With operators
$tasks = Task::list()
    ->where(Task::where('complete', false, '='))
    ->where(Task::where('due_date', '2024-12-31', '<='))
    ->fetch();

// Using IN operator
$projects = Project::list()
    ->where(Project::where('users', [1, 2, 3], 'in'))
    ->fetch();

HAS Relationship Filters

Filter by the existence of related entities:

// Projects that have at least one task
$projects = Project::list()
    ->where(Project::has('tasks', 0, '>'))
    ->fetch();

// Projects with more than 5 milestones
$projects = Project::list()
    ->where(Project::has('milestones', 5, '>'))
    ->fetch();

Including Related Entities

Eager-load related entities in a single API call:

// Include single relations
$project = Project::new()->fetch(12345, ['client']);

// Include multiple relations
$project = Project::new()->fetch(12345, ['client', 'tasklists', 'tasks', 'milestones']);

// Access included relations
echo $project->client->name;
foreach ($project->tasks as $task) {
    echo $task->name;
}

Configuration

Configuration File

Create a paymoapi.config.json file in your project root to customize behavior:

{
  "connection": {
    "url": "https://app.paymoapp.com/api/",
    "defaultName": "PaymoApi",
    "verify": false,
    "timeout": 15.0
  },
  "path": {
    "cache": "/path/to/cache/directory",
    "logs": "/path/to/logs/directory"
  },
  "enabled": {
    "cache": true,
    "logging": true
  },
  "log": {
    "connections": false,
    "requests": true
  },
  "devMode": false
}

Configuration Options

Option Type Default Description
connection.url string https://app.paymoapp.com/api/ Paymo API base URL
connection.timeout float 15.0 Request timeout in seconds
connection.verify bool false SSL certificate verification
enabled.cache bool false Enable response caching
enabled.logging bool false Enable request/response logging
log.connections bool false Log connection events
log.requests bool true Log API requests
devMode bool false Enable development mode validations
rateLimit.enabled bool true Enable automatic rate limiting
rateLimit.minDelayMs int 200 Minimum delay between requests (milliseconds)
rateLimit.safetyBuffer int 1 Start throttling when remaining requests drops to this level
rateLimit.maxRetries int 3 Maximum retries for 429 responses
rateLimit.retryDelayMs int 1000 Delay before retrying after a 429 response (milliseconds)

Caching

The SDK includes a built-in caching system to reduce API calls and help avoid rate limits.

Enable Caching

{
  "enabled": {
    "cache": true
  },
  "path": {
    "cache": "/path/to/cache/directory"
  }
}

Or define the cache path via constant before loading the SDK:

define('PAYMOAPI_REQUEST_CACHE_PATH', '/path/to/cache');

Cache Control

use Jcolombo\PaymoApiPhp\Cache\Cache;

// Set cache lifespan (default: 300 seconds / 5 minutes)
Cache::lifespan(600); // 10 minutes

// Skip cache for a specific request
$project = Project::new()->fetch(12345, [], ['skipCache' => true]);

// Ignore cache on an entity
$project = Project::new()->ignoreCache(true)->fetch(12345);

// Custom cache handlers
Cache::registerCacheMethods(
    function($key, $lifespan) { /* fetch logic */ },
    function($key, $data, $lifespan) { /* store logic */ }
);

File Uploads

Uploading Images

// Upload an image to an existing entity (like a client logo)
$client = Client::new()->fetch(123);
$client->image('/path/to/logo.png');

// Specify the property key if needed
$user = User::new()->fetch(456);
$user->image('/path/to/avatar.jpg', 'image');

Uploading Files

// Attach a file to an entity
$task = Task::new()->fetch(789);
$task->file('/path/to/document.pdf');

Working with Properties

Getting and Setting

$project = Project::new()->fetch(12345);

// Get single property
$name = $project->get('name');

// Get multiple properties
$data = $project->get(['name', 'description', 'active']);

// Set single property
$project->set('name', 'New Name');

// Set multiple properties
$project->set([
    'name' => 'New Name',
    'description' => 'New description'
]);

Dirty Tracking

The SDK tracks which properties have been modified since the last save/load:

$project = Project::new()->fetch(12345);
$project->name = "Changed Name";

// Check if any properties are dirty
if ($project->isDirty()) {
    $project->update(); // Only sends changed fields
}

// Get list of dirty property keys
$dirtyKeys = $project->getDirtyKeys(); // ['name']

// Get dirty values with original and current
$dirtyValues = $project->getDirtyValues();
// ['name' => ['original' => 'Old Name', 'current' => 'Changed Name']]

Flattening to stdClass

Export entity data as a plain PHP object:

$project = Project::new()->fetch(12345, ['client', 'tasks']);

// Get as stdClass (includes relations)
$data = $project->flatten();

// Strip null values
$data = $project->flatten(['stripNull' => true]);

JSON Serialization

Collections can be directly JSON-encoded for API responses:

$projects = Project::list()->fetch(['id', 'name', 'active']);

// Direct JSON encoding - collections implement JsonSerializable
echo json_encode($projects);
// Output: [{"id": 123, "name": "Project A", "active": true}, ...]

// Assign directly to response data structures
$response->projects = $projects;  // Auto-serializes correctly

Error Handling

The SDK throws exceptions for common error scenarios:

use Jcolombo\PaymoApiPhp\Entity\Resource\Project;

try {
    // Missing required field
    $project = Project::new()->create(); // Throws: requires 'name'

    // Fetch without ID
    $project = Project::new()->fetch(); // Throws: requires ID

    // Update without ID
    $project = Project::new();
    $project->name = "Test";
    $project->update(); // Throws: requires ID

} catch (\Exception $e) {
    echo "Error: " . $e->getMessage();
}

Protecting Dirty Data

Prevent accidental overwrites of unsaved changes:

$project = Project::new()->fetch(12345);
$project->name = "Unsaved change";
$project->protectDirtyOverwrites(true);

// This will throw an exception because there are dirty fields
$project->fetch(12345); // Throws exception

Pagination

Note: This is an undocumented Paymo API feature. See OVERRIDES.md for details.

The SDK supports server-side pagination for collection fetches:

use Jcolombo\PaymoApiPhp\Entity\Resource\Invoice;

// Fetch only the first 100 results (page 0 implied)
$invoices = Invoice::list()->limit(100)->fetch();

// Fetch page 2 with 50 results per page
$invoices = Invoice::list()->limit(2, 50)->fetch();

// Combine with filters
$tasks = Task::list()
    ->limit(25)
    ->fetch(['name'], [Task::where('complete', false)]);

// Iterate through all pages
$page = 0;
$pageSize = 100;
$allInvoices = [];

do {
    $invoices = Invoice::list()->limit($page, $pageSize)->fetch();
    $results = $invoices->raw();
    $allInvoices = array_merge($allInvoices, $results);
    $page++;
} while (count($results) === $pageSize);

Key Points:

  • Pages are 0-indexed (first page is 0)
  • limit(100) = page 0, 100 results
  • limit(2, 50) = page 2, 50 results per page
  • API does NOT return total count - track pages manually
  • WHERE conditions apply before pagination

Rate Limiting

Paymo enforces API rate limits. The SDK includes a built-in 200ms minimum delay between requests to help prevent hitting rate limits. For high-volume operations:

  1. Enable caching to reduce redundant API calls
  2. Use skipCache sparingly - only when you need fresh data
  3. Batch operations where possible by including related entities
  4. Use pagination to process large datasets in chunks

Advanced Usage

Multiple Connections

// Connect to multiple Paymo accounts
$connection1 = Paymo::connect('API_KEY_1', null, 'Account1');
$connection2 = Paymo::connect('API_KEY_2', null, 'Account2');

// Use specific connection for entities
$project = new Project($connection1);
$project->fetch(12345);

Resource Property Types

Each resource defines its property types for validation:

// Property types include:
// - 'text', 'integer', 'decimal', 'boolean'
// - 'date', 'datetime'
// - 'resource:entityname' - foreign key reference
// - 'collection:entityname' - array of related entities
// - 'intEnum:25|50|75|100' - enumerated integer values

Read-Only and Create-Only Properties

// READONLY properties (like 'id', 'created_on') cannot be set
// CREATEONLY properties can only be set during create(), not update()

$task = Task::new();
$task->project_id = 123;  // CREATEONLY - can set before create()
$task->name = "My Task";
$task->create();

$task->project_id = 456;  // Ignored - cannot change after creation
$task->update();

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Credits

Developed and maintained by Joel Colombo at 360 PSG, Inc.

Changelog

See CHANGELOG.md for a detailed history of changes.