OMSCourse Search API
Guides

WordPress / PHP

A small plugin that keeps your API key on the server and gives your theme a safe search endpoint.

Request flow
Visitor's browser ──►  your WordPress  /wp-json/oms/v1/search   (no key)
                          │
                          ▼  Authorization: Bearer oms_live_…   (key stays on the server)
                     OMS Course Search API  POST /search
                          │
                          ▼
                     JSON back to WordPress ──► browser renders results

1. Add your key to wp-config.php

define('OMS_SEARCH_API_KEY', 'oms_live_...');

2. Add the plugin

Save as wp-content/plugins/oms-course-search/oms-course-search.php and activate it. It exposes POST /wp-json/oms/v1/search to your theme, stores each visitor's conversation token in a 2-hour transient, and never sends the API key or token to the browser.

<?php
/**
 * Plugin Name: OMS Course Search
 * Description: Server-side proxy to the OMS Course Search API. The API key never reaches the browser.
 */

// In wp-config.php:  define('OMS_SEARCH_API_KEY', 'oms_live_...');
const OMS_SEARCH_ENDPOINT = 'https://oms-api.mainconsulting.co.uk/api/public/v1/search';

function oms_search(string $query, ?string $context = null): array {
    $response = wp_remote_post(OMS_SEARCH_ENDPOINT, [
        'timeout' => 30,
        'headers' => [
            'Authorization' => 'Bearer ' . OMS_SEARCH_API_KEY,
            'Content-Type'  => 'application/json',
        ],
        'body' => wp_json_encode(['query' => $query, 'context' => $context]),
    ]);

    if (is_wp_error($response)) {
        return ['ok' => false, 'status' => 0, 'error' => ['code' => 'network_error', 'message' => $response->get_error_message()]];
    }

    $status = wp_remote_retrieve_response_code($response);
    $body   = json_decode(wp_remote_retrieve_body($response), true);

    if ($status !== 200) {
        // Log $body['request_id'] so OMS support can trace the request.
        error_log('OMS search failed: ' . $status . ' ' . ($body['request_id'] ?? '-') . ' ' . ($body['error']['code'] ?? '-'));
        return ['ok' => false, 'status' => $status, 'error' => $body['error'] ?? null, 'retry_after' => wp_remote_retrieve_header($response, 'retry-after')];
    }
    return ['ok' => true, 'data' => $body];
}

// Browser -> your WordPress -> OMS API. Register a public REST route for your front end.
add_action('rest_api_init', function () {
    register_rest_route('oms/v1', '/search', [
        'methods'             => 'POST',
        'permission_callback' => '__return_true',
        'callback'            => function (WP_REST_Request $request) {
            $query = sanitize_text_field((string) $request->get_param('query'));
            if ($query === '' || mb_strlen($query) > 500) {
                return new WP_REST_Response(['error' => 'Please enter a question (max 500 characters).'], 400);
            }
            // Keep the conversation token per visitor in a short-lived transient, not in the URL.
            $visitor = sanitize_key((string) $request->get_param('visitor')) ?: wp_generate_uuid4();
            $context = $request->get_param('new_search') ? null : get_transient('oms_ctx_' . $visitor);

            $result = oms_search($query, $context ?: null);
            if (!$result['ok']) {
                $code = $result['status'] === 429 ? 429 : 502;
                return new WP_REST_Response(['error' => 'Search is temporarily unavailable. Please try again.'], $code);
            }
            set_transient('oms_ctx_' . $visitor, $result['data']['context'], 2 * HOUR_IN_SECONDS);
            $data = $result['data'];
            unset($data['context']); // the browser never needs the token
            $data['visitor'] = $visitor;
            return new WP_REST_Response($data, 200);
        },
    ]);
});

3. Call it from your theme

// Browser code calls YOUR backend (e.g. the WordPress route above), never the OMS API directly.
async function searchCourses(query, { newSearch = false } = {}) {
  const res = await fetch("/wp-json/oms/v1/search", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ query, visitor: localStorage.getItem("omsVisitor"), new_search: newSearch }),
  });
  const data = await res.json();
  if (!res.ok) throw new Error(data.error ?? "Search failed");
  localStorage.setItem("omsVisitor", data.visitor);
  return data; // same shape as the API response, minus "context"
}
Escape all text when rendering (esc_html, esc_url in PHP, textContent in JavaScript). Treat API text as plain text, not HTML.

Plain PHP (without WordPress)

<?php
// Plain PHP (no WordPress). Requires the curl extension.
$apiKey = getenv('OMS_API_KEY');
$ch = curl_init('https://oms-api.mainconsulting.co.uk/api/public/v1/search');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey, 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS     => json_encode(['query' => 'I need first aid training for my staff.']),
]);
$body   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

$data = json_decode($body, true);
if ($status !== 200) {
    echo "Error {$status}: {$data['error']['code']} (request {$data['request_id']})\n";
    exit(1);
}
echo $data['answer'] . "\n";
foreach ($data['courses'] as $course) {
    echo "- {$course['name']} ({$course['course_url']})\n";
}