CCDV-F · module 1 of 8 · 33.1% of the exam
Applications and integration
Weight: 33.1 percent of the exam. Tests whether you can wire the Claude API into a real product correctly: request shape, streaming, multimodal input, caching, batching, error handling, rate limits, and the tool use loop.
What the exam expects
This is the largest domain on CCDV-F by a wide margin, roughly one question in three. It is also the most mechanical: most questions have a single defensible answer grounded in how the API actually behaves, not in architectural taste. The exam tests whether you have shipped against this API or only read about it.
Expect scenarios written from the perspective of a team already in production: a support bot that forgets context, a caching change that produced no savings, a rate limit incident during a launch, a batch job whose results are attached to the wrong records. You are usually asked one of four things: what is the root cause, what should they do first, what is wrong with this approach, or which change most improves it.
Two habits of mind carry most of the domain. First, the API is stateless and the request body is the entire world the model sees. Second, most silent failures in this domain are not errors at all: they are successful HTTP 200 responses whose stop_reason, usage, or content shape you did not inspect.
The Messages API contract
Every call goes to POST /v1/messages. There is no server side conversation. Continuity exists only because your client stores the transcript and resends it in full on every request.
The request has three prompt-bearing parts, and they render in a fixed order: tools, then system, then messages. That order matters far more than it looks, because it determines what can be cached (see below).
Rules worth memorising:
systemis a top level parameter, not a role insidemessages. (Mid-conversationrole: "system"messages insidemessagesare a separate, model-gated feature for operator instructions that arrive later.)- The first message must be
user. Consecutive same-role messages are combined into one turn rather than rejected. max_tokensis required, and it is a hard ceiling on everything the model generates, including thinking tokens. A route sized tightly around a short visible answer can start truncating the moment reasoning is enabled on it.- Assistant prefill on the final turn returns a 400 on current models. Use structured outputs or a system prompt instruction instead.
The response is a list of content blocks (text, thinking, tool_use, and others), not a string. Flattening it to text is the single most common data modelling bug in this domain, because it silently discards the tool_use blocks the next request needs.
Always branch on stop_reason before touching content:
stop_reason | Meaning | What your code must do |
|---|---|---|
end_turn | Finished naturally | Render normally |
max_tokens | Truncated at the output ceiling | Do not present as complete; raise the budget or shorten the task |
tool_use | Wants a tool executed | Run it and continue the loop |
refusal | Declined for safety reasons | content may be empty; surface a refusal path |
pause_turn | Server tool loop paused | Resend to resume |
Log the response's request id on every call. It is the identifier Anthropic support traces on, and a client-generated UUID is no substitute.
Delivery: streaming and its limits
Streaming does not make generation faster. It changes when the user sees the first token, and it keeps the HTTP connection active so a long generation does not die on an idle timeout. Both reasons are legitimate; nothing about cost or rate limits changes.
The SDKs guard non-streaming requests with a large max_tokens for exactly this reason. If you hit that guard, the answer is to stream and read the accumulated result with the final message helper, not to suppress the guard or to split the work into artificial chunks.
When handling raw events, message_delta is where stop_reason and final usage arrive. content_block_delta carries incremental content only, and message_start fires before anything has been generated.
Multimodal input: images, PDFs, files, citations
Images go in image content blocks with one of three sources: base64, a URL, or a file_id from the Files API. PDFs go in document blocks with the same three options. The block type must match what the file actually is: an uploaded PNG referenced from a document block is a 400.
Cost and limits are dimension-driven. Claude bills images as visual tokens computed from pixel dimensions, capped by the model's resolution tier, so a 4K screenshot genuinely costs several times a 1366x768 one. Pre-resizing before upload is the direct control when you do not need the fidelity. Requests have both a payload size limit and an image or page count limit, and heavy base64 usually reaches the size limit first.
For PDFs, each page contributes extracted text and an image of the page, which is what lets Claude read charts and layout. A cost forecast built from extracted text alone will undershoot. PDFs must be standard and unencrypted; removing a password is the caller's job.
Choosing a source:
| Situation | Use |
|---|---|
| One shot call, small file | base64 inline |
| Multi-turn conversation with an attachment | Files API file_id (bytes are not resent every turn) |
| Same document queried many times | Files API plus a cache breakpoint after the document |
Files persist until you delete them, so lifecycle management is your application's responsibility.
Citations are enabled per document block and make Claude return cited text with a structured location: character offsets for plain text, page numbers for PDFs. Two constraints matter: citations are all-or-none across document blocks, and they are incompatible with an output format schema in the same request.
Cost and throughput: caching, batching, counting, limits
Prompt caching is a prefix match. Any byte change anywhere in the prefix invalidates everything after it. Design for this by ordering content from stable to volatile: frozen system prompt and deterministically serialised tools first, per-session content next, per-request content (timestamps, IDs, the actual question) last, after the final breakpoint.
The diagnostic is usage. Writes on every request with zero reads means something in the prefix changes each time. Both counters at zero with no error usually means the prefix is below the model's minimum cacheable length. Remember also that input_tokens is only the uncached remainder: total input is the sum of the three counters, which is why a heavily cached agent shows a surprisingly small number.
Two second-order effects are worth knowing. A cache entry is only readable once the first response begins streaming, so a parallel fan-out of identical requests all miss. And on current models, cache reads do not count toward your input tokens per minute limit, which makes caching a throughput lever, not just a cost one.
The Batch API processes requests asynchronously at half the standard token price, with its own rate limit pool so bulk work does not starve interactive traffic. It supports the full Messages API feature set, including caching, so a shared prefix across thousands of batch requests compounds both savings. Results come back in arbitrary order: key them by custom_id, never by position. Branch on the result type (succeeded, errored, canceled, expired) before reading a message.
Token counting uses the count_tokens endpoint with the model id you will actually use. Counts are model-specific, and tokenizers built for other providers materially undercount Claude tokens, especially on code and non-English text.
Rate limits are per organisation and per model, measured in requests, input tokens, and output tokens per minute, replenished continuously. Extra API keys add no capacity. Workspace limits can cap a workspace below the organisation total but never above it. max_tokens does not consume output budget; only generated tokens do. Drive alerts from the rate limit response headers rather than from counting 429s after users are already affected.
Failure handling
Distinguish retryable from permanent before writing any loop:
| Retryable | Not retryable |
|---|---|
429 rate limited (honour retry-after) | 400 invalid request |
| 500 and other 5xx | 401 authentication |
| 529 overloaded | 403 permission |
| Connection errors | 404 unknown model or endpoint |
A bare except around every call turns a typo in a model id into five slow attempts and thirty wasted seconds. Use the SDK's typed exception classes and catch most-specific first.
Also account for what the SDK already does: it retries connection errors, 429, and 5xx with backoff by default. A custom wrapper on top multiplies the attempt count and the worst-case wall clock. Configure max_retries and add only what the SDK does not provide.
Refusals are not errors. They arrive as HTTP 200 with stop_reason: "refusal" and possibly empty content, which is why the stop_reason check has to come before the content read.
Tools and structured outputs
The tool loop has a fixed shape. Claude returns an assistant turn containing one or more tool_use blocks. You append that entire assistant turn to messages, then append a single user message containing a tool_result block for every call, each with the matching tool_use_id. Splitting results across multiple user messages trains the model out of parallel calling. A failed tool still needs a tool_result, marked with is_error: true and an informative message, so Claude can adapt instead of waiting for a result that never comes.
Human-in-the-loop approval does not require hand-writing the loop. The SDK tool runner lets you gate inside the tool function or intervene in the per-turn hook before execution, so choose a manual loop only when your control flow genuinely does not fit.
Structured outputs come in two forms: output_config.format with a JSON schema to constrain the response, and strict: true on an individual tool definition (with additionalProperties: false and an explicit required list) to guarantee tool input shape. A schema constrains structure, not truth, and it does not exempt you from max_tokens truncation.
One thing Anthropic does not provide: an embeddings model. Retrieval pipelines use a third-party embedding provider (the documentation points to Voyage AI) and your own vector store, with Claude handling generation over the retrieved context.
How to think through the question
Domain 1 questions are diagnostic more often than they are architectural. Work them in this order.
1. Identify the actual failure signal. Is it an HTTP error, a successful response with unexpected content, or a cost or latency observation? This single split eliminates most distractors. An HTTP 200 with empty content is a refusal, not a network problem. A cost spike with no error is almost always caching or resolution.
2. Name the constraint the scenario states. Interactive latency, a fixed budget, a compliance requirement for traceability, a fleet already near its rate limit. The correct answer serves that constraint. An option that ignores it is wrong even when it is technically sound.
3. Ask what the question is really asking. "What is the root cause" wants the mechanism. "What should they do first" wants the cheapest diagnostic or the highest-leverage fix, not the complete remediation plan. "What is wrong with this approach" wants the flaw, not a redesign.
4. Eliminate compensating controls. Options that add monitoring, retries, validation, or regexes around a broken mechanism are the most common wrong answers in this domain, because they are things real teams actually do. If a cleaner mechanism exists (structured outputs instead of brace-matching regex, is_error instead of skipping a tool result, the Files API instead of trimming history), that is the answer.
5. Eliminate invented API surface. Distractors frequently name a parameter, header, or endpoint that does not exist: a conversation_id, an embeddings endpoint, a password field on a document block, a request-level strict flag. If you cannot picture the field in a real request body, it is not real.
Worked example. A due diligence workflow asks 30 questions about the same 200 page PDF, one per request, within a few minutes. Costs are far higher than expected and every request has similar latency regardless of how simple the question is.
Step 1: the signal is cost plus flat latency, with no errors. So this is about what is being reprocessed, not about correctness. Step 2: the stated constraints are a repeated large prefix and a short time window, which is exactly the shape caching is for. Step 3: the question asks which combination most reduces cost and latency, so we want the mechanism, not a workaround. Step 4: eliminate "send the 30 requests in parallel" (a compensating control that actually defeats caching, since the entry is not readable until the first response starts streaming) and "split the PDF into 30 keyword-selected chunks" (reintroducing a retrieval failure mode where the whole document already fits). Step 5: "concatenate all 30 questions into one request" uses only real API surface and does reduce processing to one pass, but it couples 30 independent answers into one generation and risks the output ceiling. The answer is the Files API for file_id plus a cache breakpoint after the document: the prefix is written once and read cheaply 29 times, which addresses both the cost and the flat latency.
Exam traps
- Treating the API as stateful. Any option involving a session id, a stored transcript, or sending only the delta is wrong. Tempting because most chat APIs developers have used do maintain sessions.
- Reading
content[0]before checkingstop_reason. Tempting because it works in the happy path and in every quickstart snippet. - Assuming
input_tokensis the whole prompt. It is the uncached remainder. Tempting because the field name says otherwise. - Adding API keys to raise rate limits. Limits are per organisation. Tempting because per-key quotas are common elsewhere.
- Lowering
max_tokensto relieve an input token limit. Input and output are metered separately. Tempting because it feels like reducing overall load. - Fixing stale RAG answers with a stronger prompt. If the index was not rebuilt, the new text never reached the model. Tempting because prompting is the cheapest thing to change.
- Joining batch results by position. Results are unordered; use
custom_id. Tempting because the corruption is silent and only shows up in a fraction of rows. - Wrapping every call in a bare retry loop. It converts fast, deterministic errors into slow ones and duplicates SDK behaviour. Tempting because "retry on failure" reads as defensive engineering.
- Regex-extracting JSON instead of using structured outputs. Tempting because the regex mostly works and the retry hides the rest.
- Assuming a manual tool loop is required for approval gates. The tool runner supports gating. Tempting because "I need control" sounds like it implies owning the loop.
Quick reference
| Symptom | First thing to check |
|---|---|
| Model forgets earlier turns | Is the full history in messages? |
| Answers cut off mid sentence | stop_reason is max_tokens; thinking shares the budget |
Empty content, HTTP 200 | stop_reason is refusal |
| Caching saves nothing | cache_read_input_tokens is 0; diff the prefix byte for byte |
| Both cache counters 0, no error | Prefix below the model's minimum cacheable length |
| Payload grows every turn | Base64 attachment being resent; move to the Files API |
| Image bill 3x a peer's | Resolution; downsample before upload |
| Wrong answers attached to wrong rows in a batch | Positional join instead of custom_id |
| Model stopped calling tools in parallel | Tool results split across multiple user messages |
| 429s during a launch ramp | Acceleration limits; ramp gradually and honour retry-after |
Ready to test this domain?
Drill mode gives instant feedback: pick a wrong answer and you immediately see why it is wrong.
Start the drill