Example file
agent-tool-endpoint.php
PHP: the same agent tool endpoint, for PHP's built-in web server or your own.
<?php
// An endpoint for a Subcontext agent tool, in PHP 8.1 or later with no packages.
//
// It offers two operations you can add to a custom integration on Subcontext:
//
// GET /policies/{{ reference }} look up a policy (the agent sends date_of_birth too)
// POST /callbacks book a callback (the body comes from your body template)
//
// Subcontext sends your API key in the X-API-Key header (the integration's
// Auth type "Api key"). Writes carry an Idempotency-Key header that stays the
// same if the same tool call is sent again, so this file answers a repeat with
// the first answer instead of booking twice.
//
// Try it with PHP's built-in web server:
// SUBCONTEXT_TOOL_KEY=A_LONG_RANDOM_VALUE php -S localhost:4100 agent-tool-endpoint.php
// Subcontext only calls https addresses on the public internet, so in real use
// serve it from your usual HTTPS web server.
declare(strict_types=1);
// The value you store as a secret variable on the integration in Subcontext.
$apiKey = getenv('SUBCONTEXT_TOOL_KEY') ?: '';
// Stand-ins for your own systems.
$policies = [
'POL-1001' => ['date_of_birth' => '1980-04-12', 'status' => 'active',
'renewal_date' => '2027-03-01', 'monthly_premium' => '18.40'],
];
// Answers already given to writes, by Idempotency-Key. Use your database in real code.
$answeredFile = sys_get_temp_dir() . '/subcontext-tool-answers.json';
function sendJson(int $status, array $body): void
{
http_response_code($status);
header('Content-Type: application/json');
echo json_encode($body);
exit;
}
if ($apiKey === '') {
sendJson(500, ['error' => 'Set SUBCONTEXT_TOOL_KEY to the API key Subcontext sends.']);
}
// 1. Check the key. A 401 tells Subcontext the credentials are wrong.
$given = $_SERVER['HTTP_X_API_KEY'] ?? '';
if (!hash_equals($apiKey, $given)) {
sendJson(401, ['error' => 'Unknown API key.']);
}
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) ?: '/';
// 2. A read: GET /policies/POL-1001?date_of_birth=1980-04-12
if ($method === 'GET' && preg_match('#^/policies/([^/]+)$#', $path, $match)) {
$reference = strtoupper(rawurldecode($match[1]));
$policy = $policies[$reference] ?? null;
// Ask for something only the customer knows before giving anything away.
if ($policy === null || $policy['date_of_birth'] !== ($_GET['date_of_birth'] ?? '')) {
// The agent sees this status and message, so make the message useful.
sendJson(404, ['error' => 'No policy matches that reference and date of birth. Ask the customer to check both.']);
}
// Small, flat and only what the agent needs to answer.
sendJson(200, [
'reference' => $reference,
'status' => $policy['status'],
'renewal_date' => $policy['renewal_date'],
'monthly_premium' => $policy['monthly_premium'],
]);
}
// 3. A write: POST /callbacks with the body your template builds.
if ($method === 'POST' && $path === '/callbacks') {
$answered = is_file($answeredFile) ? (json_decode((string) file_get_contents($answeredFile), true) ?: []) : [];
$idempotencyKey = $_SERVER['HTTP_IDEMPOTENCY_KEY'] ?? '';
if ($idempotencyKey !== '' && isset($answered[$idempotencyKey])) {
sendJson($answered[$idempotencyKey]['status'], $answered[$idempotencyKey]['body']);
}
$body = json_decode((string) file_get_contents('php://input'), true);
if (!is_array($body)) {
sendJson(400, ['error' => "The request body wasn't valid JSON."]);
}
if (empty($body['phone'])) {
sendJson(422, ['error' => 'A phone number is needed to book a callback. Ask the customer for one.']);
}
$answer = [
'status' => 201,
'body' => [
'callback_id' => bin2hex(random_bytes(16)),
'message' => 'Callback booked for ' . ($body['preferred_time'] ?? 'the next available slot') . '.',
],
];
if ($idempotencyKey !== '') {
$answered[$idempotencyKey] = $answer;
file_put_contents($answeredFile, json_encode($answered), LOCK_EX);
}
sendJson($answer['status'], $answer['body']);
}
sendJson(404, ['error' => 'No such operation.']);