Domain 2 — Applications and Integration — is worth roughly a third of the CCDV-F exam, more than any other domain by a wide margin. That weight tells you something about what this certification is actually testing: not trivia about Anthropic's product catalog, but whether you can take a business requirement, translate it into a working call against platform.claude.com, and ship it the way you'd ship any other production dependency — with retries that don't retry the wrong things, secrets that never land in git, and a config story that survives a code review.
This guide is an independent, unofficial study resource. It is not affiliated with, authorized by, or endorsed by Anthropic, and it reproduces no real exam items. The API mechanics described here — request/response shape, streaming events, error codes, auth methods — are grounded in the current public documentation at platform.claude.com as of mid-2026; general software-engineering material (version control, code review, SDLC) is standard industry practice applied to this specific integration surface.
The six sub-objectives in this domain split cleanly into three layers: requirements and lifecycle (why you're building this, and where it sits in your delivery process), API mechanics (what actually goes over the wire), and engineering discipline (how you keep it correct and maintainable once it's shipped). Treat every section below as a production decision, not a fact to memorize — the exam rewards knowing why you'd choose streaming over batch, not just that both exist.
From requirement to system: scoping a Claude integration
Before any code gets written, a Claude integration needs the same requirements pass any external dependency needs — plus a few questions that are specific to calling an LLM in a request path. Skipping this step is the single most common source of rework: teams build a synchronous chat-style integration, then discover in staging that their real workload is a nightly batch job, or they hardcode a model choice that turns out to be wrong for their latency budget.
- Latency tolerance. Is this behind a user-facing request (seconds matter) or a background job (minutes are fine)? This single answer drives the streaming-vs-batch decision covered later in this guide.
- Volume and concurrency. Ten requests a day and ten thousand a minute imply completely different architectures — the second needs you to think about rate-limit tiers and token-bucket behavior from day one, not after the first 429.
- Data sensitivity. Does the request body contain PII, regulated data, or secrets? This shapes logging policy, retention configuration, and which auth method is appropriate.
- Determinism and verifiability. Can a wrong answer be caught before it causes harm (a human review step, a test, a rollback), or does it go straight to an end user or a downstream system with side effects?
- Autonomy required. Does the task need a single bounded call, a multi-step workflow your code orchestrates, or open-ended agentic tool use? Default to the least autonomous shape that satisfies the requirement — it's cheaper to reason about, cheaper to test, and cheaper to debug.
| Shape | Who controls the loop | Fits when |
|---|---|---|
| Single call | Your code, one request/response | Bounded task with a clear input and output — classification, extraction, a single generation |
| Workflow | Your code, multiple calls in a fixed sequence | Multi-step but predictable — summarize, then classify, then format |
| Agent (tool use) | The model, within a loop your code still terminates | The steps needed can't be fully specified up front and depend on intermediate results |
This maps onto a standard systems development life cycle — requirements, design, build, test, deploy, operate — with one addition: an LLM integration needs an explicit evaluation step that a typical CRUD feature doesn't. Before build sign-off, you want a small, versioned set of representative inputs and a way to judge outputs against them, even if that judgment is a human rubric rather than an assertion. Treat that eval set as a project artifact from day one; it becomes your regression suite the first time someone proposes a prompt change or a model upgrade.
Requirements gathering for an LLM feature also has a communication dimension that's easy to skip: stakeholders who haven't built with a model before will often describe the requirement in terms of a specific output string ("it should say exactly this") rather than a behavior ("it should correctly identify the account tier and phrase the response politely"). Part of the developer's job during requirements is translating the first framing into the second — an LLM integration that's specified as producing exact strings is either over-constrained into something a template would do more cheaply, or it's quietly going to fail every time phrasing varies. Write requirements as acceptance criteria an eval can check, not as example transcripts to match verbatim.
Systems life cycle: shipping a Claude-powered feature stage by stage
Section one of this guide sketched requirements-through-operate as a single paragraph; this section slows down and walks each stage of the systems development life cycle on its own terms, because it's graded as its own sub-objective. None of the stages below are unique to Claude — every one of them exists in a standard SDLC for any service dependency. What's specific to an LLM integration is the one thing that's different or additional at each stage, and that's the part worth being able to name precisely rather than gesture at.
| Stage | Standard practice | What's different with Claude in the loop |
|---|---|---|
| Requirements | Functional requirements: inputs, outputs, who consumes the result | Also capture model-behavior requirements — acceptable failure modes and what "good enough" output looks like — because these are harder to spec precisely than a typical software requirement |
| Design | Component boundaries, data flow, error-handling strategy | Decide the architecture shape up front: single call, workflow, or agent (see the requirements section earlier in this guide), plus the error-handling strategy for retryable vs. permanent failures |
| Build | Implement against the design, write code alongside tests | Implement against a small, fixed eval set from day one — not by eyeballing outputs and calling it done once a few examples look right |
| Test | Unit and integration tests with deterministic assertions | Add an eval suite that compares actual model outputs against expected characteristics, not exact-match — deterministic checks where the output shape allows them, judgment-based checks where it doesn't |
| Staging / deploy | Standard rollout — feature flags, canaries, blue/green | Canary or gradual rollout specifically for prompt and model changes — a change that passes a small eval set can still silently regress on the diversity of real traffic |
| Monitor / iterate | Error-rate and latency dashboards, on-call alerting | Track cost and token usage as first-class production metrics alongside latency and error rate, and treat every prompt or model change with the same regression-testing rigor as a code change |
Two stages deserve a second look because they're where teams most often quietly skip the LLM-specific step and fall back to "looks fine to me." Test means more than confirming the code compiles and the happy path returns a 200 — it means running the eval set built during requirements and build against the actual model output, checking for the right characteristics (does the response correctly identify the account tier? does it avoid a specific factual error class?) rather than an exact string match, since two correct answers can be phrased differently. Where a characteristic can be checked deterministically — a required field present, a value within a valid enum, a response under a length bound — do that in code; reserve human or model-graded judgment for what genuinely needs it.
Staging and deploy for a prompt or model change is not a binary flip. Because the exact same fixed eval set that passed in build can still miss a regression that only shows up against the diversity of production traffic, a gradual or canary rollout — a small percentage of real traffic first, widening only once the metrics hold — catches what a static eval set can't. Once live, the monitoring dashboard for a Claude-powered feature needs three metrics a typical CRUD endpoint doesn't track as centrally: cost per request, token usage trends (which move independently of request volume if prompts or outputs drift longer), and latency broken out by whether the request streamed. Error rate stays standard, but classify it the same retryable-vs-permanent way covered later in this guide, or your alerting will page someone for a 400 that a code fix — not a retry — will resolve.
Messages API mechanics: what's actually on the wire
Anthropic's API documentation now lives at platform.claude.com (moved from the earlier docs.anthropic.com). Every Claude integration — single call, workflow, or agent — is built on one endpoint: POST /v1/messages. Everything else in this domain is really about how you shape requests to it and how you handle what comes back.
| Field | Required? | Notes |
|---|---|---|
model | Yes | Model identifier string |
max_tokens | Yes | Hard cap on output length — the request fails without it |
messages | Yes | Array alternating user/assistant roles |
system | No | System prompt, kept separate from the conversation turns |
tools / tool_choice | No | Declares callable tools and how the model should use them |
thinking | No | Enables extended/adaptive reasoning before the final answer |
cache_control | No | Marks a prefix as cacheable to cut latency and cost on repeated context |
stream | No | true switches the response to Server-Sent Events |
Headers matter as much as the body: every request carries x-api-key for authentication and anthropic-version to pin the API's dated release. Pinning the version header is a configuration-management decision as much as an API detail — it means an Anthropic-side change can't silently alter your response shape until you deliberately bump it.
The response content array is typed by block: text for ordinary prose, image and document for multimodal input echoed back in certain flows, tool_use when the model wants to call a tool, tool_result for what you send back, and thinking for extended-reasoning output. Code that blindly reads content[0].text breaks the moment a response contains a thinking block ahead of the text — always branch on .type before reading a block's payload.
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from the environment
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
system="You are a support ticket triage assistant.",
messages=[
{"role": "user", "content": "Customer says their export is stuck at 90%."}
],
)
# Never assume content[0] is text -- branch on block type first.
for block in response.content:
if block.type == "text":
print(block.text)
print(response.stop_reason) # why generation stopped
print(response.usage) # input/output/cache token countsimport Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic(); // reads ANTHROPIC_API_KEY from the environment
const response = await client.messages.create({
model: 'claude-sonnet-4-5',
max_tokens: 1024,
system: 'You are a support ticket triage assistant.',
messages: [
{ role: 'user', content: "Customer says their export is stuck at 90%." },
],
});
// Never assume content[0] is text -- branch on block type first.
for (const block of response.content) {
if (block.type === 'text') {
console.log(block.text);
}
}
console.log(response.stop_reason); // why generation stopped
console.log(response.usage); // input/output/cache token countsstop_reason | Meaning |
|---|---|
end_turn | The model finished naturally |
max_tokens | Hit the output cap — the response may be cut off mid-thought |
stop_sequence | A configured stop string was reached |
tool_use | The model wants to call a tool; execute it and continue the loop |
pause_turn | A long-running agentic turn paused and can be resumed |
refusal | The model declined to continue for safety reasons |
The usage object is where cost and cache behavior become observable: input_tokens and output_tokens are the baseline counts, while cache_creation_input_tokens and cache_read_input_tokens tell you whether a cache_control breakpoint actually paid off. If cache_read_input_tokens stays at zero across requests that should share a prefix, something in your prompt-assembly code is silently breaking the cache — a timestamp, a non-deterministic JSON serialization, or a reordered tool list are the usual culprits.
One structural point worth internalizing for the exam: system is a separate top-level field, not the first entry in messages. This isn't cosmetic — it's how the API distinguishes standing instructions (which render first in the cache-relevant ordering, and stay stable across a whole session) from the actual conversation turns (which change every request). Mixing the two — folding system-level instructions into the first user message — works, but it forfeits the caching and ordering benefits the API is designed around, and it makes the request harder to reason about later.
Streaming vs. Batch: matching execution mode to the workload
Once the request shape is settled, the next decision is how the request executes. This is where the "latency tolerance" question from requirements gathering pays off directly: interactive workloads stream, high-volume latency-insensitive workloads batch, and getting this wrong is a common source of production incidents that look like flaky infrastructure but are actually a design mismatch.
| Mode | Best for | Key constraint |
|---|---|---|
| Synchronous (non-streaming) | Short, bounded requests where the whole answer is needed at once | Non-streaming calls with a large max_tokens risk hitting idle-connection timeouts around 10 minutes |
| Streaming (SSE) | User-facing generation, long outputs, anything where partial results are useful | You own the client-side accumulation of the response as it arrives |
| Batch API | Bulk, latency-insensitive processing — classification sweeps, offline scoring, backfills | Supports up to 300K output tokens (beta) vs. 128K on synchronous calls; results aren't real-time |
Streaming responses arrive as a fixed sequence of Server-Sent Events: message_start opens the response, then for each content block a content_block_start / content_block_delta* / content_block_stop cycle streams the block incrementally, message_delta carries top-level changes (including the final stop_reason and usage), and message_stop closes the response. A client that only listens for content_block_delta and ignores message_start/message_stop will work most of the time and then mysteriously drop the first or last chunk under load — handle the full event sequence, not just the deltas.
with client.messages.stream(
model="claude-sonnet-4-5",
max_tokens=4096,
messages=[{"role": "user", "content": "Draft a release note for v2.3.0."}],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
# get_final_message() gives you the fully accumulated response --
# use this even in a streaming flow if you need usage/stop_reason
# rather than hand-rolling event accumulation yourself.
final = stream.get_final_message()
print("\n---")
print(final.stop_reason, final.usage)The Batch API's headline trade is throughput for latency: it's designed for high-volume, offline work, not for anything a user is waiting on. If a requirement calls for processing tens of thousands of independent documents overnight, batch is very likely the right shape — building that as ten thousand synchronous calls in a loop wastes both money and reliability budget you didn't need to spend.
Errors: retryable vs. permanent, and the idempotency trap
Error handling is one of the most exam-relevant parts of this domain because it's where a plausible-sounding wrong answer is easy to construct: not every error should be retried, and the API gives you no built-in protection against a tool call running twice.
| Code | Error type | Retry? |
|---|---|---|
| 400 | invalid_request_error | No — the request itself is malformed |
| 401 | authentication_error | No — fix credentials first |
| 402 | billing_error | No |
| 403 | permission_error | No |
| 404 | not_found_error | No — usually a bad model ID or endpoint |
| 409 | conflict_error | No |
| 413 | request_too_large | No — reduce payload size |
| 429 | rate_limit_error | Yes — back off and retry |
| 500 | api_error | Yes |
| 504 | timeout_error | Yes |
| 529 | overloaded_error | Yes |
import time
import random
import anthropic
def call_with_retry(client, max_retries=5, base_delay=1.0, max_delay=60.0, **kwargs):
last_exception = None
for attempt in range(max_retries):
try:
return client.messages.create(**kwargs)
except anthropic.RateLimitError as e:
last_exception = e
except anthropic.APIStatusError as e:
if e.status_code >= 500:
last_exception = e
else:
raise # 4xx other than 429: do not retry
delay = min(base_delay * (2 ** attempt) + random.uniform(0, 1), max_delay)
time.sleep(delay)
raise last_exceptionThe Anthropic SDKs (Python, TypeScript, C#, Go, Java, PHP, Ruby) already implement this pattern by default — typed errors, streaming support, and automatic retry with backoff are built in. Custom retry logic is only justified when you need behavior the SDK doesn't offer: a different backoff curve, custom telemetry per attempt, or coordinating retries across a multi-step workflow rather than a single call.
In practice this means: for any tool whose execution isn't purely read-only, generate a request-scoped idempotency key in your own code before the first attempt, and check-and-set against a store you control (a database unique constraint, a cache with a TTL) inside the tool's handler — not inside the model's control flow, which you don't own and can't guarantee runs exactly once.
Authentication and secrets: keys, workload identity, and what never touches git
Three authentication methods cover essentially every deployment shape a Claude integration will need, and picking the right one for the environment is a configuration-management decision, not just a security checkbox.
| Method | Shape | Best for |
|---|---|---|
| Static API keys | sk-ant-api..., set via ANTHROPIC_API_KEY | Development, simple deployments — choose an explicit expiration (3h/1d/7d/30d/custom/Never) at creation time |
| Workload Identity Federation | Exchanges a cloud IdP JWT for short-lived tokens | Production and CI — no static secret to leak or rotate |
| App Attest | Hourly-expiring, Messages-API-scoped only | Native iOS/macOS clients calling the API directly from the device |
Where a static key is genuinely the right call, the operational baseline is unremarkable but non-negotiable: store it in a secrets manager, never hardcode it in source, never commit it (even to a private repo — private is not the same as safe), and set the shortest expiration that's practical for the workflow. A key with a 30-day expiration that nobody remembers to rotate is functionally a permanent secret with extra steps.
- Read the key from environment or a secrets manager at process start — never construct it from string concatenation or a config file checked into version control.
- Scope keys per-environment (dev/staging/prod) so a leaked staging key can't touch production traffic.
- Log request IDs, not request bodies, when the body might contain the key or sensitive user data — application logs are a common leak vector that has nothing to do with the API itself.
- For CI/CD pipelines specifically, prefer Workload Identity Federation over injecting a static key as a pipeline secret — it removes a secret that would otherwise need rotation on every credential-management schedule.
Application design: SDKs, layering, and testable integrations
Anthropic ships official SDKs for Python, TypeScript, C#, Go, Java, PHP, and Ruby, plus an ant CLI — all with streaming, typed errors, and auto-retry built in. Reaching for the SDK instead of hand-rolling HTTP calls is the default, not a preference: you inherit correct backoff behavior, typed exceptions you can catch specifically (rather than string-matching error messages), and response parsing that tracks the API's actual shape.
The design question that matters most for a maintainable integration is where the API call lives in your application's layering. A common anti-pattern is calling client.messages.create(...) directly from wherever the feature happens to need an LLM response — a request handler, a background job, a UI event handler — scattered across the codebase. That makes three things hard: swapping models later, adding a caching layer, and — critically for testing — mocking the API in unit tests without also mocking half the surrounding feature.
| Layer | Owns | Why it's separated |
|---|---|---|
| Feature code | Business logic: when to call, what to do with the result | Doesn't need to know about retries, headers, or SDK internals |
| Integration/service layer | The messages.create call itself, error translation, retry policy | One place to change model, add caching, or swap the SDK version |
| Config layer | Model ID, max_tokens, timeout, which environment's key | Changes without a code deploy; testable independently |
This layering pays off directly in testing strategy: with the API call isolated behind a service boundary, unit tests for feature code can mock that boundary and assert on business logic (did the ticket get routed correctly given a mocked classification result?) without making real network calls, burning real tokens, or being flaky because a live model's exact phrasing changed. Reserve actual API calls for a smaller set of integration tests that verify the service layer's request-building and error-translation logic against the real endpoint — ideally against a fixed, versioned eval set rather than ad hoc prompts.
Each official SDK maps API error codes to typed exception classes rather than leaving you to string-match a message field — a RateLimitError, an AuthenticationError, a NotFoundError, and so on, all deriving from a common base error type. Catching the specific type you expect and handling a broader base type as a fallback is more robust than any string comparison, and it's what makes the retryable-vs-permanent distinction from the previous section straightforward to implement correctly: except RateLimitError and except APIStatusError where status_code >= 500 are retry branches; everything else the SDK surfaces as a client error is not.
- Wrap the SDK call in your service layer, not in feature code — one place to catch, translate, and log errors consistently.
- Preserve the original error's status code and type when you translate it into an application-level error — downstream code (and whoever's debugging an incident at 2am) needs that information, not a flattened generic exception.
- Log the request ID the SDK exposes on every response and error — it's the identifier you'd hand to support if you ever needed to trace a specific call.
- Keep tool-execution code (the functions your tools actually call) separate from the loop that drives the conversation — this is what makes a tool testable in isolation, independent of whether the model decided to call it correctly.
Configuration management: pinning, environments, and change control
Configuration management for a Claude integration covers three things that are easy to under-invest in because none of them cause a bug on day one: version pinning, environment separation, and treating prompts as versioned artifacts rather than incidental strings.
- Pin the `anthropic-version` header explicitly. This is your contract with the API's response shape. Bump it deliberately, in a change you can review and roll back, not implicitly by upgrading a dependency.
- Keep model IDs in configuration, not hardcoded in feature code. A model upgrade should be a config change plus a re-run of your eval set, not a code change scattered across every call site.
- Separate configuration per environment. Dev, staging, and production should each have their own API key, their own rate-limit budget awareness, and — where behavior differs — their own
max_tokensor timeout settings, sourced from environment-specific config rather than conditional logic in the code. - Version your prompts alongside your code. A system prompt is a functional part of the application, not a comment. Changing it should go through the same code review and the same eval-set regression check as any other logic change — a prompt edit that silently degrades output quality is a shipped bug, even though nothing 'broke' in the traditional sense.
| Change | Review needed? | Rollback path |
|---|---|---|
| Model ID upgrade | Yes — re-run eval set, review cost/latency delta | Config revert to prior model ID |
| System prompt edit | Yes — same rigor as a logic change | Git revert; redeploy config |
anthropic-version bump | Yes — check for response-shape changes in changelog | Pin back to prior dated version |
| Retry/backoff tuning | Lightweight — verify against rate-limit headers | Config revert |
The software-engineering foundations this domain expects — version control discipline, meaningful code review, a defined SDLC with a test gate before deploy — apply to a Claude integration exactly as they apply to any other service dependency. What's specific to this domain is recognizing that prompts and model configuration are part of the system under version control, not an external, unmanaged input. Treat a prompt change with the same review bar as a change to a pricing calculation, because from the user's perspective, it often has the same blast radius.
Software engineering foundations: REST, JSON, concurrency, and change discipline
This sub-objective is worth 7.4% of the exam on its own, and it's easy to underestimate because none of it is Claude-specific — it's the general-purpose engineering literacy the exam expects you to already carry into an integration. The Messages API doesn't invent a new wire protocol or a new way of managing concurrent work; it's a REST-shaped JSON API, and the same foundations that make you competent with any REST API make you competent here. What follows applies those foundations specifically to POST /v1/messages, rather than restating them in the abstract.
POST /v1/messages is REST-shaped in the ways that matter for this exam: the URL names a resource (messages, under the API's version-scoped root), the HTTP method describes the action (POST creates a new message resource — there's no GET /v1/messages/{id} to re-fetch a past response, because nothing is persisted server-side to fetch), and the HTTP status code is the primary error-classification mechanism the API exposes. That last point is the direct link back to this guide's error-handling section: the 4xx-vs-5xx boundary that separates permanent from retryable failures isn't a Claude-specific convention, it's REST's standard use of status codes doing its job.
REST also distinguishes idempotent methods (repeating the same request has the same effect as making it once — GET, PUT, DELETE) from non-idempotent ones (POST, where repeating the request creates a second thing). POST /v1/messages falls on the non-idempotent side of that line by design: every call is a new generation request, and the API has no way to recognize "this is the same request as five seconds ago, don't run it twice." That's exactly why the idempotency-key trap covered earlier in this guide exists — it isn't a gap Anthropic overlooked, it's the ordinary behavior of a POST endpoint that creates a new resource on every call, and the responsibility for de-duplication sits with the caller, same as it would with any other non-idempotent REST endpoint.
- Every request and response body is JSON — comfort with nested structures matters more here than with a typical CRUD API, because the
contentarray can hold several different block types (text,tool_use,tool_result,thinking, and more) at arbitrary depth, and code that assumes a flat shape breaks the first time a response looks slightly different than the example it was tested against. - Parse defensively, not optimistically. A tool result, a piece of retrieved context, or an upstream service's response can arrive malformed or in an unexpected shape. Validate before you index into it — a missing key or an unexpected type should produce a handled error, not an uncaught exception that takes down the request.
- Don't raw-string-match serialized JSON. Tool call inputs can be escaped differently than you'd hand-write them (Unicode escaping, forward-slash escaping); always parse with the language's JSON parser and compare parsed values, never compare substrings of the raw text.
Processing more than one document, ticket, or record against Claude at once is an ordinary concurrent-programming problem, and it has an ordinary concurrent-programming failure mode: firing every request at once with Promise.all() or unbounded thread spawning doesn't parallelize your workload, it queues it behind a 429 from the rate limiter. The fix is the same one you'd reach for calling any rate-limited external service — a semaphore or a fixed-size worker pool that caps how many requests are in flight at once, with the remaining work queued behind it rather than fired all at once and retried into the same wall.
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic();
const MAX_CONCURRENT = 5;
async function classifyDocument(doc: string): Promise<string> {
const response = await client.messages.create({
model: 'claude-sonnet-4-5',
max_tokens: 256,
messages: [{ role: 'user', content: `Classify this document:\n\n${doc}` }],
});
const block = response.content.find((b) => b.type === 'text');
return block?.type === 'text' ? block.text : '';
}
// A worker-pool pattern: a fixed number of workers pull from a shared queue,
// rather than firing Promise.all() across the whole array and hoping the
// rate limiter is generous.
async function classifyAll(docs: string[]): Promise<string[]> {
const results = new Array<string>(docs.length);
let next = 0;
async function worker() {
while (next < docs.length) {
const i = next++;
results[i] = await classifyDocument(docs[i]);
}
}
const workers = Array.from({ length: MAX_CONCURRENT }, () => worker());
await Promise.all(workers);
return results;
}- Error handling completeness — does the diff handle every documented error code the endpoint can return, or only the happy path plus a generic catch-all? A PR that only tests
end_turnand never exercisesmax_tokens,refusal, or a 429 hasn't been reviewed for the failure modes that will actually happen in production. - No hardcoded secrets — an API key, even a short-lived one, has no business appearing in a diff. This is the same check any reviewer runs on any PR touching credentials; it doesn't relax because the credential happens to be for Claude.
- Retry logic doesn't wrap non-idempotent calls blindly — a retry decorator or wrapper applied uniformly to every call site can silently retry a tool call with side effects. A reviewer should ask, for each retried call, whether a duplicate execution is actually safe.
- Prompt changes are tested against the fixed eval set, with the results attached to the PR — the same bar this guide's pre-deploy checklist applies at deploy time, checked earlier, at review time, instead.
Refactoring discipline shows up in this domain in one recurring, concrete shape: the same "call Claude with standard retry, error translation, and logging" pattern gets copy-pasted into a second call site, then a third. The third occurrence is the signal, not the first — extract a shared helper (or the service-layer boundary this guide's application-design section already recommends) once the pattern repeats three or more times, rather than letting each call site drift its own slightly different retry logic. A codebase with five near-identical retry loops is five places a bug fix has to be applied instead of one.
Key takeaways
- 01Start every integration by scoping the requirement: latency tolerance, volume, data sensitivity, and how much autonomy the task actually needs — default to the least autonomous shape (single call over workflow, workflow over agent) that satisfies it.
- 02The Messages API is one endpoint (
POST /v1/messages);max_tokensandmessagesare required, and every response block must be branched on.typebefore you read its payload. - 03Stream or batch anything that might run long — non-streaming requests risk idle-connection timeouts around 10 minutes, and the Batch API trades real-time results for far higher throughput (up to 300K output tokens vs. 128K synchronous).
- 04Only 429 and ≥500 (plus network errors) are retryable; every 4xx below 429 is permanent. The SDKs auto-retry the retryable set with backoff by default.
- 05There is no built-in idempotency-key mechanism on the Messages API — you must design your own dedup key for any tool call with side effects that might be retried.
- 06Prefer Workload Identity Federation over static API keys in production and CI; where a static key is used, give it a short explicit expiration and keep it in a secrets manager, never in source.
- 07Treat prompts and model configuration as versioned, reviewed parts of the system — not incidental strings — with the same change-control rigor as any other production logic.
- 08The systems life cycle for a Claude feature runs through the same requirements → design → build → test → deploy → monitor stages as any service — the differences worth naming are model-behavior requirements, an eval-set-driven build/test phase, and canary rollout plus cost/regression monitoring in production.
- 09
POST /v1/messagesis REST-shaped and non-idempotent by design — status codes drive the retryable-vs-permanent distinction, and any code touching JSON content blocks, concurrent calls, or prompt changes should get the same defensive parsing, bounded concurrency, and code-review rigor as any other production code path.
Common mistakes
Retrying every non-2xx response the same way, including 400s and 404s.
Branch on the error type: retry only 429 and ≥500 (and network errors) with backoff; treat every other 4xx as permanent and fix the request instead of resending it.
Assuming a retried tool call with side effects is automatically safe because the SDK retries transient failures.
The Messages API has no idempotency-key mechanism. Generate your own dedup key and check-and-set against a store you control before any side-effecting tool executes.
Sending a large max_tokens value on a non-streaming request for a task that might run long.
Stream the request, or move the workload to the Batch API if it's high-volume and latency-insensitive — non-streaming requests risk silent failure on dropped idle connections around the 10-minute mark.
Hardcoding the model ID and system prompt directly in feature code, scattered across call sites.
Isolate the API call behind a service/config layer so model upgrades and prompt changes are reviewed, versioned config changes — not code edits repeated at every call site.
Frequently asked
Is Claude Domain V2 mostly about API syntax, or about system design?
Both, but the weighting (33.1% of the exam, split across six sub-objectives) signals that request/response mechanics alone won't carry you. Expect questions that combine an API fact (what does stop_reason: max_tokens mean) with a design judgment (what should your code do about it, and where does that logic live).
Do I need to memorize every HTTP status code and error type?
Know the boundary, not the trivia: 429 and ≥500 (plus network errors) are retryable, everything else 4xx is permanent. That distinction — and the fact that the SDKs already implement it for you by default — is the testable concept. The exact wording of each error type string is less important than knowing which side of the retry line it falls on.
When would I actually reach for the Batch API instead of just streaming everything?
When the workload is high-volume and nobody is waiting on an individual result in real time — a nightly reclassification job, a bulk backfill, scoring a large dataset. Batch supports far higher output-token ceilings than a synchronous call (up to 300K vs. 128K) precisely because it isn't optimized for turnaround time.
Why does idempotency come up in an API-mechanics domain instead of a general software-engineering one?
Because it's a place where the API's behavior (auto-retry on transient failure) and your application's responsibility (dedup before a side effect) intersect, and the API deliberately doesn't solve it for you the way some other platforms do. It's exactly the kind of API-specific gap the exam is likely to probe.
Independent, unofficial study material. Not affiliated with, endorsed by, or authorized by Anthropic. Every example is original and written to teach the public exam objectives — no real exam questions are reproduced. Technical details reflect Claude, the Anthropic API, Claude Code, and MCP as of August 24, 2026; always confirm specifics against current official documentation.
Test yourself
Turn what you just read into answers you can check.