Time: 45 minutes | Difficulty: Beginner
Welcome to the first tutorial in our series on building AI agents! This tutorial is your comprehensive introduction to the Claude PHP SDK. We'll start with installation and basic usage, explore all the core features, and then introduce you to agentic AI concepts.
By the end, you'll have a solid foundation in both the SDK and the agent patterns that power the rest of this tutorial series.
By the end of this tutorial, you'll understand:
- How to install and configure the Claude PHP SDK
- Core SDK configuration options (API keys, timeouts, retries)
- How to make basic API requests to Claude
- The different Claude models and when to use each
- How to work with messages and conversation history
- How to handle responses and extract information
- Introduction to streaming for real-time responses
- Error handling patterns for robust applications
- The difference between chatbots and AI agents
- The ReAct (Reason-Act-Observe) pattern
- How tool use enables agent capabilities
- When to use agents vs simple API calls
Before getting started, ensure you have:
- PHP 8.1 or higher installed
- Composer for dependency management
- Anthropic API Key (Get one here)
Install the Claude PHP SDK using Composer:
composer require claude-php/claude-php-sdkCreate a .env file in your project root:
ANTHROPIC_API_KEY=sk-ant-your-api-key-here💡 Tip: Never commit your
.envfile to version control. Add it to.gitignoreto keep your API key secure.
Load the environment in your PHP code:
<?php
require 'vendor/autoload.php';
// Load environment variables
$envFile = __DIR__ . '/.env';
if (file_exists($envFile)) {
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) continue;
if (strpos($line, '=') === false) continue;
[$name, $value] = explode('=', $line, 2);
$_ENV[trim($name)] = trim($value);
}
}
use ClaudePhp\ClaudePhp;
$client = new ClaudePhp(
apiKey: $_ENV['ANTHROPIC_API_KEY']
);Note: The SDK also provides a helper in
/examples/helpers.phpwithloadEnv()andgetApiKey()functions for convenience.
The Claude PHP SDK provides flexible configuration options to customize the client behavior:
use ClaudePhp\ClaudePhp;
$client = new ClaudePhp(
apiKey: $_ENV['ANTHROPIC_API_KEY'] // API key (required)
);use ClaudePhp\ClaudePhp;
$client = new ClaudePhp(
apiKey: $_ENV['ANTHROPIC_API_KEY'], // API key
baseUrl: 'https://api.anthropic.com/v1', // Default: Anthropic API
timeout: 30.0, // Request timeout in seconds
maxRetries: 2, // Auto-retry on failures
customHeaders: [ // Additional headers
'X-Custom-Header' => 'value'
]
);| Option | Type | Default | Purpose |
|---|---|---|---|
apiKey |
string | ANTHROPIC_API_KEY env var |
Your API authentication key |
baseUrl |
string | https://api.anthropic.com/v1 |
API endpoint URL |
timeout |
float | 30.0 |
Request timeout in seconds |
maxRetries |
int | 2 |
Auto-retry on 429/5xx errors |
customHeaders |
array | [] |
Additional HTTP headers |
Best Practice: Keep default values for most use cases. Only adjust
timeoutfor very long responses ormaxRetriesfor unreliable networks.
Here's the simplest way to make a request to Claude:
use ClaudePhp\ClaudePhp;
$client = new ClaudePhp(apiKey: $_ENV['ANTHROPIC_API_KEY']);
$response = $client->messages()->create([
'model' => 'claude-sonnet-4-5',
'max_tokens' => 1024,
'messages' => [
[
'role' => 'user',
'content' => 'Hello, Claude! What can you tell me about PHP?'
]
]
]);
// Extract the response text
foreach ($response->content as $block) {
if ($block['type'] === 'text') {
echo $block['text'];
}
}💡 Quick Tip: The
contentarray can contain multiple blocks. Always check thetypefield before accessing block-specific properties.
Every request to the Messages API requires these parameters:
| Parameter | Type | Description |
|---|---|---|
model |
string | The model to use (e.g., 'claude-sonnet-4-5') |
max_tokens |
integer | Maximum tokens in the response (1-4096) |
messages |
array | Array of message objects with 'role' and 'content' |
Important:
max_tokensis always required. If the response is cut off, you'll getstop_reason: 'max_tokens'. Increase the limit if needed.
All API responses have this structure:
$response->id // Unique request ID
$response->type // Always "message"
$response->model // Model used
$response->content // Array of content blocks
$response->stop_reason // Why Claude stopped (see later)
$response->usage // Token usage statistics
->input_tokens // Tokens in prompt
->output_tokens // Tokens in responseClaude has multiple models optimized for different use cases:
$response = $client->messages()->create([
'model' => 'claude-sonnet-4-5-20250929', // Or use alias: 'claude-sonnet-4-5'
'max_tokens' => 1024,
'messages' => [
['role' => 'user', 'content' => 'Explain machine learning']
]
]);Best for:
- General-purpose tasks
- Balanced performance and cost
- Most use cases
- Complex reasoning
Trade-offs:
- Medium speed
- Medium cost
- Highest capability
$response = $client->messages()->create([
'model' => 'claude-haiku-4-5-20251001', // Or use alias: 'claude-haiku-4-5'
'max_tokens' => 1024,
'messages' => [
['role' => 'user', 'content' => 'What is 5+3?']
]
]);Best for:
- High-volume tasks
- Speed-critical applications
- Cost-sensitive workloads
- Simple questions
Trade-offs:
- Fastest response time
- Lowest cost
- Lower capability on complex tasks
$response = $client->messages()->create([
'model' => 'claude-opus-4-5-20251101',
'max_tokens' => 1024,
'messages' => [
['role' => 'user', 'content' => 'Your message']
]
]);Best for:
- Specialized scientific/technical tasks
- When you need maximum accuracy
- Research applications
| Task Type | Recommended Model | Reason |
|---|---|---|
| General Q&A | Sonnet 4.5 | Best balance |
| Quick answers | Haiku 4.5 | Fast & cheap |
| Complex reasoning | Sonnet 4.5 | Most capable |
| Math problems | Haiku 4.5 | Fast enough, cost-effective |
| Writing/summarization | Sonnet 4.5 | Best quality |
| Batch processing | Haiku 4.5 | Cost savings |
Model Aliases: You can use short aliases like
claude-sonnet-4-5instead of full model IDs. Aliases automatically point to the latest version of that model tier.
The simplest pattern: ask a question, get an answer.
$response = $client->messages()->create([
'model' => 'claude-sonnet-4-5',
'max_tokens' => 1024,
'messages' => [
['role' => 'user', 'content' => 'What is the capital of France?']
]
]);
echo $response->content[0]['text']; // Output: ParisBuild conversations by including the full message history:
$messages = [
['role' => 'user', 'content' => 'Hello, Claude'],
['role' => 'assistant', 'content' => 'Hello! How can I help you today?'],
['role' => 'user', 'content' => 'Can you explain LLMs?']
];
$response = $client->messages()->create([
'model' => 'claude-sonnet-4-5',
'max_tokens' => 1024,
'messages' => $messages
]);
// To continue the conversation, add the assistant's response
$messages[] = ['role' => 'assistant', 'content' => $response->content[0]['text']];
$messages[] = ['role' => 'user', 'content' => 'Can you give me an example?'];
// Make another request with the updated history
$response2 = $client->messages()->create([
'model' => 'claude-sonnet-4-5',
'max_tokens' => 1024,
'messages' => $messages
]);Important: The Messages API is stateless. You must provide the complete conversation history with each request. Claude doesn't remember previous conversations.
Performance Tip: Use Prompt Caching for long conversation histories to reduce costs by up to 90%.
System prompts guide Claude's behavior and personality:
$response = $client->messages()->create([
'model' => 'claude-sonnet-4-5',
'max_tokens' => 1024,
'system' => 'You are a helpful physics teacher. Explain concepts clearly.',
'messages' => [
['role' => 'user', 'content' => 'What is quantum entanglement?']
]
]);System prompts are useful for:
- Setting a specific personality or tone
- Providing context for the task
- Establishing rules for how to respond
- Improving response consistency
Guide Claude's response by prefilling the beginning:
$response = $client->messages()->create([
'model' => 'claude-sonnet-4-5',
'max_tokens' => 100,
'messages' => [
['role' => 'user', 'content' => 'List 3 programming languages:'],
['role' => 'assistant', 'content' => '1. Python\n2. '] // Prefilled start
]
]);
// Claude will complete from where you left off
// Output might be: "JavaScript\n3. Go"Prefilling helps with:
- Formatting responses in a specific way
- Getting single-word answers (combine with low max_tokens)
- Constraining output to specific formats
Use Case: Prefilling is especially useful for multiple-choice questions, structured output, or when you need responses in a specific format.
Responses can contain multiple content blocks. Always check the type:
$response = $client->messages()->create([
'model' => 'claude-sonnet-4-5',
'max_tokens' => 1024,
'messages' => [
['role' => 'user', 'content' => 'Hello!']
]
]);
// Responses are arrays of content blocks
foreach ($response->content as $block) {
if ($block['type'] === 'text') {
echo "Text: " . $block['text'];
}
// Later we'll see other types like 'tool_use' for agents
}The stop_reason tells you why Claude stopped generating:
$response = $client->messages()->create([
'model' => 'claude-sonnet-4-5',
'max_tokens' => 1024,
'messages' => [
['role' => 'user', 'content' => 'Tell me a story']
]
]);
if ($response->stop_reason === 'end_turn') {
echo "Claude finished naturally";
} elseif ($response->stop_reason === 'max_tokens') {
echo "Response was cut off - increase max_tokens";
}Possible stop reasons:
end_turn: Claude finished naturallymax_tokens: Response hit max_tokens limittool_use: Claude wants to call a tool (agents - covered later)
Every response includes token usage information:
$response = $client->messages()->create([
'model' => 'claude-sonnet-4-5',
'max_tokens' => 1024,
'messages' => [
['role' => 'user', 'content' => 'What is AI?']
]
]);
$inputTokens = $response->usage->input_tokens;
$outputTokens = $response->usage->output_tokens;
echo "Input tokens: {$inputTokens}\n";
echo "Output tokens: {$outputTokens}\n";
echo "Total tokens: " . ($inputTokens + $outputTokens) . "\n";Tokens represent small units of text. Token usage directly affects API costs, so it's important to monitor and optimize.
Cost Estimation: Roughly 1 token ≈ 4 characters (or 0.75 words). Use the token counting example to estimate costs before running large batches.
Streaming is useful for:
- Displaying responses as they're generated (better UX)
- Long responses (users see progress)
- Real-time applications (chatbots, assistants)
use ClaudePhp\Lib\Streaming\MessageStream;
$rawStream = $client->messages()->stream([
'model' => 'claude-sonnet-4-5',
'max_tokens' => 1024,
'messages' => [
['role' => 'user', 'content' => 'Tell me a short story']
]
]);
$stream = new MessageStream($rawStream);
// Display text as it arrives
echo $stream->textStream();| Aspect | Streaming | Non-Streaming |
|---|---|---|
| Latency | Lower (progressive) | Higher (wait) |
| UX | Better (real-time) | Basic |
| Complexity | More code | Simpler |
| Response time | First token fast | All at once |
| Accumulation | Need to build | Complete object |
When to Stream: Use streaming for user-facing applications where real-time feedback improves UX. Use non-streaming for batch processing or when you need the complete response immediately.
For now, focus on non-streaming. We'll explore streaming in depth in later examples (streaming_basic.php, streaming_comprehensive.php).
The SDK provides specific exceptions for different error types:
AnthropicException (base)
├── AuthenticationError (invalid API key)
├── RateLimitError (too many requests)
├── APIConnectionError (network issues)
├── APITimeoutError (request timeout)
└── APIError (other API errors)
Error Handling Strategy: Always catch specific exceptions first (most specific to least specific) to handle different errors appropriately.
use ClaudePhp\Exceptions\{
AnthropicException,
AuthenticationError,
RateLimitError,
APIConnectionError
};
try {
$response = $client->messages()->create([
'model' => 'claude-sonnet-4-5',
'max_tokens' => 1024,
'messages' => [
['role' => 'user', 'content' => 'Hello!']
]
]);
} catch (AuthenticationError $e) {
echo "API key is invalid. Check ANTHROPIC_API_KEY";
} catch (RateLimitError $e) {
echo "Rate limited. Retry after: " . $e->response->getHeaderLine('retry-after');
// Implement exponential backoff
} catch (APIConnectionError $e) {
echo "Network error: " . $e->getMessage();
} catch (AnthropicException $e) {
echo "Unexpected error: " . $e->getMessage();
}function makeRequestWithRetry($client, $params, $maxRetries = 3) {
$attempt = 0;
while ($attempt < $maxRetries) {
try {
return $client->messages()->create($params);
} catch (RateLimitError $e) {
$attempt++;
if ($attempt >= $maxRetries) throw $e;
// Exponential backoff
sleep(2 ** $attempt);
} catch (APITimeoutError $e) {
$attempt++;
if ($attempt >= $maxRetries) throw $e;
// Retry on timeout
sleep(1);
}
}
}Controls response randomness (0.0 = deterministic, 1.0 = creative):
// Deterministic responses (best for facts)
$response = $client->messages()->create([
'model' => 'claude-sonnet-4-5',
'max_tokens' => 1024,
'temperature' => 0.0, // Always the same answer
'messages' => [
['role' => 'user', 'content' => 'What is 2+2?']
]
]);
// Creative responses (best for writing)
$response = $client->messages()->create([
'model' => 'claude-sonnet-4-5',
'max_tokens' => 1024,
'temperature' => 1.0, // Different each time
'messages' => [
['role' => 'user', 'content' => 'Write a poem about coding']
]
]);When to adjust:
- Factual Q&A → Use 0.0-0.3
- Creative writing → Use 0.7-1.0
- Default (1.0) → Good for most tasks
Note: Temperature 1.0 is the default. Lower temperatures make responses more focused and deterministic, which is useful for tasks requiring consistency.
Now that you understand the basics of the Claude PHP SDK, let's explore what makes AI systems "agentic" and why agents are more powerful than simple chatbots.
Think of the difference like this:
🤖 Chatbot (Traditional LLM Use):
- You ask a question → It responds
- Single turn interaction
- Relies only on its training data
- Cannot take actions or gather new information
- Passive responder
🧠 AI Agent (Agentic System):
- You give a goal → It figures out how to achieve it
- Multi-turn autonomous operation
- Can use tools to gather information or take actions
- Makes decisions about next steps
- Active problem solver
Chatbot Interaction:
You: "What's the weather in San Francisco?"
Bot: "I don't have access to real-time weather data.
I was last trained in [date]..."
Agent Interaction:
You: "What's the weather in San Francisco?"
Agent: [Thinks: I need current weather data]
[Acts: Calls weather API for San Francisco]
[Observes: API returns 68°F, sunny]
[Responds: "It's currently 68°F and sunny in San Francisco"]
ReAct (Reason-Act-Observe) is the fundamental pattern that powers agentic behavior. It's a loop that continues until the task is complete:
┌─────────────────────────────────────────┐
│ 1. REASON (Think) │
│ "What do I need to do next?" │
│ "What information do I need?" │
└──────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 2. ACT (Execute) │
│ "Call a tool to get information" │
│ "Perform an action" │
└──────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 3. OBSERVE (Analyze) │
│ "What did the tool return?" │
│ "Do I have enough information?" │
└──────────────┬──────────────────────────┘
│
▼
┌──────────────┐
│ Complete? │
└──────┬───────┘
│
┌──────┴──────┐
│ │
No Yes
│ │
│ ▼
│ ┌──────────────┐
│ │ Respond to │
│ │ User │
│ └──────────────┘
│
└──────> (Loop back to REASON)
Let's see how the ReAct loop works for a complex task:
Task: "Book me a flight to New York tomorrow"
Iteration 1:
REASON: "I need to know the user's departure city and preferred time"
ACT: Ask user for details
OBSERVE: User provides "San Francisco, morning flight"
Iteration 2:
REASON: "Now I can search for flights"
ACT: Call flight search API
OBSERVE: Found 3 morning flights
Iteration 3:
REASON: "I should present options and get confirmation"
ACT: Show flight options to user
OBSERVE: User selects 7:30 AM flight
Iteration 4:
REASON: "Now I can book the selected flight"
ACT: Call booking API
OBSERVE: Booking confirmed
COMPLETE: "Your flight is booked! Confirmation #ABC123"
Notice how the agent autonomously decided what information to gather, which APIs to call, and when the task was complete. This is the essence of agentic behavior.
Tools are functions that Claude can call to:
- Get Information: Weather, stock prices, database queries
- Take Actions: Send emails, book appointments, make purchases
- Compute: Math calculations, data analysis, code execution
- Interact: Web search, API calls, file operations
A tool definition tells Claude:
[
'name' => 'get_weather', // What to call it
'description' => 'Get current weather // What it does
for a location',
'input_schema' => [ // What parameters it needs
'type' => 'object',
'properties' => [
'location' => [
'type' => 'string',
'description' => 'City name'
]
],
'required' => ['location']
]
]1. You provide tools to Claude
↓
2. Claude decides if/when to use them
↓
3. Claude requests tool execution with parameters
↓
4. Your code executes the tool
↓
5. You return results to Claude
↓
6. Claude uses results to formulate response
Key Insight: You define what tools are available, but Claude decides when and how to use them. This autonomy is what makes the system "agentic."
- Research Tasks: "Find the top 5 ML papers on agent architectures"
- Multi-step Workflows: "Analyze this dataset and create a report"
- Dynamic Problem Solving: "Debug why the API is returning errors"
- Information Gathering: "Compare prices across 3 vendors"
- Task Automation: "Summarize my emails and draft responses"
- Simple Q&A: "What is Python?" → Direct response is fine
- Static Content: "Translate this text" → No tools needed
- Real-time Chat: High latency from multiple iterations
- Deterministic Tasks: "Calculate 2+2" → Tool call overhead unnecessary
- Cost-Sensitive: Agents use more tokens (more iterations)
| Scenario | Simple API Call | Agent |
|---|---|---|
| User asks for definition | ✅ | ❌ |
| User needs current data | ❌ | ✅ |
| Multi-step reasoning | ❌ | ✅ |
| Needs to take action | ❌ | ✅ |
| Simple calculation | ✅ | ❌ |
| Complex workflow | ❌ | ✅ |
Agents make decisions about what to do and when to do it. You don't script every step; you give them capabilities and a goal.
Tools extend Claude's capabilities beyond its training data. They're the "hands and eyes" of your agent.
Agents maintain conversation history across turns. Each iteration builds on previous observations.
Agents need to know when they're done. This could be:
- Task completed successfully
- Maximum iterations reached
- Error encountered
- User explicitly stops
Always set max iterations to prevent infinite loops:
$maxIterations = 10; // Safety limitAgents use more tokens because:
- Multiple API calls (each iteration)
- Tool definitions in every request
- Growing conversation history
- Special system prompts for tool use
Example: A simple question might use 500 tokens, while an agent task could use 5,000+ tokens.
Each iteration adds ~1-3 seconds. A 5-iteration agent task takes 5-15 seconds.
More complexity = more failure points:
- Tools can fail
- APIs can timeout
- Agent might get stuck
- Need robust error handling
- Start Simple: Begin with one tool, add complexity gradually
- Clear Tool Descriptions: Help Claude choose the right tool
- Validate Input: Check tool parameters before execution
- Handle Errors Gracefully: Tools will fail; plan for it
- Limit Iterations: Prevent runaway loops
- Log Everything: Debug agents by reviewing their reasoning
- Test Edge Cases: What if tools return errors? Empty results?
When debugging, look at:
- Tool Selection: Did Claude pick the right tool?
- Parameters: Are the inputs correct?
- Tool Results: What data came back?
- Stop Reason: Why did it stop? (
tool_use,end_turn,max_tokens) - Iteration Count: Did it hit the limit?
- Token Usage: Are you approaching limits?
We'll explore these in later tutorials:
Your first working agent with a calculator tool
Chooses from multiple tools intelligently
Error handling, retries, memory
Planning, reflection, extended thinking
Task decomposition, parallel execution, orchestration
Before moving on, here's a quick reference to help you decide when to use SDK basics vs agents:
| Scenario | SDK Approach | Agent Approach |
|---|---|---|
| "What is PHP?" | ✅ Simple API call with system prompt | ❌ Overkill |
| "What's the weather in Paris?" | ❌ Can't access real-time data | ✅ Agent with weather tool |
| "Translate this text" | ✅ Simple API call | ❌ No tools needed |
| "Summarize and email this report" | ❌ Multiple steps needed | ✅ Agent with tools |
| "Calculate 123 × 456" | ✅ Claude can do this mentally | |
| "Research and compare 3 products" | ❌ Needs external data | ✅ Agent with search tools |
Rule of Thumb: If the task requires external data, multiple steps, or conditional logic, use an agent. Otherwise, a simple SDK call is sufficient.
Before moving on, make sure you understand:
SDK Fundamentals:
- How to install and configure the Claude PHP SDK
- The required parameters for making API requests
- How to access response content and metadata
- The different Claude models and when to use each
- How to handle conversation history
- When to use system prompts and response prefilling
- Basic streaming vs non-streaming trade-offs
- Error handling patterns and exception types
- How to monitor token usage
Agentic AI Concepts:
- Difference between chatbots and agents
- What the ReAct pattern is
- How tools enable agent capabilities
- When to use agents vs simple API calls
- Why iteration limits are important
Ready to build your first agent? Continue to:
Tutorial 1: Your First Agent →
You'll build a working agent with a calculator tool and see the ReAct loop in action!
SDK Documentation:
- Anthropic API Documentation
- SDK Examples: basic_request.php
- SDK Examples: get_started.php
- SDK Examples: error_handling.php
Agent Concepts:
Run the companion code examples:
# See all the concepts in action
php tutorials/00-introduction/concepts.phpThis will demonstrate both SDK basics and agentic concepts with working code examples!