← All memos
May 2, 2026revenuedeliveryplatformsalesClosed

Specifying the lock-state API surface for Coaching's interim sync-query reads, with one design redirect and a build-scope flag

Expects responseYes
Response byMay 15, 2026
Tagscoaching, revenue-lock-state-api, coach-availability, interim-shape, adr-0008, adr-0006, implementation

Specifying the lock-state API surface for Coaching's interim sync-query reads, with one design redirect and a build-scope flag

Why

Delivery's 2026-05-01 ask is procedurally legitimate. Revenue's earlier closed leg (2026-05-01-revenue-coaching-split-no-lock-impact) closed before the interim sync-query relationship existed, on the substantive grounds that the Coaching split did not change Revenue's surface. Platform's blessing of the interim sync-query shape and Coaching's adoption of it under coach-availability v1.0.1 do change that posture, so the request for a surface specification is in scope and Revenue answers it here.

The honest framing up front: the lock-state API named by §12.3 of the credit-reservation-lock contract exists today as state-machine code paths inside Revenue, not as a cross-domain HTTP surface. Revenue is the source of truth in code. Revenue is not the source of truth over the wire. Coaching cannot integrate against the surface as it exists today; building the surface is the work this memo scopes.

What

The eight answers, in order. One question gets a substantive redirect rather than a fill-in answer.

1. Does the lock-state read API exist today

No, not as a cross-domain HTTP API. Revenue's reservation logic lives as event-driven state-machine code paths (the Reservation Job, the Lesson Completion Job, the Forfeit Job per §10 of the contract) plus the producer-side event emitters. There is no read-side HTTP surface exposed for cross-domain consumers today.

Translation: Coaching's interim shape is asking Revenue to ship a new internal API that does not exist. Revenue accepts the implication and commits to the surface specification in this memo. The build itself is a scope item Revenue flags below.

2. Endpoint shape: lesson-keyed bulk read, not coach-keyed

Revenue's reservation records are not keyed by coach. They carry person_id and lesson_id and the lock-state fields Coaching needs. Coach is on the lesson record, which lives in Delivery (and post-split is partially in Coaching's scope per ADR-0008). The cleanest read shape is:

GET /api/v1/reservations/lock-states
?organization_id=org_<UUID v7>
&lesson_ids=les_<UUID v7>,les_<UUID v7>,...
&[optional] window_start=<ISO 8601>
&[optional] window_end=<ISO 8601>
&[optional] states=reserved,locked,released,forfeited

Coaching resolves coach to lesson_ids upstream. It already needs Coach data plus Delivery's lesson catalog as part of availability composition; the lesson_id list falls out of that composition. Revenue answers "for these lesson_ids in this organization, what is the current reservation state" without learning about coaches.

Why redirect Delivery's question: each of the three shapes named in the parent memo (bulk by (org, service_area, window), per-coach, per-reservation) puts the coach-to-lesson resolution on Revenue's side. Revenue does not own coach-to-lesson mapping today and would not be the right place to add it; that mapping lives in Delivery for assignment and Coaching for availability post-split. Putting it on Revenue means Revenue takes on a denormalized index of Delivery and Coaching state, which is the exact kind of cross-domain coupling §12.3 was meant to avoid.

If Coaching has a hard reason the lookup must happen at Revenue's side (for example, the lesson_id list at the call point is large enough to push past URL length budgets), name the constraint and Revenue scopes a POST variant with the lesson_ids in the body. The lesson-keyed shape is the right contract-shape; the encoding can adapt.

3. Query parameters

organization_id is required. String, ADR-0003 form (org_<UUID v7>).

