A professional PHP SDK for the Puzzler Media REST API.
Install via Composer:
composer require tomgould/puzzlerphpsdk- PHP 7.4 or higher
- cURL extension
- JSON extension
<?php
require_once 'vendor/autoload.php';
use TomGould\PuzzlerPHPSDK\PuzzlerClient;
use TomGould\PuzzlerPHPSDK\Exception\PuzzlerException;
// Initialize the client
$client = new PuzzlerClient(
'YOUR_CLIENT_ID',
'YOUR_API_KEY',
'YOUR_SECRET_KEY'
);
// Check API health
try {
$health = $client->health()->check();
echo $health; // "I am healthly!"
} catch (PuzzlerException $e) {
echo "Error: " . $e->getMessage();
}
// Get all puzzles
try {
$puzzles = $client->puzzle()->collect();
print_r($puzzles);
} catch (PuzzlerException $e) {
echo "Error: " . $e->getMessage();
}use TomGould\PuzzlerPHPSDK\PuzzlerClient;
$client = new PuzzlerClient(
'YOUR_CLIENT_ID', // X-Client-Id
'YOUR_API_KEY', // X-Api-Key
'YOUR_SECRET_KEY', // Secret Key
'https://rest-api.puzzlerdigital.uk' // Optional: Base URL
);$health = $client->health()->check();The dictionary endpoint returns all available puzzle types and names from the latest bundle.
$dictionary = $client->puzzle()->dictionary();
// Available puzzle types
print_r($dictionary['types']); // ['XW', 'SU', 'WS', ...]
// Available puzzle names by type
print_r($dictionary['names']); // ['SU' => ['Sudoku'], ...]Example response:
[
'types' => ['XW', 'XC', 'WW', 'WS', 'SW', 'SU', 'JS', 'HM', 'AU'],
'names' => [
'XW' => ['Crossword'],
'XC' => ['Cryptic Crossword'],
'WW' => ['Word Wheel'],
'WS' => ['Wordsearch'],
'SW' => ['Splitwords'],
'SU' => ['Hard Sudoku', 'Medium Sudoku', 'Easy Sudoku'],
'JS' => ['Jigsaw'],
'HM' => ['Hangman'],
'AU' => ['Add Up']
]
]$puzzles = $client->puzzle()->collect();$puzzles = $client->puzzle()->collect([
'puzzleDate' => '2025-11-12'
]);Note: Date must be in YYYY-MM-DD format. The API returns puzzles from the latest bundle, so requesting dates outside the bundle's range will return empty results.
$puzzles = $client->puzzle()->collect([
'puzzleDateFrom' => '2025-11-01',
'puzzleDateTo' => '2025-11-30'
]);$puzzles = $client->puzzle()->collect([
'puzzleTypes' => ['XW', 'WS', 'SU']
]);Available puzzle type abbreviations:
XW- CrosswordXC- Cryptic CrosswordWW- Word WheelWS- WordsearchSW- SplitwordsSU- Sudoku (all variants)JS- JigsawHM- HangmanAU- Add Up
When filtering by puzzle names, you can specify exact puzzle names. If you want only specific puzzle variants (e.g., only "Easy Sudoku"), do NOT include that puzzle type in puzzleTypes, otherwise all puzzles of that type will be returned.
// Get only "Easy Sudoku" puzzles (DO NOT include 'SU' in puzzleTypes)
$puzzles = $client->puzzle()->collect([
'puzzleNames' => ['Easy Sudoku']
]);
// Get all Crosswords and Wordsearches, plus only "Easy Sudoku"
$puzzles = $client->puzzle()->collect([
'puzzleTypes' => ['XW', 'WS'], // Note: 'SU' is NOT included here
'puzzleNames' => ['Easy Sudoku']
]);$puzzles = $client->puzzle()->collect([
'puzzleDate' => '2025-11-12',
'puzzleTypes' => ['XW', 'WS'],
'puzzleNames' => ['Easy Sudoku']
]);Get today's wordsearches:
$puzzles = $client->puzzle()->collect([
'puzzleDate' => date('Y-m-d'),
'puzzleTypes' => ['WS']
]);Get all sudoku variants for a date range:
$puzzles = $client->puzzle()->collect([
'puzzleDateFrom' => '2025-11-01',
'puzzleDateTo' => '2025-11-07',
'puzzleTypes' => ['SU']
]);Build a weekly puzzle archive:
// Get all puzzles from last 7 days
$startDate = date('Y-m-d', strtotime('-7 days'));
$endDate = date('Y-m-d');
$puzzles = $client->puzzle()->collect([
'puzzleDateFrom' => $startDate,
'puzzleDateTo' => $endDate
]);
foreach ($puzzles as $puzzle) {
echo "{$puzzle['name']} - {$puzzle['rdate']}\n";
}The SDK throws specific exceptions for different error types:
use TomGould\PuzzlerPHPSDK\Exception\AuthenticationException;
use TomGould\PuzzlerPHPSDK\Exception\BadRequestException;
use TomGould\PuzzlerPHPSDK\Exception\NotFoundException;
use TomGould\PuzzlerPHPSDK\Exception\MethodNotAllowedException;
use TomGould\PuzzlerPHPSDK\Exception\ServerException;
use TomGould\PuzzlerPHPSDK\Exception\PuzzlerException;
try {
$puzzles = $client->puzzle()->collect(['puzzleDate' => '2025-11-12']);
} catch (AuthenticationException $e) {
// Handle authentication errors (401)
echo "Authentication failed: " . $e->getMessage();
} catch (BadRequestException $e) {
// Handle bad request errors (400)
echo "Bad request: " . $e->getMessage();
echo "Response: " . $e->getResponseBody();
} catch (NotFoundException $e) {
// Handle not found errors (404)
echo "Not found: " . $e->getMessage();
} catch (ServerException $e) {
// Handle server errors (500, 502)
echo "Server error: " . $e->getMessage();
} catch (PuzzlerException $e) {
// Handle any other errors
echo "Error: " . $e->getMessage();
// Get raw response body if needed
if ($e->getResponseBody()) {
echo "Response: " . $e->getResponseBody();
}
}Main client class for accessing the API.
public function __construct(
string $clientId,
string $apiKey,
string $secretKey,
string $baseUrl = 'https://rest-api.puzzlerdigital.uk'
)puzzle(): PuzzleClient- Get puzzle client instancehealth(): HealthClient- Get health client instance
Client for puzzle-related operations.
collect(array $filters = []): array- Collect puzzles with optional filtersdictionary(): array- Get dictionary of available puzzle types and names
| Parameter | Type | Required | Format | Description |
|---|---|---|---|---|
puzzleDate |
string | No | YYYY-MM-DD | Exact date to filter by |
puzzleDateFrom |
string | No | YYYY-MM-DD | Start date for range filter |
puzzleDateTo |
string | No | YYYY-MM-DD | End date for range filter |
puzzleTypes |
array | No | Array of strings | Puzzle type abbreviations |
puzzleNames |
array | No | Array of strings | Exact puzzle names |
Client for health check operations.
check(): string- Check API health status
Exception
└── PuzzlerException
├── AuthenticationException (401)
├── BadRequestException (400)
├── NotFoundException (404)
├── MethodNotAllowedException (405)
└── ServerException (500, 502)
Problem: You're filtering by a specific date but getting an empty array back, even though puzzles should exist.
Possible causes:
- Date is outside the bundle range - The API only returns puzzles from the "latest bundle". If you request a date that's not in the current bundle, you'll get no results.
- Date format is incorrect - Ensure you're using YYYY-MM-DD format (e.g., '2025-11-12', not '11/12/2025')
- No puzzles published for that date - Some dates may not have puzzles available
Solution:
// First, check what's available by getting all puzzles
$allPuzzles = $client->puzzle()->collect();
// Check the date range in the bundle
$dates = array_unique(array_column($allPuzzles, 'rdate'));
print_r($dates);
// Then filter by a date you know exists
$puzzles = $client->puzzle()->collect([
'puzzleDate' => '2025-11-12' // Use a date from the above list
]);Problem: You need to archive puzzles from multiple days/weeks/months, but the API only provides the "latest bundle".
Solution: You'll need to implement a cron job or scheduled task that regularly fetches and stores puzzles:
// Example: Daily archival script
$client = new PuzzlerClient($clientId, $apiKey, $secretKey);
// Get today's puzzles
$todaysPuzzles = $client->puzzle()->collect([
'puzzleDate' => date('Y-m-d')
]);
// Store them in your database
foreach ($todaysPuzzles as $puzzle) {
// Save to database
$db->insert('puzzle_archive', [
'puzzle_id' => $puzzle['puzzle_id'],
'pml_id' => $puzzle['pml_id'],
'type' => $puzzle['abbr'],
'name' => $puzzle['name'],
'date' => $puzzle['rdate'],
'game_data' => json_encode($puzzle['game_data']),
'archived_at' => date('Y-m-d H:i:s')
]);
}Problem: Getting 401 Authentication Failed errors.
Possible causes:
- Incorrect credentials - Double-check your Client ID, API Key, and Secret Key
- Clock skew - The API signature is time-sensitive (valid for 5 minutes). Ensure your server's clock is synchronized
Solution:
// Verify your credentials are correct
$client = new PuzzlerClient(
'YOUR_CLIENT_ID',
'YOUR_API_KEY',
'YOUR_SECRET_KEY'
);
// Test with health check first (public endpoint)
try {
$health = $client->health()->check();
echo "Connection successful!\n";
} catch (AuthenticationException $e) {
echo "Auth failed. Check your credentials.\n";
}
// Check server time
echo "Server time: " . date('Y-m-d H:i:s') . " UTC: " . gmdate('Y-m-d H:i:s') . "\n";Problem: You're filtering by puzzle name but getting all puzzles of that type.
Cause: When using puzzleNames, you must NOT include the corresponding puzzle type in puzzleTypes, otherwise the API returns all puzzles of that type.
Incorrect:
// This will return ALL Sudoku puzzles, ignoring the name filter
$puzzles = $client->puzzle()->collect([
'puzzleTypes' => ['SU'], // ❌ Don't include 'SU' here
'puzzleNames' => ['Easy Sudoku']
]);Correct:
// This will return ONLY "Easy Sudoku" puzzles
$puzzles = $client->puzzle()->collect([
'puzzleNames' => ['Easy Sudoku'] // ✅ Omit 'SU' from puzzleTypes
]);
// Or combine with other types:
$puzzles = $client->puzzle()->collect([
'puzzleTypes' => ['XW', 'WS'], // ✅ 'SU' is NOT included
'puzzleNames' => ['Easy Sudoku']
]);Problem: You're certain your filters should match puzzles, but you're getting an empty array.
Debug steps:
// Step 1: Get the dictionary to see what's available
$dictionary = $client->puzzle()->dictionary();
print_r($dictionary);
// Step 2: Get all puzzles to see what's in the bundle
$allPuzzles = $client->puzzle()->collect();
echo "Total puzzles: " . count($allPuzzles) . "\n";
// Step 3: Check a specific puzzle's properties
if (!empty($allPuzzles)) {
print_r($allPuzzles[0]);
}
// Step 4: Try filtering by something you know exists
$firstPuzzle = $allPuzzles[0];
$filtered = $client->puzzle()->collect([
'puzzleDate' => $firstPuzzle['rdate'],
'puzzleTypes' => [$firstPuzzle['abbr']]
]);
echo "Filtered results: " . count($filtered) . "\n";The Puzzler API has an unusual requirement for the request body format:
-
When filters are provided: Send them directly in the request body
{ "puzzleDate": "2025-11-12", "puzzleTypes": ["WS"] } -
When NO filters are provided: The body must be wrapped in a "model" property
{ "model": {} }
This SDK handles this automatically, so you don't need to worry about it.
The API operates on the "latest bundle" concept:
- Puzzles are bundled and released periodically
- You can only access puzzles from the current/latest bundle
- Historical puzzles from older bundles are not accessible via the API
- To build an archive, you must fetch and store puzzles regularly
Authentication signatures are valid for 5 minutes from generation. The SDK generates a fresh signature for each request, so this shouldn't be an issue unless you have extreme clock skew.
- Copy the environment example file:
cp .env.example .env- Edit
.envand add your credentials:
PUZZLER_CLIENT_ID=your_client_id_here
PUZZLER_API_KEY=your_api_key_here
PUZZLER_SECRET_KEY=your_secret_key_here
PUZZLER_BASE_URL=https://rest-api.puzzlerdigital.uk- Load environment variables:
export $(cat .env | xargs)Run all tests:
./vendor/bin/phpunitRun only unit tests (no credentials required):
./vendor/bin/phpunit --testsuite UnitRun only integration tests (credentials required):
./vendor/bin/phpunit --testsuite IntegrationTest your credentials specifically:
./vendor/bin/phpunit tests/Integration/CredentialsTest.phpRun with coverage:
./vendor/bin/phpunit --coverage-html coveragetests/
├── Unit/ # Unit tests (no API calls)
│ ├── PuzzlerClientTest.php
│ ├── Http/HttpClientTest.php
│ └── Exception/PuzzlerExceptionTest.php
└── Integration/ # Integration tests (require credentials)
├── CredentialsTest.php # Test your API credentials
├── HealthCheckTest.php # Test health endpoint
├── PuzzleDictionaryTest.php # Test dictionary endpoint
└── PuzzleCollectTest.php # Test puzzle collection
MIT License - see LICENSE file for details
Tom Gould
- GitHub: @tomgould
https://github.com/tomgould/PuzzlerPHPSDK
For issues, questions, or contributions, please visit the GitHub repository.