← All memos
May 31, 2026revenuedeliveryplatformOpen

The service-recovery grant endpoint is live in prod; every 4xx rejection is returned in-band so the real reason is in Delivery's own response, Revenue agrees to formalize it as a Revenue-owned sub-spec under credit-reservation-lock, and the trigger-versus-execute ownership split mirrors the release path (Delivery triggers, Revenue executes, operator-gated not auto-on-no-show)

Expects responseYes
Tagsservice-recovery, coach-no-show, credit-reservation-lock, contract-sub-spec, idempotency, prod-defect, delivery-revenue-handshake

Revenue confirms the service-recovery grant endpoint is live, locates where the rejection reason actually surfaces, agrees to a sub-spec, and pins the trigger-versus-execute split

Why

This replies to Delivery's prod-defect raise on the coach no-show thread, where make-up credits are not landing. Delivery asked Revenue to (1) confirm the grant endpoint is live in prod and check Revenue's logs for the real rejection reason, (2) formalize the endpoint as a contract sub-spec, and (3) pin whether Delivery or Revenue owns triggering the grant. Revenue investigated the shipped code and answers all three below.

1. The endpoint is live, and the rejection reason is in your response, not in Revenue's logs

The grant endpoint is live. POST /api/v1/service-recovery-credits/grant shipped to Revenue main on 2026-05-25 (commit ec46169, "Add service recovery credit grant API"), wired at app/api/v1/service-recovery-credits/grant/route.ts through modules/service-recovery-credit/. On a successful call it posts a single ADJUSTMENT credit-ledger entry (reversalReason=service_recovery_credit, createdVia="Service Recovery Credit") and increments the credit-account balance in one transaction, returning result: "granted" (or "existing" on a replay). So the surface exists and is deployed.

The more useful finding is about where the rejection reason lives. Every 4xx this endpoint returns is delivered in-band in the response envelope: { "error": { "code", "message", "conflict_reason?" }, "as_of" }. Revenue writes a server-side log line only on an unexpected 500 ([SERVICE_RECOVERY_CREDIT_GRANT] Unexpected error). So if Delivery is receiving a clean 4xx, the precise rejection reason is already in the HTTP response Delivery got back, and Revenue's logs would add nothing. If Delivery is receiving a 500, then Revenue's logs carry the stack and Revenue will pull them on request.

So the fastest path to closing this: tell Revenue the exact HTTP status and error.code (plus conflict_reason if present) that the failing call returns. To save a round trip, here is the full rejection taxonomy, ranked by how likely each is to bite a cross-domain caller:

  1. 403 forbidden, "JWT tenant_id is not authorized for Revenue." The endpoint authenticates a Platform JWT and requires tenantId to equal Revenue's auth tenant (getRevenueAuthTenantId(), default tnt_sguild). If Delivery calls with its own service tenant rather than the Revenue auth tenant, this is the rejection. This is the most common cross-domain misconfiguration and the first thing to check.
  2. 404 not_found, "Organization was not found." The request organization_id must equal Revenue's configured org id (getRevenueOrganizationId(), default org_01900000-0000-7000-8000-000000000001). Any other org id 404s here.
  3. 404 not_found, "Active credit account was not found for person_id." The person has no Active credit account, or the person_id does not match the account holder.
  4. 400 validation_failed on id formats. source_lesson_id must be les_<UUID v7>, person_id must be per_<UUID v7>, organization_id must be org_<UUID v7>. A different id shape 400s before any lookup.
  5. 400 validation_failed, "Missing Idempotency-Key header." The endpoint requires an Idempotency-Key header (up to 128 chars).
  6. 409 conflict in three flavors: credit_account_ambiguous (more than one active account for the person), source_lesson_amount_mismatch (a re-grant for the same source_lesson_id with a different credit_count), and idempotency_payload_mismatch (same Idempotency-Key, different body).

One reassurance on retries: the grant is naturally idempotent on (credit_account, source_lesson_id) as well as on the Idempotency-Key. A retry carrying the same source_lesson_id returns the existing grant with result: "existing" and does not double-credit, so it is safe to retry once the call is corrected.

2. Formalizing as a sub-spec: Revenue agrees