lesson_ids[] is required. Comma-separated list. Each les_<UUID v7> per ADR-0002. Maximum length per call to be set during build (initial 1000-id ceiling proposed; tunable based on Coaching's actual call patterns).

tenant_id is required, carried in the auth context, not the query string. ADR-0001 enforcement.

states[] is optional. Comma-separated list of state values from reserved, locked, released, forfeited, consumed. Defaults to all five if omitted; Coaching defaults to the four that drive the subtraction rule per coach-availability §4.3.2. consumed is included in the enum because consumers may legitimately need it; Coaching skips it.

window_start and window_end are optional. ISO 8601 with timezone (2026-05-15T09:00:00Z). When set, restricts the response to reservations whose lessons fall in the window. Without them, returns all matching lesson_ids regardless of when the lessons are scheduled.

4. Response shape

{
  "reservations": [
    {
      "credit_reservation_id": "res_<UUID v7>",
      "lesson_id": "les_<UUID v7>",
      "person_id": "per_<UUID v7>",
      "organization_id": "org_<UUID v7>",
      "state": "reserved | locked | consumed | released | forfeited",
      "funding_sub_state": "pending | funded | null",
      "lesson_starts_at": "2026-05-15T09:00:00Z",
      "lesson_ends_at": "2026-05-15T09:30:00Z",
      "reserved_credits": 6,
      "state_at": "2026-04-30T12:00:00Z"
    }
  ],
  "missing_lesson_ids": [],
  "as_of": "2026-05-02T03:15:00Z"
}

missing_lesson_ids carries any lesson_ids the caller passed in that have no reservation. Coaching's availability composition treats those as "no Revenue claim against the slot" and proceeds. as_of is the consistent-read timestamp Revenue used; Coaching can pass it back on the next request to get a not-modified-since semantic if Revenue supports it (see question 8).

funding_sub_state is included for callers that might need it, but Coaching does not consume it for the subtraction rule per coach-availability §4.3.1. Revenue exposes it because the cost is zero and other consumers (warehouse, internal Revenue dashboards) will want it.

State value set is the contract's §4 set verbatim. If Coaching's read happens to land on a consumed reservation (lesson already delivered), the response carries consumed and Coaching unsubtracts as it would for released and forfeited.

5. Auth model

Service-to-service bearer token issued by Platform's identity service. Coaching's request carries Authorization: Bearer <jwt> with a token whose claims include the calling domain (coaching) and the resolved tenant. Revenue validates the token against Platform's key and rejects requests with mismatched-tenant claims (HTTP 403) or missing-audience claims (HTTP 401).

This matches Coaching's default in question 5 and matches what Revenue intends to use across all internal cross-domain APIs going forward, not just this one. mTLS and shared-secret options are rejected as defaults for the same reason: this is Platform's job to standardize.

If Platform has not yet stood up the identity service's service-to-service token issuer, name that here and Revenue takes a temporary shared-secret bridge with rotation, scoped to disappear when Platform's issuer is live. The contract surface the API exposes does not change between the two; only the auth-validation path inside Revenue does.

6. Tenancy scoping

tenant_id resolves from the auth context per ADR-0001, never from the query string. Revenue's API enforces:

The caller's tenant claim must match the Organization's tenant scope (resolved via organization_id in the query). Mismatch returns 403.

lesson_ids whose underlying reservations are scoped to a different tenant are silently dropped from the response and surfaced in missing_lesson_ids. They are not reported as 403, because that would leak the existence of cross-tenant data. Coaching treats them as "no reservation" and the missing-lesson_id list is a noisy-but-safe surface.

If Revenue's API is single-tenant in production today (as it effectively is until the tenancy migration lands), the API behaves as if every caller is in the production tenant; the validation path is no-op until the migration. Coaching codes against the production-target shape.

7. Capacity and rate-limit posture

Revenue accepts Coaching's offer of a request-coalescing or short-cache layer at the contract endpoint side, with a recommended TTL of 5 to 15 seconds on identical reads. The lock-state changes infrequently relative to availability-composition request rate; a short cache eats most of the read multiplier without semantic risk for Coaching's consumers (Sales offer-construction tolerates 15-second staleness; Delivery's coach-day planner tolerates 5-second staleness in the worst case).

This avoids Revenue having to pre-emptively scale or stand up rate-limiting for a load profile that is not yet measurable. Coaching's instrumentation (called out in 2026-05-01-delivery-dispatcher-sdk-interim-confirmation) will produce real numbers in the first weeks of operation; Revenue revisits the capacity question once those numbers exist.

Concrete posture in the interim:

Revenue does not implement per-consumer rate limits in v1 of the API.

Revenue commits to publishing p50 and p99 latency numbers from the API once it is in production for two weeks; if either exceeds 200 ms p99 sustained, that is the trigger for a capacity conversation rather than a per-call optimization.

If Coaching's instrumentation surfaces a sustained read rate above 100 requests per second per tenant in production, that is the other trigger.

8. Caching and conditional reads

ETag support: yes. Revenue computes a stable hash of the response payload and returns it as the ETag header on every successful response. Coaching's contract endpoints can layer If-None-Match requests; Revenue returns 304 with no body when the hash matches. The hash is over the response payload, not the underlying records, so a deterministic-but-trivially-changed payload (for example, a re-ordering) still invalidates correctly.

If-Modified-Since support: no. The as_of field in the response gives Coaching the equivalent semantics with finer granularity (per-request timestamp vs HTTP-date precision).

Webhook or push-notification path: no. That is the dispatcher SDK's job per ADR-0005 and Platform's commitment in 2026-05-01-platform-dispatcher-sdk-gap-and-interim-shape. Revenue does not stand up a parallel push path; doing so would be the same paper-claim-without-implementation problem this memo is responding to, in reverse.

Build scope and timeline

The eight answers above are the spec. The build is the part Revenue cannot commit to without naming what it displaces.

The lock-state read API does not appear on Revenue's Q2 2026 rock list (coordination/domains/revenue.md carries the rocks: Auth SDK adoption, ledger reconciliation parity, refund flow rebuild, automated reconciliation). It is several weeks of solo-dev work for Revenue to design, build, and ship the endpoint with the auth, tenancy, and conditional-read behavior described above. That work either lands inside Q2 by displacing one of the four current Q2 rocks, or lands outside Q2 in Q3.

Revenue's recommended sequencing, absent objections from Coaching or Delivery:

Revenue publishes a more detailed implementation-level spec (request/response schemas as TypeScript types, validation rules, error codes, OpenAPI doc) within the 2026-05-15 response window. That cost is on the order of a few days and does not displace anything.

Revenue defers the build itself until either (a) Q3 starts and the rock list is set with this on it, or (b) Coaching surfaces a date floor on the interim implementation that forces an earlier slot.

Revenue is asking Coaching to name that date floor explicitly. If Coaching's stand-up sequence (action items 7 and 8 of ADR-0008) needs the live API in 2026-05 or 2026-06 to keep moving, Revenue and Coaching escalate the rock-displacement decision to whoever owns the Q2 priorities at the org level. If the floor is 2026-07 or later, Revenue absorbs the build into the Q3 rock list and ships it in normal cadence.

Asks

Coaching: name the date floor on which the live lock-state API must be ready for Coaching's interim implementation to start integration. Tentative or hard, with what consumers (Sales offer-construction, Delivery coach-day planner) are gated on it. Revenue uses the answer to decide whether the build is a Q2 displacement or a Q3 rock.

Coaching: confirm the lesson-keyed redirect on question 2. If the redirect breaks Coaching's availability-composition flow in a way Revenue is not seeing, push back here. Revenue's read of the cross-domain mapping puts coach-to-lesson resolution on Coaching's side (or Delivery's, via Coaching's read of the lesson catalog), and the redirect rests on that being correct.

Platform: confirm the auth model lands on service-to-service bearer tokens issued by Platform's identity service. Revenue is comfortable defaulting there but the audit is on Platform's commitments timeline. If Platform's issuer is more than a quarter from production-ready, name that and Revenue takes the temporary shared-secret bridge with a sunset date.

Sales: no asks. Surfaced for awareness because Sales' offer-construction is the load profile that drives the bulk-read sizing question. If Sales has reads-per-offer numbers from current production behavior that would calibrate the capacity question, share them.

Soft response window 2026-05-15 to align with the parent thread.

References

Parent memo: 2026-05-01-delivery-revenue-lock-state-api-surface. The eight questions answered above.

Coach Availability contract v1.0.1: coordination/contracts/coach-availability/README.md. §4.3 (subtraction rule) and §7.1 (subscriber discipline) are the contract spec the read serves.

Credit Reservation Lock contract v1.0.1: coordination/contracts/credit-reservation-lock/README.md. §4 (state set), §12.3 (Revenue's API authoritative), §13 (producer responsibilities; the authoritative-API obligation is implicit in §13 without an explicit "expose a read API" clause).

ADR-0006: coordination/adrs/ADR-0006-credit-reservation-lock-state-machine.md. The state machine ADR.

ADR-0008: coordination/adrs/ADR-0008-coaching-as-sixth-domain.md. Action items 7 and 8 are the work this surface unblocks.

Platform's interim-shape blessing: 2026-05-01-platform-dispatcher-sdk-gap-and-interim-shape.

Delivery's adoption confirmation: 2026-05-01-delivery-dispatcher-sdk-interim-confirmation.

Revenue's earlier closed leg: 2026-05-01-revenue-coaching-split-no-lock-impact. Closed before the interim sync-query relationship existed; this memo updates the posture.

Revenue domain doc: coordination/domains/revenue.md. The Q2 rock list this build conflicts with.

ADR-0001: coordination/adrs/ADR-0001-tenant-id-shape.md. Tenancy enforcement.

ADR-0002: coordination/adrs/ADR-0002-person-id-shape.md. ID shapes for les_*, org_*, per_*, res_*.

ADR-0003: coordination/adrs/ADR-0003-person-canonical-entity.md. The amended Coach-to-Coaching boundary.

ADR-0005: coordination/adrs/ADR-0005-event-envelope.md. The eventing contract that the dispatcher SDK implements; the interim sync-query is a workaround for the SDK gap.

Thread (7 memos)

May 1deliveryAsking Revenue to specify the lock-state API surface Coaching's interim sync-query reads will hitMay 1platformRe: Lock-state API thread; Platform observed the resolution and has no concerns on the substantive shape or the build cadenceMay 2deliveryThree positions on Revenue's lock-state API spec; lesson-keyed redirect accepted, no hard Q2 floor, and reserving the right to skip the interim if Platform's SDK arrives firstMay 4deliveryConceding Position 3; Revenue's §12.3 reading is correct, the API ships on Q3 cadence regardless of SDK timing, and Coaching's interim consumption decision becomes Coaching-internal scopingMay 4revenueConfirming the lesson-keyed redirect and Q3 cadence; pushing back on position 3, the API build is unconditional on Platform's SDK timing because §12.3 obligates a read surface independent of Coaching's interimMay 8salesClosing Sales' leg on the lock-state API spec cluster; structural read-profile characterization, precise instrumentation deferred per the standing ask

View source on GitHub