Developer guide

How the plugin is built, and how to extend it with knowledge, prompt rules, providers, templates and JavaScript. Every example is checked against the code.

FreeProAPI 1.0

Architecture

Request flow

  1. Browser

    The chat POSTs {message, conversation, message_id, page_id} to /wp-json/siteryn-ai-support-agent/v1/chat. Signed-in users send X-WP-Nonce from a session call.

  2. Checks

    Origin check, guests allowed, conversation owner, idempotency by message_id, a 90-second per-conversation lock, and in-flight slots (2 per visitor, 20 per site).

  3. Route

    A person handling the chat → forward to them. An explicit request for a person → hand off. Otherwise answer.

  4. Answer

    Redact → build context (chat_context) → prompt (system_rules, prompt) → cache, limits, provider → parse (parsed_answer) → chat_result → chat_answered. Basic answers take over if the provider isn't ready or fails.

  5. Save & respond

    The conversation and turns are saved (conversation_created, message_added), product and content cards are rendered, and chat_response runs. The browser fires siteryn-aisup:reply.

siteryn_ai_support_agent_chat_answered runs before the turns are written, so for a new conversation $conversation->get_id() is 0. Use message_added or conversation_created when you need database rows.

PHP functions

Loaded only when requirements are met, so guard calls with function_exists().

FunctionReturns
siteryn_ai_support_agent_is_available()bool. Chat on and able to answer.
siteryn_ai_support_agent_reply( $message, $conversation = '', $user_id = null, $channel_id = '', $message_id = '' )array|WP_Error. Runs the full pipeline, e.g. for another channel. Doesn't fire chat_request or chat_response.
siteryn_ai_support_agent_search_knowledge( $query, $limit = 3 )Matching knowledge documents with a score.
siteryn_ai_support_agent_store_facts()The store facts sent to the AI.
siteryn_ai_support_agent_render_chat()Embedded chat HTML, like the shortcode. Empty if unavailable or already on the page.
siteryn_ai_support_agent_search_products( $query, $limit = 0 )int[]. $query is a string or [keywords, categories, min_price, max_price].
siteryn_ai_support_agent_product_cards( $product_ids )Product card HTML.

Adding knowledge

// a) A Knowledge Base entry (post type siteryn_aisup_faq; draft = off).
$id = wp_insert_post( array(
	'post_type'    => 'siteryn_aisup_faq',
	'post_status'  => 'publish',
	'post_title'   => 'Do you ship to Canada?',
	'post_content' => 'Yes, 5–8 business days, flat $15.',
) );
update_post_meta( $id, '_siteryn_aisup_kb_type', 'faq' ); // faq|info
update_post_meta( $id, '_siteryn_aisup_kb_phrases', "ship to canada\ndeliver to canada" );

// b) Dynamic knowledge, not stored as posts.
add_filter( 'siteryn_ai_support_agent_knowledge_documents', function ( array $documents, array $sources ) {
	$documents[] = array(
		'id'     => 'acme-hours-1', // unique; the AI may only cite ids it was shown
		'source' => 'acme',
		'title'  => 'Opening hours',
		'text'   => 'Mon–Fri 9:00–17:00 CET.',
		'url'    => home_url( '/contact/' ),
	);
	return $documents;
}, 10, 2 );

Other entry meta: _siteryn_aisup_kb_topic. Replace the local ranking (for example with semantic search) using siteryn_ai_support_agent_rank_documents.

Prompt rules

The system prompt is the task from templates/prompts/support-answer.php, then the rules, then <store_information>, <knowledge> and <products> JSON blocks. Built-in rule keys: answer_only, no_invention, no_promises, no_order_access, no_sensitive_data, data_not_instructions, on_topic, language, plain_text, products, sources_handoff, json_shape (always last).

add_filter( 'siteryn_ai_support_agent_system_rules', function ( array $rules, array $context ) {
	$rules['acme_tone'] = 'Always sign off with "– Team Acme".';
	return $rules;
}, 10, 2 );