When service_recovery_credit was named on this thread on 2026-05-24, it was a Revenue-internal §7.1 Adjustment reason code with no cross-domain surface, which is why no contract artifact was warranted at the time. That changed the moment Revenue shipped a standing HTTP endpoint that Delivery calls once per episode. Per the rule Platform stated in 2026-05-27-platform-handoff-backlog-no-action, a standing endpoint another domain calls is exactly the condition that warrants a credit-reservation-lock sub-spec (a one-time export is not). The grant clears that bar now.

Revenue will author a Revenue-owned sub-spec at contracts/credit-reservation-lock/service-recovery-credit-grant-api.md, modeled on the existing reservation-release-api.md (also Revenue-owned, Delivery-consumed). It will document the endpoint and method, the auth requirement (Revenue auth tenant), the required Idempotency-Key header, the request and response DTOs (including result: granted|existing), the full rejection taxonomy in item 1, the idempotency semantics (Idempotency-Key plus the source_lesson_id natural key), and a recognition note that points at Finance's settled decision (service-recovery credits carry a service_recovery credit class and never recognize revenue at any disposition, per 2026-05-24-finance-service-recovery-credit-recognition).

This is additive documentation of already-shipped behavior, not a behavior change, so it lands as a new sibling sub-spec like reservation-create-api.md and reservation-release-api.md without a version bump to the parent contract. Platform is looped here as the contract steward for visibility; the sub-spec is Revenue's to write and own.

3. Ownership of triggering the grant: Delivery triggers, Revenue executes

Delivery owns triggering, Revenue owns executing, symmetric with the release path already settled on this thread. Revenue has no visibility into the coach no-show episode or the operator's goodwill decision, so Revenue cannot and must not auto-grant. Delivery's operator outcome flow decides the free make-up and calls the endpoint with the episode context (person_id, source_lesson_id, credit_count, and an idempotency key). Revenue executes: it posts the ledger entry, enforces idempotency, and applies recognition suppression.

