Google Sheets as a REST API in PHP and Laravel
Query and write Google Sheet data from PHP using SheetsAPI with Guzzle, Laravel's HTTP client, caching, and queue jobs.
PHP remains one of the most widely deployed server-side languages, and Laravel is its dominant framework. SheetsAPI gives you a clean REST endpoint for any Google Sheet, so you can treat your spreadsheet as a lightweight data source without any Google API credentials in your PHP app.
Prerequisites
- PHP 8.2+ with Composer
- Laravel 11 (or plain PHP with Guzzle)
- A Google Sheet with a header row
- A SheetsAPI account and a
YOUR_USER_KEYkey from the dashboard
1. Plain PHP with Guzzle
Install Guzzle:
composer require guzzlehttp/guzzle<?php
use GuzzleHttp\Client;
$client = new Client(['base_uri' => 'https://sheetsapi.gkit.mreshank.com']);
$userKey = 'YOUR_USER_KEY';
$apiKey = 'sk_your_api_key_here';
$response = $client->get("/api/spreadsheets/{$userKey}/Products", [
'headers' => ['Authorization' => "Bearer {$apiKey}"],
'query' => ['limit' => 20, 'sort' => 'name'],
]);
$body = json_decode($response->getBody(), associative: true);
echo "Total: {$body['meta']['total']}\n";
foreach ($body['data'] as $product) {
echo "{$product['name']} - \${$product['price']}\n";
}2. Laravel HTTP client
Laravel wraps Guzzle with a fluent interface. Add your credentials to .env:
SHEETS_USER_KEY=YOUR_USER_KEY
SHEETS_API_KEY=sk_your_api_key_here
SHEETS_BASE_URL=https://sheetsapi.gkit.mreshank.comCreate a service class:
// app/Services/SheetsApiService.php
<?php
namespace App\Services;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
class SheetsApiService
{
private string $base;
private string $userKey;
private string $apiKey;
public function __construct()
{
$this->base = config('services.sheetsapi.base_url');
$this->userKey = config('services.sheetsapi.user_key');
$this->apiKey = config('services.sheetsapi.api_key');
}
public function list(string $sheet, array $params = []): array
{
$response = Http::withToken($this->apiKey)
->get("{$this->base}/api/spreadsheets/{$this->userKey}/{$sheet}", $params)
->throw();
return $response->json();
}
public function addRow(string $sheet, array $row): array
{
return Http::withToken($this->apiKey)
->post("{$this->base}/api/spreadsheets/{$this->userKey}/{$sheet}", $row)
->throw()
->json();
}
}Register in config/services.php:
'sheetsapi' => [
'base_url' => env('SHEETS_BASE_URL'),
'user_key' => env('SHEETS_USER_KEY'),
'api_key' => env('SHEETS_API_KEY'),
],3. Controller
// app/Http/Controllers/ProductController.php
<?php
namespace App\Http\Controllers;
use App\Services\SheetsApiService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ProductController extends Controller
{
public function __construct(private SheetsApiService $sheets) {}
public function index(Request $request): JsonResponse
{
$validated = $request->validate([
'search' => 'nullable|string|max:100',
'sort' => 'nullable|string|in:name,-name,price,-price',
'page' => 'nullable|integer|min:1',
]);
$limit = 20;
$offset = ($validated['page'] - 1) * $limit;
$params = [
'limit' => $limit,
'offset' => $offset,
];
if (!empty($validated['search'])) {
$params['search'] = "name:{$validated['search']}";
}
if (!empty($validated['sort'])) {
$params['sort'] = $validated['sort'];
}
$result = $this->sheets->list('Products', $params);
return response()->json($result);
}
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'price' => 'required|numeric|min:0',
'category' => 'required|string|max:100',
]);
$row = $this->sheets->addRow('Products', $validated);
return response()->json($row, 201);
}
}Route:
// routes/api.php
Route::apiResource('products', ProductController::class)->only(['index', 'store']);4. Laravel caching
Cache SheetsAPI responses to reduce latency and API calls:
use Illuminate\Support\Facades\Cache;
class SheetsApiService
{
// ... (constructor as above)
public function listCached(string $sheet, array $params = [], int $ttl = 60): array
{
$key = "sheets:{$sheet}:" . md5(serialize($params));
return Cache::remember($key, $ttl, fn () => $this->list($sheet, $params));
}
public function invalidate(string $sheet): void
{
// Use cache tags if your driver supports them (Redis, Memcached)
Cache::forget("sheets:{$sheet}:*");
}
}With Redis and cache tags (more precise invalidation):
public function listCached(string $sheet, array $params = [], int $ttl = 60): array
{
$key = md5(serialize($params));
return Cache::tags(["sheets", "sheet:{$sheet}"])
->remember($key, $ttl, fn () => $this->list($sheet, $params));
}
public function invalidate(string $sheet): void
{
Cache::tags(["sheet:{$sheet}"])->flush();
}5. Paginate all rows
public function fetchAll(string $sheet): array
{
$all = [];
$limit = 100;
$offset = 0;
do {
$result = $this->list($sheet, ['limit' => $limit, 'offset' => $offset]);
$all = array_merge($all, $result['data']);
$offset += $limit;
} while (count($all) < $result['meta']['total']);
return $all;
}6. Queue job for background sync
For large imports, dispatch a queue job so the HTTP response returns immediately:
// app/Jobs/SyncProductsJob.php
<?php
namespace App\Jobs;
use App\Services\SheetsApiService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class SyncProductsJob implements ShouldQueue
{
use Dispatchable, Queueable;
public int $tries = 3;
public int $backoff = 30;
public function handle(SheetsApiService $sheets): void
{
$products = $sheets->fetchAll('Products');
foreach ($products as $product) {
\DB::table('products')->updateOrInsert(
['name' => $product['name']],
[
'price' => $product['price'],
'category' => $product['category'],
'updated_at' => now(),
]
);
}
}
}
// Dispatch from a controller or scheduled command:
SyncProductsJob::dispatch();Schedule via app/Console/Kernel.php:
$schedule->job(new SyncProductsJob)->hourly();7. Error handling
use Illuminate\Http\Client\RequestException;
try {
$result = $this->sheets->list('Products', $params);
} catch (RequestException $e) {
$status = $e->response->status();
if ($status === 429) {
$retryAfter = $e->response->header('Retry-After') ?: 10;
sleep((int) $retryAfter);
// retry once
$result = $this->sheets->list('Products', $params);
} elseif ($status === 404) {
abort(404, 'Sheet not found');
} else {
logger()->error('SheetsAPI error', ['status' => $status, 'body' => $e->response->body()]);
abort(502, 'Upstream error');
}
}Query parameter reference
| Parameter | Example | Description |
|---|---|---|
limit | 20 | Rows per page (max 500) |
offset | 0 | Pagination offset |
search | category:books | Filter field:value |
sort | name or -price | Ascending / descending |
fields | name,price | Return only named columns |
Summary
| Concern | Approach |
|---|---|
| HTTP client | GuzzleHttp\Client or Http::withToken() |
| DI / service | SheetsApiService bound in service container |
| Caching | Cache::remember() or cache tags with Redis |
| Pagination | do/while with offset < meta.total |
| Background work | ShouldQueue job dispatched to queue worker |
| Errors | RequestException with status-based branching |
No Google API credentials, no OAuth flow - just a REST endpoint your PHP app can call like any other JSON API.