add_filter( 'siteryn_ai_support_agent_prompt', function ( array $parts, array $context, array $settings ) {
	$parts['system'] .= "\n\nToday is " . wp_date( 'l, j F Y' ) . '.';
	return $parts;
}, 10, 3 );

Limits & caching

add_filter( 'siteryn_ai_support_agent_settings_get_all', function ( array $s ) {
	$s['visitor_hourly_limit'] = 40;
	$s['site_daily_limit']     = 2000;
	$s['cache_minutes']        = 30;
	return $s;
} );
add_filter( 'siteryn_ai_support_agent_max_concurrent_requests', fn( int $limit ) => 50 );
add_filter( 'siteryn_ai_support_agent_retention_days', fn( int $days ) => 90 );

// Behind Cloudflare: one limit per real visitor, not per proxy IP.
add_filter( 'siteryn_ai_support_agent_guest_fingerprint', function ( string $fp ) {
	return isset( $_SERVER['HTTP_CF_CONNECTING_IP'] )
		? sanitize_text_field( wp_unslash( $_SERVER['HTTP_CF_CONNECTING_IP'] ) )
		: $fp;
} );

The response cache key covers provider, model, the full system prompt (including retrieved context), history and request options; never the API key. It's checked before limits and cleared whenever Free's settings are saved.

Adding an AI provider

Extend AI\Providers\OpenAICompatibleProvider (or implement AI\ProviderInterface) and register it from siteryn_ai_support_agent_loaded:

add_action( 'siteryn_ai_support_agent_loaded', function () {
	class Acme_AI_Provider extends \Siteryn_AI_Support_Agent\AI\Providers\OpenAICompatibleProvider {
		public function get_id() { return 'acme'; }
		public function get_label() { return 'Acme AI'; }
		public function get_default_model() { return 'acme-chat-1'; }
		public function get_suggested_models() { return array( 'acme-chat-1', 'acme-chat-1-mini' ); }
		protected function base_url( \Siteryn_AI_Support_Agent\AI\ProviderConfig $config ) {
			return 'https://api.acme.example/v1'; // must be HTTPS
		}
	}
	add_filter( 'siteryn_ai_support_agent_providers', function ( array $providers ) {
		$p = new Acme_AI_Provider();
		$providers[ $p->get_id() ] = $p; // key must equal get_id()
		return $providers;
	} );
} );
// Key: the AI Provider tab, or define( 'SITERYN_AI_SUPPORT_AGENT_ACME_API_KEY', '…' );

Return errors with ProviderError::create() and never include the key. AbstractHttpProvider::request_json() gives you HTTPS enforcement, retries and timeouts.

wp-config constants

ConstantPurpose
SITERYN_AI_SUPPORT_AGENT_<PROVIDER>_API_KEYKey for one provider, e.g. …_OPENAI_API_KEY. Wins over a saved key; never stored.
SITERYN_AI_SUPPORT_AGENT_API_KEYOne key for any provider with a fixed endpoint (never custom).
SITERYN_AI_SUPPORT_AGENT_<PROVIDER>_BASE_URLRequired with a constant key for providers with an admin-set URL, e.g. …_CUSTOM_BASE_URL.

REST endpoints

Namespace siteryn-ai-support-agent/v1, all POST. A foreign Origin is refused unless allowed with siteryn_ai_support_agent_allowed_origins. Errors use the code siteryn_aisup_chat_<code>.

