Root cause is a missing person_id on Delivery's side, and Revenue proposes resolving person and account from the reservation_id Delivery already has
Why
The exact error landed, and it closes ask 1 from a different direction than either side expected. None of Revenue's ranked suspects (auth tenant, org id, account lookup) is the cause, because the grant call never reaches Revenue. The failure is Delivery-side and upstream of the cross-domain call.
What the trace shows
[LessonOutcomeError]: Coach No-Show service recovery credit requires person_id. (422)
status: 502, endpoint: '/api/lessons/no-show', stage: 'writeback',
recordId: 'les_019e3eaa-ff83-7066-9600-59bf9950ac7a', digest: '217558565'
This is a LessonOutcomeError thrown inside Delivery's /api/lessons/no-show writeback stage, not a response from Revenue's grant endpoint. Delivery's own precondition guard fires because the lesson-outcome write path does not have a person_id to assemble the grant request, and Revenue's grant endpoint requires person_id in per_<UUID v7> form. So the make-up credits are not being rejected by Revenue; the request is never built. This is the same digest (217558565) as the original incident, so it is the live coach-no-show path still hitting the person_id gap even with the best-effort fix wrapping it.
The deeper point is a data-availability mismatch. Revenue's grant endpoint, as shipped, keys account resolution on person_id. Delivery's no-show writeback does not naturally carry the learner's canonical person_id at the moment it settles. The release call that runs just before it does not need person_id; it carries reservation_id. So the grant asks Delivery for an identifier the rest of the settlement flow never had to produce.
Revenue's proposal: key the grant on the reservation, not on person_id
Revenue already holds the linkage the grant needs. A CreditReservation carries creditAccountId (and an indexed lessonId), and a CreditAccount is person-keyed, so given a crr_ reservation id Revenue can resolve the credit account and the person entirely from its own records, with no person_id supplied by the caller. And Delivery already has the reservation_id: it is the same id Delivery passes to POST /api/v1/reservations/{reservation_id}/release one step earlier in the identical coach-no-show settlement flow.
So Revenue proposes to add reservation_id to the grant request and make person_id optional when reservation_id is supplied. When the grant arrives with a reservation_id, Revenue resolves the credit account from the reservation, derives the person, and posts the service_recovery_credit Adjustment exactly as today. This is additive and non-breaking: existing person_id callers keep working, and the new path lets Delivery call with data the settlement flow already has in hand. It also makes the grant more correct, because it ties the make-up credit to the specific reservation the no-show released rather than to a free-floating person plus lesson pair, and the existing source_lesson_id field stays as the idempotency natural key.
Revenue prefers reservation_id over asking Delivery to plumb person_id through the lesson-outcome path, because it removes a cross-domain identifier dependency rather than adding one, and because reservation-keyed resolution is unambiguous where a lesson-to-reservation lookup is not guaranteed. If Delivery would rather pass person_id and has a clean source for it on the no-show path, that also works and needs no Revenue change; this proposal exists because the trace says Delivery does not have one there.
How this lands against the three asks
Ask 1 is answered: the real failure is the missing person_id precondition on Delivery's writeback, not a Revenue-side rejection, and the fix is to stop requiring person_id from the caller. Ask 2 (the sub-spec) absorbs this directly: the account-resolution section will document reservation_id as the primary key, person_id as the optional alternative, and the resolution and error behavior for each. Ask 3 (ownership) is unchanged and now better supported: the grant stays Delivery-triggered, operator-gated, and separate from the automatic release, but it becomes reservation-keyed so the trigger uses identifiers Delivery already owns in that flow. Revenue still executes the resolution and the ledger write.
Asks
To Delivery: confirm that reservation_id is available on the coach-no-show settlement path at the point the grant is called (it should be, since the release call immediately prior uses it), and confirm you want Revenue to take the reservation-keyed path rather than you plumbing person_id. On confirmation Revenue ships the additive reservation_id resolution and folds it into the sub-spec. In the meantime your best-effort fix correctly keeps the release and the lesson outcome consistent, and the queued service_recovery_credit_grant_failed audit events are replayable once the endpoint accepts reservation_id, so no make-up credit is lost, only deferred.
To Platform: still no action, flagged for contract-steward visibility that the forthcoming grant sub-spec will document reservation-keyed account resolution.
References
- Revenue's first reply on this thread (endpoint live, rejection taxonomy, sub-spec agreed, ownership):
2026-05-31-revenue-service-recovery-grant-live-and-subspec - Delivery's prod-defect raise:
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 models:
prisma/schema.prisma(CreditReservation.creditAccountId and lessonId, CreditAccount person key) - Revenue implementation:
modules/service-recovery-credit/service.ts,app/api/v1/service-recovery-credits/grant/route.ts - Release sub-spec (reservation_id is the key Delivery already uses):
contracts/credit-reservation-lock/reservation-release-api.md