Get started
Use PHP's curl_* functions to fetch a list and a single entry from genko.me and render them as HTML.
This guide connects PHP to the genko.me blog API and fetches a list and a single entry. It uses only the standard cURL and JSON extensions.
Prerequisites
- A genko.me account and workspace. You need the Owner role to create an API and issue an API key.
- PHP 8.1 or later. The code uses constructor promotion and
readonlyproperties. Enable the cURL (ext-curl) and JSON (ext-json) extensions; check withphp -m | grep -E 'curl|json'. - One list API and one published entry. If needed, create them in steps 2 and 3 of the quickstart.
Replace your-workspace with your workspace ID and blog with your endpoint throughout this guide.
Install
Nothing to install. You do not need a Composer package. Add the following code to your project.
Configure an API key and workspace ID
If your dashboard URL is https://genko.me/w/your-workspace, your workspace ID is your-workspace.
Create an API key in Settings and plan → API keys (/w/your-workspace/settings/api-keys). Scope it to blog under Selected APIs only to limit exposure if the key leaks. You cannot view its value again after creation.
Read the key from an environment variable. Use SetEnv with Apache, env[GENKO_API_KEY] with php-fpm, or your Docker or PaaS environment settings.
$apiKey = getenv('GENKO_API_KEY');Do not write the key directly in a PHP file. If you must use a configuration file, keep it outside the document root and out of Git. Depending on server configuration, a .env file in the document root could be served as plain text.
Write a client
Put delivery API GET requests in one place.
<?php
declare(strict_types=1);
const GENKO_BASE_URL = 'https://api.genko.me';const GENKO_WORKSPACE = 'your-workspace';
final class GenkoApiError extends RuntimeException{}
/** * Send a GET request to the delivery API and return a decoded associative array. * * @param array<string, string|int> $queries * @return array<string, mixed> */function genko_get(string $path, array $queries = []): array{ $url = GENKO_BASE_URL . '/v1/' . GENKO_WORKSPACE . $path; if ($queries !== []) { $url .= '?' . http_build_query($queries); }
$curl = curl_init($url); curl_setopt_array($curl, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_HTTPHEADER => ['X-API-KEY: ' . getenv('GENKO_API_KEY')], ]);
$body = curl_exec($curl); if ($body === false) { throw new GenkoApiError('Could not connect to the delivery API: ' . curl_error($curl)); }
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE); $decoded = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
if ($status >= 400) { throw new GenkoApiError($decoded['message'] ?? 'Unknown error.', $status); }
return $decoded;}http_build_query() URL-encodes brackets in filters and non-ASCII values. Do not concatenate a query string yourself.
This code does not call curl_close(): it became unnecessary in PHP 8.0 and deprecated in 8.5. $curl is released when it leaves scope.
Show a list
<?php
require __DIR__ . '/../src/Genko.php';
$list = genko_get('/blog', [ 'limit' => 10, 'orders' => '-publishedAt', 'fields' => 'title,publishedAt',]);
?><h1>Blog (<?= (int) $list['totalCount'] ?> entries)</h1><ul><?php foreach ($list['contents'] as $post): ?> <li> <a href="/blog.php?id=<?= rawurlencode($post['id']) ?>"> <?= htmlspecialchars($post['title'], ENT_QUOTES, 'UTF-8') ?> </a> </li><?php endforeach; ?></ul>The response is an associative array with contents, totalCount, offset, and limit. When no entries match, contents is an empty array rather than an error.
Always pass text fields through htmlspecialchars(). The delivery API returns text without escaping it.
Map entries to types
An associative array will not flag a mistyped field ID. A class matching your schema also gives you IDE completion.
<?php
declare(strict_types=1);
final class Post{ public function __construct( public readonly string $id, public readonly string $title, public readonly string $body, public readonly ?string $category, /** @var list<string> */ public readonly array $tags, public readonly ?DateTimeImmutable $publishedAt, ) { }
/** @param array<string, mixed> $entry */ public static function fromEntry(array $entry): self { return new self( id: $entry['id'], title: $entry['title'] ?? '', body: $entry['body'] ?? '', // Optional fields and fields added later may be absent from older entries. category: $entry['category'] ?? null, tags: $entry['tags'] ?? [], publishedAt: isset($entry['publishedAt']) ? new DateTimeImmutable($entry['publishedAt']) : null, ); }}When you narrow the response with fields, unspecified fields are omitted. Keep the fallback values provided with ??.
$posts = array_map(Post::fromEntry(...), $list['contents']);Show a single entry
Append an id from the list to the path.
<?php
require __DIR__ . '/../src/Genko.php';require __DIR__ . '/../src/Post.php';
$id = $_GET['id'] ?? '';
try { $post = Post::fromEntry(genko_get('/blog/' . rawurlencode($id)));} catch (GenkoApiError $error) { if ($error->getCode() === 404) { http_response_code(404); exit('Entry not found.'); } throw $error;}
?><article> <h1><?= htmlspecialchars($post->title, ENT_QUOTES, 'UTF-8') ?></h1> <?php if ($post->publishedAt !== null): ?> <time datetime="<?= $post->publishedAt->format(DateTimeInterface::ATOM) ?>"> <?= $post->publishedAt->setTimezone(new DateTimeZone('Asia/Tokyo'))->format('F j, Y') ?> </time> <?php endif; ?> <?= $post->body ?></article>Unlike a list response, a detail response is the entry's associative array itself, without a contents wrapper. Unpublished, deleted, and nonexistent IDs all return 404.
Dates arrive as ISO 8601 strings in UTC. Convert them to Asia/Tokyo before display.
Rich text (body) is an HTML string, so output it without htmlspecialchars(). It displays content entered in the dashboard; give editing access only to trusted authors. If your site accepts external submissions, sanitize the HTML with HTML Purifier or a similar library before output.
Filter entries
filters combines a field ID and an operator in a string.
$tech = genko_get('/blog', [ 'filters' => 'category[equals]tech', 'orders' => '-publishedAt', 'limit' => 10,]);
$recent = genko_get('/blog', [ 'filters' => 'publishedAt[greater_than]' . (new DateTimeImmutable('-30 days'))->format('Y-m-d\TH:i:s.v\Z'),]);See the query reference for the operators and how to combine conditions with [and] and [or].
Handle errors
| Status | Likely cause |
|---|---|
400 | Invalid query format, value, or operator |
401 | Missing, invalid, or expired API key |
403 | Key and workspace do not match |
404 | Workspace, API, or entry does not exist, or is outside the key's scope |
429 | More than 300 requests per minute with the same API key |
The client above puts the HTTP status in the GenkoApiError code. Branch on $error->getCode(). See Errors and limits for details.
Next steps
Continue with the tutorial for pagination, previewing saved drafts, and webhook signature verification.
Last updated: October 6, 2026