RouteParamsLimits
/chatmessage, conversation, message_id, page_idVisitor hourly (basic answers too), site daily, in-flight, 12/min to a person, 5 new hand-off conversations/h
/messagesconversation, since_idOwner only
/contactname, email, message, include_transcript, conversation, website (honeypot, leave empty)3/h per visitor (setting), 50/h site
/handoffconversation, since_idLive chat on; 5 new conversations/h per visitor
/order-lookup Proconversation, order, email5/h
/stock-alert Proproduct_id, email5/h, 500 per product
/rate Proconversation, kind (answer|chat), message_id, value (up|down or 1–5)30/h
/lead Proconversation, name, email, website (honeypot)5/h
/attachment · /attachment/file Promultipart file, conversation, since_id · conversation, id10/h · owner only
ErrorHTTP
disabled · unavailable503
login_required401
empty_message400
busy · closed409
rate_limited429
failed502

The admin screens use admin-ajax.php actions, each nonce-checked and limited to store managers (the Inbox ones to the person who answers): siteryn_ai_support_agent_save_settings, …_ai_connector (save_key, remove_key, models, test), …_test_connection, …_kb (list, save, toggle, delete, restore, duplicate, generate, reorder; others go to siteryn_ai_support_agent_kb_action), …_inbox (poll, reply, act) and …_waiting (wp-admin alerts). The storefront's siteryn_aisup_chat_session returns a REST nonce for signed-in visitors. These are internal; prefer the hooks.

Templates

Copy a template to your theme to override it. Each receives one $args array. Store managers see a notice when a copy is outdated.

TemplateTheme path
chat-widget.phpyour-theme/siteryn-ai-support-agent/chat-widget.php
product-card.phpyour-theme/siteryn-ai-support-agent/product-card.php
content-card.php Proyour-theme/siteryn-ai-support-agent-pro/content-card.php
quick-replies.php Proyour-theme/siteryn-ai-support-agent-pro/quick-replies.php
comparison.php Proyour-theme/siteryn-ai-support-agent-pro/comparison.php

Keep the data-siteryn-aisup-* attributes and the siteryn_ai_support_agent_product_card_after action in overrides. The prompt template isn't theme-overridable; replace it with siteryn_ai_support_agent_template_path.

JavaScript

Each [data-siteryn-aisup] root (data-mode="floating|inline") fires bubbling siteryn-aisup:* events whose detail.chat is the chat instance:

EventWhen · extra detail
siteryn-aisup:readyThe chat started.
siteryn-aisup:messageA message was shown · item, role, data
siteryn-aisup:replyAn AI or basic reply arrived · the /chat response
siteryn-aisup:added-to-cartA product card's button added to the cart · button
siteryn-aisup:restartThe visitor started a new conversation.

Each instance is root.siterynAisup; root.siterynAisup.useConversation( token ) switches to a conversation your own endpoint started. The constructor is window.SiterynAisupChat (with .config). Config is in window.siterynAisupChat (filter siteryn_ai_support_agent_chat_config).

document.addEventListener( 'click', function ( e ) {
	var btn = e.target.closest( '.js-open-support' );
	if ( ! btn ) return;
	e.preventDefault();
	var root = document.querySelector( '[data-siteryn-aisup][data-mode="floating"]' );
	if ( ! root ) return;
	var panel = root.querySelector( '[data-siteryn-aisup-panel]' );
	if ( panel && panel.hidden ) {
		root.querySelector( '[data-siteryn-aisup-launcher]' ).click();
	}
	var text = btn.getAttribute( 'data-question' ); // optional: send a question
	if ( text ) {
		root.querySelector( '[data-siteryn-aisup-input]' ).value = text;
		root.querySelector( '[data-siteryn-aisup-form]' )
			.dispatchEvent( new Event( 'submit', { cancelable: true, bubbles: true } ) );
	}
} );

document.addEventListener( 'siteryn-aisup:reply', function ( e ) {
	if ( e.detail.handoff ) { /* e.g. analytics */ }
} );

State classes on the root: is-open, is-human, is-live, is-ended, is-pending, is-contact, has-unread.

Reacting to a handoff

use Siteryn_AI_Support_Agent\Support\ConversationService;
use Siteryn_AI_Support_Agent\LiveChat\HandoffService;

