Every lesson in this course has asked you to make one judgment call at a time — pick a status code, name a field, choose a pagination style. The capstone asks something harder: hold all of those judgment calls in your head simultaneously, on one coherent domain, and produce a written design that a real engineering team could actually build from. There's no code to run at the end of this lesson and no automated grader — the deliverable is a design document, and the discipline this lesson is really testing is whether you can defend every decision in it with a reason, not just a pattern you remembered seeing somewhere in this course.
The domain for this capstone is a multi-venue event ticketing platform — think venues, events, ticket types, orders, and the partners and internal teams who all need to interact with it differently. This domain was chosen because it forces every tradeoff this course has covered to show up naturally: a public consumer-facing surface for people buying tickets, a partner surface for box-office integrations that need contractual stability, an internal surface for the operations team, a genuinely long-running operation (processing a large group order), a resource that needs idempotent creation (an order, where money changes hands), and a plausible AI-agent use case (a booking concierge that searches events and completes purchases on a user's behalf).
This lesson gives you three staged deliverables, each with explicit acceptance criteria, rather than one open-ended prompt to 'design a good API.' That's deliberate, and it mirrors the very first lesson of this course: 'good' is a bundle of tradeoffs, not one property, and a design brief that doesn't say which tradeoffs matter here produces exactly the kind of inconsistent, undirected design this course has spent its entirety warning against. Work through the three stages in order — each one builds on decisions made in the previous stage, the same way a real design review would catch you if Stage 2's error handling contradicted Stage 1's resource model.
The Brief: What You're Building
Stage 1: Resource Model and Paradigm Choice
Produce a resource map for the platform: identify at least six core resources — plausible choices include `Venue`, `Event`, `TicketType`, `Order`, `Payment`, and `Attendee` — and state each resource's ownership relationship explicitly, either as a root-level resource with a stated reason it doesn't belong under anything else, or as a subresource nested under its logical parent, the way this course's lesson on resources and URL structure taught. From this map, write out the URL structure for at least ten endpoints that together cover standard CRUD operations plus at least two actions that don't map cleanly onto a CRUD verb — refunding an order or checking a venue's live seat availability are two plausible candidates — and state explicitly why each of those two used a sub-resource or action-oriented path instead of forcing it into a `PATCH`.
Choose a primary API paradigm — REST, GraphQL, gRPC, or an explicit hybrid — and justify the choice in two to three sentences grounded in this platform's actual consumers, not in the abstract. A justification that says 'REST is simpler' without naming which consumer's actual access pattern that simplicity serves is not sufficient; a justification that says 'the partner box-office integration needs simple, cacheable, resource-oriented reads it can retry safely, and GraphQL's flexible querying isn't worth the added complexity for a consumer that mostly needs the same five fields on every call' is the level of specificity this stage requires.
Acceptance criteria for Stage 1: every resource in your map has a stated owning parent, or a stated reason it's root-level; naming is consistent across every endpoint (plural nouns, one consistent casing convention); the ten-endpoint list includes at least two non-CRUD actions, each with a one-sentence justification for its path shape; and your paradigm choice is justified against a named consumer's actual need, not asserted as a general preference.
Stage 2: Contract, Errors, and Versioning
Fully specify the request and response shape for three endpoints from your Stage 1 map: the list-events endpoint (must demonstrate a pagination strategy, with a stated reason for the choice), the create-order endpoint (must demonstrate an idempotency-key strategy, since this is the one operation in the platform where a duplicate write means a customer is charged twice), and one more endpoint of your choosing. Write these as simplified JSON schema fragments — field names, types, and a one-line note on any non-obvious constraint — rather than full OpenAPI YAML; the goal is demonstrating the judgment, not the tooling syntax.
Define a single error envelope shape used consistently across all three endpoints, and write an error taxonomy table listing at least six distinct error codes, each with a one-line description and an explicit retryable flag — exactly the structured, agent-actionable shape this course's tool-use lesson argued every API should have regardless of whether an agent is actually calling it. Then choose and justify a versioning strategy for the platform as a whole, and state explicitly what deprecation notice period you'd commit to for a breaking change at each of the internal, partner, and public tiers, applying the audience-tiering framework from earlier in this course.
Acceptance criteria for Stage 2: the error envelope's field names and shape are identical across all three endpoints, with no ad hoc per-endpoint variation; the idempotency-key strategy explicitly states what happens on a retry with the same key and an identical body, and what happens on a retry with the same key but a different body, since leaving that second case unspecified is the single most common gap in a written idempotency design; and your versioning strategy names a specific, different notice period for each of the three audience tiers, not one blanket number applied to all three.
# stage2_error_envelope.py -- a runnable sketch of the consistent envelope Stage 2 requires
ERROR_TAXONOMY = {
"VALIDATION_FAILED": {"retryable": False, "desc": "one or more fields failed validation"},
"RESOURCE_NOT_FOUND": {"retryable": False, "desc": "the referenced resource does not exist"},
"IDEMPOTENCY_CONFLICT": {"retryable": False, "desc": "same idempotency key reused with a different request body"},
"RATE_LIMITED": {"retryable": True, "desc": "too many requests -- retry after backoff"},
"SEAT_UNAVAILABLE": {"retryable": False, "desc": "requested seats were reserved by another order first"},
"UPSTREAM_TIMEOUT": {"retryable": True, "desc": "a dependency timed out -- safe to retry with the same idempotency key"},
}
def build_error(code: str, field: str = None) -> dict:
"""One envelope shape, reused by every endpoint in the platform --
the Stage 2 requirement this function exists to demonstrate."""
meta = ERROR_TAXONOMY[code]
return {"code": code, "field": field, "detail": meta["desc"], "retryable": meta["retryable"]}
for code in ("VALIDATION_FAILED", "IDEMPOTENCY_CONFLICT", "RATE_LIMITED"):
print(build_error(code, field="ticket_type_id" if code == "VALIDATION_FAILED" else None))
retryable_count = sum(1 for m in ERROR_TAXONOMY.values() if m["retryable"])
print(f"\n{retryable_count} of {len(ERROR_TAXONOMY)} error codes are retryable -- "
f"an agent or client can act on this flag without parsing the detail text.")
Stage 3: Documentation and Client Experience
Write a short getting-started documentation excerpt, aimed at a new partner developer, for one endpoint of your choosing — it must state the authentication method, any applicable rate limit, and a complete example request and response, in the self-describing style this course's lesson on documentation argued readers actually use. Then design a tool schema, in the JSON-Schema-shaped format this course's agent-design lesson covered, exposing one capability of this platform to an AI booking concierge — `search_events` or `book_ticket` are plausible choices — with every parameter typed precisely, every non-obvious constraint stated in the description, and a structured, retryable-aware error shape reusing the taxonomy from Stage 2.
Finally, classify every one of the ten endpoints from your Stage 1 map as internal, partner, or public, applying the audience-tiering framework from earlier in this course, and identify at least one endpoint that would need different response shaping depending on which tier is calling it — a good candidate is an order-detail endpoint, where an internal caller might see a fraud-risk score, a partner sees carrier and fulfillment status, and a public caller sees only their own order's status and total. State explicitly which fields that endpoint would show or hide per tier.
Acceptance criteria for Stage 3: the documentation excerpt includes a complete, concrete example request and response, not a schema alone; the agent tool schema uses a closed enum for any parameter with a genuinely closed value space, and states at least one cross-field constraint explicitly in its description; the audience classification names a tier for every one of the ten Stage 1 endpoints, with no endpoint left unclassified; and the identified multi-tier endpoint lists specific fields shown or hidden per tier, not a vague statement that the response 'differs.'
# stage3_agent_tool_schema.py -- a runnable sketch of the tool schema Stage 3 requires
BOOK_TICKET_TOOL_SCHEMA = {
"name": "book_ticket",
"description": (
"Books tickets for a single event on behalf of the user. quantity must not exceed "
"the event's remaining_capacity at call time, or the call fails with SEAT_UNAVAILABLE. "
"This action charges the user's saved payment method immediately."
),
"parameters": {
"event_id": {"type": "string", "description": "the event's unique identifier"},
"ticket_type": {"type": "string", "enum": ["general", "premium", "vip"]},
"quantity": {"type": "integer", "minimum": 1, "maximum": 8},
"idempotency_key": {"type": "string", "description": "required; reused on retry to avoid a duplicate booking"},
},
}
def validate_call(event_remaining_capacity: int, quantity: int, ticket_type: str) -> dict:
"""What a well-built calling layer checks BEFORE this ever reaches the platform --
the closed enum and the stated cross-field constraint make this possible."""
allowed_types = BOOK_TICKET_TOOL_SCHEMA["parameters"]["ticket_type"]["enum"]
if ticket_type not in allowed_types:
return {"code": "VALIDATION_FAILED", "field": "ticket_type", "retryable": False}
if quantity > event_remaining_capacity:
return {"code": "SEAT_UNAVAILABLE", "field": "quantity", "retryable": False}
return {"code": None, "result": "call is valid, proceed"}
print(validate_call(event_remaining_capacity=3, quantity=2, ticket_type="vip"))
print(validate_call(event_remaining_capacity=3, quantity=5, ticket_type="vip"))
print(validate_call(event_remaining_capacity=3, quantity=2, ticket_type="platinum"))
How Your Submission Will Be Evaluated
Before treating any stage as finished, run your own design against the acceptance criteria listed for that stage, line by line, the same way this course's lesson on contract testing argued a design should be checked against a written contract rather than a gut feeling of 'looks done.' A design is not complete because it feels thorough — it's complete because every stated criterion has a specific, checkable answer in the document, and 'the error handling seems reasonable' is not the same claim as 'the error envelope is identical across all three specified endpoints,' which is the actual, checkable criterion Stage 2 asks for.
Measurable, specific evaluation criteria matter here for the same reason they matter in the API you're designing: a vague acceptance bar produces inconsistent results depending on who's judging it, exactly like a vague API contract produces inconsistent behavior depending on which engineer happened to implement a given endpoint. Every acceptance criterion in this lesson was written to be checkable by reading your document and answering yes or no, not by forming a subjective impression — and that's the standard your own design document should be held to internally as well, since the whole point of this capstone is practicing the judgment a real design review would apply to your work.
# self_review_checklist.py -- turning each stage's acceptance criteria into a checkable pass/fail
def review_stage1(resource_map: dict, endpoints: list, paradigm_justification: str) -> list[str]:
issues = []
for resource, owner in resource_map.items():
# "ROOT: <reason>" is an explicitly stated root-level decision -- only a bare
# None (never actually decided) is a real gap in the map.
if owner is None:
issues.append(f"resource '{resource}' has no stated owner and no stated reason to be root-level")
non_crud = [e for e in endpoints if e.get("is_action")]
if len(non_crud) < 2:
issues.append(f"only {len(non_crud)} non-CRUD action endpoints -- need at least 2")
if len(paradigm_justification.split()) < 15:
issues.append("paradigm justification too thin -- must name a specific consumer's actual need")
return issues
sample_map = {
"Venue": "ROOT: independent entity, not owned by any single event",
"Event": "Venue",
"Order": "ROOT: spans multiple events via its line items",
"Payment": "Order",
}
sample_endpoints = [
{"path": "/orders/{id}/refund", "is_action": True},
{"path": "/events", "is_action": False},
]
sample_justification = "REST fits the partner integration's simple, cacheable, resource-oriented needs."
for issue in review_stage1(sample_map, sample_endpoints, sample_justification):
print("FLAG:", issue)
print("review complete -- fix every FLAG before moving to Stage 2")
Common Pitfalls to Avoid
The most common Stage 1 failure is modeling resources around a plausible internal database schema instead of around what a consumer actually needs to accomplish — designing a `TicketInventoryRow` resource because that's how the data would naturally live in a table, rather than a `TicketType` resource shaped around what a buyer or a partner integration actually needs to request. This is the exact failure this course's second lesson warned about: an API modeled around internal structure instead of consumer intent leaks implementation detail into a contract that then can't evolve independently of that internal structure.
The most common Stage 2 failure is leaving the idempotency-key conflict case unspecified — stating that retries with the same key are handled, without stating what happens if the retried request's body doesn't match the original. Left unspecified, a real implementation either silently returns the original result regardless of what changed, masking a genuine bug in the caller, or silently processes the new body as if it were the original request, defeating the entire point of the key. The most common Stage 3 failure is writing an agent tool schema exactly like a human-facing docstring — free-text parameters, constraints mentioned in prose a model has to parse loosely rather than expressed as an enum or a validation rule the calling layer can enforce structurally before your platform ever sees a bad call.
A specific trap worth calling out directly: it's tempting to treat Stage 3's agent tool schema as an afterthought layered on top of a REST API you've already fully designed, reusing the REST endpoint's existing parameter names and error shapes without re-checking whether they actually meet an agent's stricter needs. The symptom is a tool schema with a free-text `status` parameter because that's what the REST endpoint used, even though the actual value space is five closed options. The root cause is designing for a human reader first and assuming 'good enough for a person' transfers automatically to 'good enough for an agent.' The fix, as this course's lesson on agent design argued, is re-checking every parameter against the agent-specific bar directly — closed enums, explicit cross-field constraints, structured retryable errors — rather than assuming the REST design already cleared it.
Submission Checklist and What Good Looks Like
A finished submission is a single design document containing, in order: your Stage 1 resource map and ten-endpoint list with paradigm justification; your Stage 2 three-endpoint schema fragments, error taxonomy table, idempotency-conflict specification, and per-tier versioning commitment; and your Stage 3 documentation excerpt, agent tool schema, and full ten-endpoint audience classification with the one multi-tier endpoint's field-level breakdown. Nothing here requires a working server or a deployed API — every deliverable is checkable by reading the document itself against the acceptance criteria already stated for each stage.
What separates a genuinely strong submission from an adequate one isn't extra length or extra endpoints beyond what was asked for — it's whether every decision in the document is defensible on the specific grounds this course has spent thirty-four lessons building: named consumer needs driving the resource model, a consistent contract instead of ad hoc per-endpoint choices, and an explicit accounting of who each part of the API actually serves. A submission that hits every acceptance criterion technically but can't explain why any particular choice was made over the alternative has completed the exercise without absorbing the judgment the exercise exists to build — and that judgment, not the specific ticketing-platform design itself, is the actual deliverable this capstone is checking for.
- A capstone with staged deliverables and explicit, checkable acceptance criteria produces a more defensible design than one open-ended prompt, for the same reason a clear API contract produces more consistent behavior than a vague one.
- Resources must be modeled around what a consumer actually needs to accomplish, not around a plausible internal database schema, or the resulting contract leaks implementation detail it can never cleanly remove later.
- An idempotency-key design is incomplete until it explicitly specifies the conflict case -- a retried key with a changed request body -- since that ambiguous case, left unspecified, is where a real implementation is most likely to fail silently.
- A single, consistent error envelope reused across every endpoint, carrying a stable code and an explicit retryable flag, is what makes an API's error handling genuinely usable by both human developers and AI agents.
- Every endpoint in a platform needs an explicit audience classification -- internal, partner, or public -- because an unclassified endpoint tends to default to whichever tier's assumptions the original implementer happened to have in mind.
- An agent-facing tool schema needs its own explicit pass for closed enums, stated cross-field constraints, and structured errors -- reusing a human-facing REST design's shape unexamined is the most common gap between a working API and a genuinely agent-ready one.
- The actual deliverable of this capstone is transferable judgment about tradeoffs, not the specific ticketing-platform design -- a submission that satisfies every criterion without being able to explain why has completed the exercise without absorbing what it was built to teach.