CClaude Cert Prep
V233.1% of exam

CCDV-F Domain 2: Applications and Integration

The single heaviest domain on the exam: how to scope, build, and ship a production Claude integration — request mechanics, streaming vs. batch, the retryable-vs-permanent error trap, and the engineering discipline around it.

18 min read Reviewed August 24, 2026
On this page

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.
ShapeWho controls the loopFits when
Single callYour code, one request/responseBounded task with a clear input and output — classification, extraction, a single generation
WorkflowYour code, multiple calls in a fixed sequenceMulti-step but predictable — summarize, then classify, then format
Agent (tool use)The model, within a loop your code still terminatesThe 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.

StageStandard practiceWhat's different with Claude in the loop
RequirementsFunctional requirements: inputs, outputs, who consumes the resultAlso 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
DesignComponent boundaries, data flow, error-handling strategyDecide 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
BuildImplement against the design, write code alongside testsImplement against a small, fixed eval set from day one — not by eyeballing outputs and calling it done once a few examples look right
TestUnit and integration tests with deterministic assertionsAdd 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 / deployStandard rollout — feature flags, canaries, blue/greenCanary 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 / iterateError-rate and latency dashboards, on-call alertingTrack 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.

FieldRequired?Notes
modelYesModel identifier string
max_tokensYesHard cap on output length — the request fails without it
messagesYesArray alternating user/assistant roles
systemNoSystem prompt, kept separate from the conversation turns
tools / tool_choiceNoDeclares callable tools and how the model should use them
thinkingNoEnables extended/adaptive reasoning before the final answer
cache_controlNoMarks a prefix as cacheable to cut latency and cost on repeated context
streamNotrue 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.

python
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 counts
Minimal Messages API request and response handling
typescript
import 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 counts
The same request in TypeScript, using the official @anthropic-ai/sdk package
stop_reasonMeaning
end_turnThe model finished naturally
max_tokensHit the output cap — the response may be cut off mid-thought
stop_sequenceA configured stop string was reached
tool_useThe model wants to call a tool; execute it and continue the loop
pause_turnA long-running agentic turn paused and can be resumed
refusalThe 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.

ModeBest forKey constraint
Synchronous (non-streaming)Short, bounded requests where the whole answer is needed at onceNon-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 usefulYou own the client-side accumulation of the response as it arrives
Batch APIBulk, latency-insensitive processing — classification sweeps, offline scoring, backfillsSupports 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.

python
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)
Streaming a request and accumulating the final message

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.

CodeError typeRetry?
400invalid_request_errorNo — the request itself is malformed
401authentication_errorNo — fix credentials first
402billing_errorNo
403permission_errorNo
404not_found_errorNo — usually a bad model ID or endpoint
409conflict_errorNo
413request_too_largeNo — reduce payload size
429rate_limit_errorYes — back off and retry
500api_errorYes
504timeout_errorYes
529overloaded_errorYes
python
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_exception
Custom retry with exponential backoff (only needed beyond the SDK's built-in default)

The 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.

MethodShapeBest for
Static API keyssk-ant-api..., set via ANTHROPIC_API_KEYDevelopment, simple deployments — choose an explicit expiration (3h/1d/7d/30d/custom/Never) at creation time
Workload Identity FederationExchanges a cloud IdP JWT for short-lived tokensProduction and CI — no static secret to leak or rotate
App AttestHourly-expiring, Messages-API-scoped onlyNative 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.

LayerOwnsWhy it's separated
Feature codeBusiness logic: when to call, what to do with the resultDoesn't need to know about retries, headers, or SDK internals
Integration/service layerThe messages.create call itself, error translation, retry policyOne place to change model, add caching, or swap the SDK version
Config layerModel ID, max_tokens, timeout, which environment's keyChanges 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.

  1. Wrap the SDK call in your service layer, not in feature code — one place to catch, translate, and log errors consistently.
  2. 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.
  3. 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.
  4. 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_tokens or 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.
ChangeReview needed?Rollback path
Model ID upgradeYes — re-run eval set, review cost/latency deltaConfig revert to prior model ID
System prompt editYes — same rigor as a logic changeGit revert; redeploy config
anthropic-version bumpYes — check for response-shape changes in changelogPin back to prior dated version
Retry/backoff tuningLightweight — verify against rate-limit headersConfig 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.

Before shipping a Claude integration change0 of 8 done

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 content array 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.

typescript
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;
}
Bounded concurrency: a fixed worker pool processes documents with at most 5 in-flight Claude API calls
  • 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_turn and never exercises max_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_tokens and messages are required, and every response block must be branched on .type before 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.
  • 09POST /v1/messages is 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.

Take the mock exam

Keep studying

All guides

These guides are free and never paywalled. Keep Claude Cert Prep free ♥