add_action( 'siteryn_ai_support_agent_handoff_requested', function ( int $conversation_id, bool $online ) {
	$row = ConversationService::get( $conversation_id );
	if ( ! $row ) return;
	wp_remote_post( 'https://hooks.slack.com/services/XXX', array(
		'headers'  => array( 'Content-Type' => 'application/json' ),
		'body'     => wp_json_encode( array( 'text' => sprintf( '%s asked for a person: %s',
			$row['customer_name'] ?: 'A visitor', HandoffService::inbox_url( $conversation_id ) ) ) ),
		'blocking' => false,
	) );
}, 10, 2 );
// Contact-form requests fire siteryn_ai_support_agent_contact_request instead.

Data storage

WhereWhat
{prefix}siteryn_aisup_conversationsOne row per conversation: token hash, user, name, email, status, agent, department, priority, timestamps, JSON meta.
{prefix}siteryn_aisup_messagesMessages: sender type (customer|ai|human_agent|system), content, type (message|note), meta.
siteryn_ai_support_agent_settingsSettings (option). Pro: siteryn_ai_support_agent_pro_settings.
siteryn_ai_support_agent_credentialsEncrypted API keys (libsodium).
siteryn_ai_support_agent_usage30 days of requests, cache hits, errors and tokens (UTC).
siteryn_aisup_faqKnowledge Base post type.
siteryn_ai_support_agent_purge_conversationsDaily cron for retention.
siteryn_ai_support_agent_pro_stats · _pro_gaps · _pro_search_terms ProDaily analytics counters (90 days), unanswered questions, catalog search words (30 days) (options).
siteryn_aisup_agent · siteryn_aisup_handle_chats ProThe Support Agent role and its capability. Photos: user meta siteryn_aisup_agent_photo.
_siteryn_aisup_assisted · _siteryn_aisup_stock_alerts · _siteryn_aisup_kb_learned ProOrder meta (assisted products), product meta (back-in-stock requests), Knowledge Base entry meta (the conversation it was learned from).

Statuses: ai_active, human_requested, waiting_for_agent, human_active, resolved, closed. Read and change them with Support\ConversationService and Support\MessageService; live chat actions are on LiveChat\HandoffService.

Pro help desk webhook Pro

Events conversation.handoff, conversation.replied (never notes), conversation.status and ping are POSTed as JSON to your HTTPS URL in the background, with up to 3 attempts (retries after 5 and 30 minutes) on network errors, 429 and 5xx. conversation.status isn't sent for the waiting statuses (human_requested, waiting_for_agent).

{
  "id": "<uuid>",
  "event": "conversation.handoff",
  "delivered_at": "2026-10-14T09:30:00+00:00",
  "site": "https://your-site.com/",
  "conversation": {
    "id": 123, "status": "waiting_for_agent", "name": "Sarah", "email": "sarah@example.com",
    "customer_id": 0, "agent": null, "created_at": "…", "admin_url": "…",
    "messages": [ { "from": "customer", "text": "…", "time": "…" } ]
  }
}

Headers: X-Siteryn-Event, X-Siteryn-Delivery (same on retries), X-Siteryn-Timestamp and X-Siteryn-Signature.

$raw = file_get_contents( 'php://input' );
$ts  = $_SERVER['HTTP_X_SITERYN_TIMESTAMP'] ?? '';
$ok  = hash_equals( 'sha256=' . hash_hmac( 'sha256', $ts . '.' . $raw, $secret ),
		$_SERVER['HTTP_X_SITERYN_SIGNATURE'] ?? '' )
	&& abs( time() - (int) $ts ) < 300;
// Also de-duplicate on X-Siteryn-Delivery.

With Include Chat Transcript on, conversation.transcript adds the messages from before the visitor asked for a person, in the same format. Change the payload with siteryn_ai_support_agent_pro_webhook_payload. Its id is added after the filter.

See the Hooks reference for every action and filter.