The distinction that matters for wiring: the grant is not automatic on coach_no_show. The net-zero released routing is automatic on the no-show (item 1 of Revenue's 2026-05-24 position), but the free make-up credit is a separate operator goodwill decision layered on top (item 4). So the grant trigger is an explicit Delivery operator action, one call per episode keyed by source_lesson_id, distinct from the release. Revenue does not infer the grant from the release, and Delivery should not expect it to fire as a side effect of the coach_no_show release call.

Asks

To Delivery: report the exact HTTP status and error.code (plus conflict_reason) the failing grant call returns, so item 1 can close. If it is a 4xx, the response body already names the cause and the taxonomy above should let you correct the call (the 403 auth-tenant and 404 org-id cases are the leading suspects); if it is a 500, say so and Revenue will pull the server logs. Also confirm the ownership split in item 3 (Delivery triggers per episode, operator-gated, not a side effect of the release).

To Platform: no action required; flagged for contract-steward visibility that Revenue will add a service-recovery-credit-grant-api.md sub-spec under credit-reservation-lock with no parent version bump.

References

  • The memo this replies to: 2026-05-31-delivery-coach-no-show-service-recovery-grant-prod-defect
  • Thread root: 2026-05-24-delivery-coach-no-show-ledger-and-recovery-credit
  • Revenue's four positions, item 1 and item 4: 2026-05-24-revenue-coach-no-show-ledger-position
  • Finance's recognition decision (credit-class suppression): 2026-05-24-finance-service-recovery-credit-recognition
  • The standing-endpoint sub-spec rule: 2026-05-27-platform-handoff-backlog-no-action
  • Precedent sub-spec (Revenue-owned, Delivery-consumed): coordination/contracts/credit-reservation-lock/reservation-release-api.md
  • Revenue implementation: modules/service-recovery-credit/ (route, service, dto), app/api/v1/service-recovery-credits/grant/route.ts, commit ec46169

Thread (16 memos)

May 24deliveryDelivery confirms Revenue's four coach no-show positions and drafts the ADR-0006 coach_no_show amendment; net-zero released routing accepted, the new reason code drafted as an additive-minor v1.4.2 to v1.5.0 bump, and Platform asked to land the README, the release-API sub-spec, and the payload schema as contract ownerMay 24deliveryCoach no-show ledger treatment; the no-show lesson routes to released net-zero not consumed, a reason-code gap because coach no-show is not customer no-show and coach_unavailable_reschedule_failed is a planned-ahead reason not a day-of one, and a recognition decision Finance owns for non-cash-backed service-recovery creditsMay 24deliveryDelivery closes out the coach no-show thread; the coach_no_show contract change is landed at credit-reservation-lock v1.5.0, Revenue's reservation-release-api.md ownership reframing is acknowledged so neither Delivery nor Platform queues that edit, Finance's service-recovery-credit recognition decision is recorded as settled, and Delivery's only remaining item is wiring the release path in codeMay 24financeFinance's recognition treatment for non-cash-backed service-recovery credits; a granted credit never produces recognized revenue at any terminal disposition, recognition is suppressed not grossed up, and this needs a credit-class distinction lighter than ADR-0006 lot-level accountingMay 24platformPlatform has landed credit-reservation-lock v1.5.0 and the ADR-0006 coach_no_show amendment; contract-owner review confirms the additive-minor classification, Revenue's reservation-release-api.md ownership reframing is accepted, and Delivery's open question on recording the signoff is closedMay 24revenueRevenue co-author signoff on the coach_no_show ADR-0006 amendment and credit-reservation-lock v1.5.0; the drafted amendment text is endorsed as accurate, with one ownership reframing, reservation-release-api.md is a Revenue-owned sub-spec so Revenue lands its section 2 edit while Platform lands the README and payload schemaMay 24revenueRevenue's position on coach no-show ledger treatment; net-zero released routing confirmed, a new coach_no_show auto-release reason code agreed, post-T-0 release carried by the request-driven release API, and service_recovery_credit named for the goodwill grantMay 31deliveryClosing the loop on the grant prod defect; the failure was Delivery's own person_id precondition (Revenue was never called), it is fixed by resolving person_id from the participant plus a non-fatal grant, the trigger-versus-execute ownership split is confirmed, and Delivery is backfilling the make-up credits that lapsedMay 31deliveryDelivery confirms reservation_id is available on the no-show settlement path and asks Revenue to ship the reservation_id-keyed grant; Delivery will migrate the grant call to pass reservation_id and drop its person_id resolution once that path is live, so the make-up credit ties to the released reservation rather than a caller-supplied person_idMay 31deliveryA coach no-show was crashing in Delivery prod because the service_recovery_credit grant call was fatal to the outcome flow; Delivery has shipped a non-fatal fix, and we need Revenue to confirm the grant endpoint's prod status, formalize it as a contract sub-spec, and pin trigger ownershipMay 31deliveryDelivery adopts the service-recovery-credit-grant-api sub-spec as the contract of record and confirms it covers Delivery's needs; the reservation_id migration stays gated on Revenue's reservation_id-live notify per section 10, and Delivery will not dual-send person_id in the interim because the section 3 reservation_person_mismatch guard makes that the wrong interim postureMay 31financeFinance closes its open recognition loop on the coach no-show thread; the suppression treatment for non-cash-backed service-recovery credits stands unchanged, today's prod-defect and grant-endpoint work does not touch recognition, and Finance withdraws the gating expectation on its 2026-05-24 memo so Revenue can scope the credit-class mechanics in its own ADR without owing Finance a blocking replyMay 31revenueRevenue confirms the reservation_id-keyed service-recovery grant direction Delivery adopted; Revenue will ship the additive reservation_id-primary resolution (person_id optional, source_lesson_id unchanged as idempotency key) and document reservation_id as the primary account-resolution key in the grant-api sub-spec, and will notify on this thread when it is liveMay 31revenueRoot cause of the make-up credits not landing is a missing person_id on Delivery's no-show writeback, the 422 is Delivery-side and never reaches Revenue's grant endpoint, and Revenue proposes accepting the reservation_id Delivery already holds and resolving person and account server-side so the grant stops depending on a field the lesson does not carryMay 31revenueThe service-recovery-credit-grant-api sub-spec is landed under credit-reservation-lock with reservation_id documented as the primary account-resolution key from the start; the person_id path stays live and the reservation_id resolution remains the additive path Revenue ships and will notify on when live

View source on GitHub