Event Fund Claims
Event Fund Claims API
Overview
When an event organizer sells tickets, revenue is held in escrow — one escrow entry per buyer checkout session. A Fund Claim is a formal request to release all currently HELD escrows for an event into the organizer's wallet. Every claim goes through a PENDING → APPROVED / REJECTED lifecycle, and funds only move on explicit admin approval.
Claim approval is an all-or-nothing escrow operation — it releases every HELD escrow for that event at the moment of approval. Partial escrow release is not supported by design (see Financial Safety). What the organizer does with the wallet balance after release is handled by the separate DisbursementService.
Authentication: All endpoints require Authorization: Bearer <jwt_token>
Business Rules
| Rule | Detail |
|---|---|
| One pending per event | At most ONE PENDING claim per event at any time |
| Claimable amount formula | totalRevenue − totalRefunded − totalClaimed − totalPendingClaims |
| Admin-only for active events | If event has not ended, only ADMIN can initiate a claim |
| Refund deadline | 3 days before event start. Past this → no refunds, organizer may claim early |
| Escrow release | Approval releases ALL HELD escrows for the event atomically → organizer wallet. No partial release |
| No partial amounts | Escrows are atomic per buyer. "80% release" has no business meaning at the escrow layer — staging happens via DisbursementService after funds land in wallet |
| Admin claims require note | adminNote is mandatory on admin-initiated claims |
| Refund safety | Refunds and approvals use pessimistic write lock on EscrowAccount to prevent race conditions |
Claim Status Lifecycle
| Status | Meaning |
|---|---|
PENDING |
Submitted, awaiting admin review. Escrow NOT yet released |
APPROVED |
Admin approved. Escrow atomically released to organizer wallet |
REJECTED |
Admin rejected. No funds moved. Organizer may submit a new claim |
CANCELLED |
Organizer cancelled before admin action. No funds moved |
Standard Response Format
Success
{
"success": true,
"httpStatus": "OK",
"message": "Fund claim submitted successfully",
"action_time": "2026-04-27T10:30:45",
"data": { }
}
Error
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "A pending claim already exists for this event",
"action_time": "2026-04-27T10:30:45",
"data": "A pending claim already exists for this event"
}
Endpoints
1. List All Claims (Admin)
Endpoint: GET /api/v1/e-events/claims
Access: 🔒 ROLE_SUPER_ADMIN or ROLE_STAFF_ADMIN
Purpose: Returns all fund claims across all events. Optionally filter by status.
Query Parameters:
| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
status |
ClaimStatus enum |
No | Filter by: PENDING, APPROVED, REJECTED, CANCELLED |
— (all returned) |
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Fund claims retrieved",
"data": [
{
"claimId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"claimNumber": "EFC-2026-000001",
"eventId": "uuid",
"eventTitle": "Dar Jazz Night",
"eventStatus": "PUBLISHED",
"organizerId": "uuid",
"organizerName": "Amina Hassan",
"status": "PENDING",
"claimedAmount": 50000.00,
"totalRevenueSnapshot": 80000.00,
"totalRefundedSnapshot": 5000.00,
"totalPreviouslyClaimedSnapshot": 0.00,
"totalPendingAtSubmission": 0.00,
"currency": "TZS",
"adminInitiated": false,
"adminId": null,
"adminNote": null,
"organizerNote": "Requesting first partial claim",
"reviewedById": null,
"reviewerName": null,
"reviewNote": null,
"reviewedAt": null,
"escrowsReleasedCount": 0,
"escrowsSkippedCount": 0,
"actualReleasedAmount": null,
"initiatedAt": "2026-04-20T08:00:00",
"updatedAt": "2026-04-20T08:00:00"
}
]
}
Response Fields:
| Field | Type | Description |
|---|---|---|
claimId |
UUID |
Unique claim identifier |
claimNumber |
string |
Human-readable reference, e.g. EFC-2026-000001 |
eventId |
UUID |
The event this claim belongs to |
eventTitle |
string |
Display name of the event |
eventStatus |
EventStatus |
Current event status |
organizerId |
UUID |
Organizer's account ID |
organizerName |
string |
Organizer's display name |
status |
ClaimStatus |
Current claim status |
claimedAmount |
BigDecimal |
Amount the organizer requested |
totalRevenueSnapshot |
BigDecimal |
Organizer's net revenue at time of submission |
totalRefundedSnapshot |
BigDecimal |
Total refunds issued at time of submission |
totalPreviouslyClaimedSnapshot |
BigDecimal |
Already released to wallet before this claim |
totalPendingAtSubmission |
BigDecimal |
Other pending claim amounts at submission time |
currency |
string |
Currency code, e.g. TZS |
adminInitiated |
boolean |
true if an admin submitted on behalf of organizer |
adminId |
UUID |
Admin's account ID (null if organizer-initiated) |
adminNote |
string |
Admin's reason for initiating (null if organizer-initiated) |
organizerNote |
string |
Optional note from organizer |
reviewedById |
UUID |
ID of admin who reviewed (null if pending) |
reviewerName |
string |
Name of reviewing admin (null if pending) |
reviewNote |
string |
Admin's review note (null if pending) |
reviewedAt |
LocalDateTime |
Timestamp of review (null if pending) |
escrowsReleasedCount |
Integer |
Number of escrow entries released on approval |
escrowsSkippedCount |
Integer |
Escrow entries skipped (e.g. already refunded) |
actualReleasedAmount |
BigDecimal |
Actual amount credited to wallet on approval |
initiatedAt |
LocalDateTime |
When the claim was submitted |
updatedAt |
LocalDateTime |
Last updated timestamp |
Possible Errors:
| HTTP Status | Description |
|---|---|
401 UNAUTHORIZED |
Invalid or expired JWT |
403 FORBIDDEN |
Caller does not have admin role |
2. Get My Claims (Organizer)
Endpoint: GET /api/v1/e-events/claims/my-claims
Access: 🔒 Authenticated organizer (any role)
Purpose: Returns all fund claims submitted by the currently authenticated organizer, across all their events.
No query parameters.
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "My fund claims retrieved",
"data": [ /* array of EventFundClaimResponse — same structure as endpoint 1 */ ]
}
Possible Errors:
| HTTP Status | Description |
|---|---|
401 UNAUTHORIZED |
Invalid or expired JWT |
3. Get Claim by ID
Endpoint: GET /api/v1/e-events/claims/{claimId}
Access: 🔒 Authenticated — admin sees any claim; organizer sees only their own
Purpose: Returns full details of a single fund claim.
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
claimId |
UUID |
Yes | The claim's unique identifier |
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Claim retrieved",
"data": { /* EventFundClaimResponse */ }
}
Possible Errors:
| HTTP Status | Description |
|---|---|
401 UNAUTHORIZED |
Invalid or expired JWT |
403 FORBIDDEN |
Organizer attempting to view another organizer's claim |
404 NOT_FOUND |
Claim with given ID does not exist |
4. Get Event Claims
Endpoint: GET /api/v1/e-events/claims/event/{eventId}
Access: 🔒 Authenticated — organizer (own events) or admin (any event)
Purpose: Returns all claims ever submitted for a specific event.
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
eventId |
UUID |
Yes | The event's unique identifier |
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Event claims retrieved",
"data": [ /* array of EventFundClaimResponse */ ]
}
Possible Errors:
| HTTP Status | Description |
|---|---|
401 UNAUTHORIZED |
Invalid or expired JWT |
403 FORBIDDEN |
Organizer does not own this event |
404 NOT_FOUND |
Event not found |
5. Get Claimable Amount
Endpoint: GET /api/v1/e-events/claims/event/{eventId}/claimable-amount
Access: 🔒 Authenticated — organizer (own events) or admin
Purpose: Returns the current claimable amount breakdown for an event, including eligibility status and the active pending claim if one exists.
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
eventId |
UUID |
Yes | The event's unique identifier |
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Claimable amount retrieved",
"data": {
"eventId": "uuid",
"eventTitle": "Dar Jazz Night",
"totalRevenue": 80000.00,
"totalRefunded": 5000.00,
"totalClaimed": 0.00,
"totalPendingClaims": 0.00,
"claimableAmount": 60000.00,
"currency": "TZS",
"eligible": true,
"ineligibilityReason": null,
"activePendingClaimId": null,
"refundDeadline": "2026-05-10T00:00:00+03:00",
"pastRefundDeadline": false
}
}
Note on claimable amount:
claimableAmount = totalRevenue − totalRefunded − totalClaimed − totalPendingClaims. This is a snapshot estimate used to indicate eligibility — theactualReleasedAmounton approval is the real number, as it reflects the liveHELDescrow balance at the moment of approval.
Response Fields:
| Field | Type | Description |
|---|---|---|
totalRevenue |
BigDecimal |
Net organizer share of all non-refunded tickets |
totalRefunded |
BigDecimal |
Gross amount returned to buyers |
totalClaimed |
BigDecimal |
Already released to organizer wallet |
totalPendingClaims |
BigDecimal |
Locked by current PENDING claims |
claimableAmount |
BigDecimal |
What the organizer can claim right now |
eligible |
boolean |
Whether a new claim can be submitted |
ineligibilityReason |
string |
Human-readable reason if eligible = false |
activePendingClaimId |
UUID |
ID of the existing pending claim, or null |
refundDeadline |
ZonedDateTime |
eventStartDate − 3 days |
pastRefundDeadline |
boolean |
true = refund window closed, full amount claimable |
Possible Errors:
| HTTP Status | Description |
|---|---|
401 UNAUTHORIZED |
Invalid or expired JWT |
403 FORBIDDEN |
Organizer does not own this event |
404 NOT_FOUND |
Event not found |
6. Get Revenue Summary (Admin)
Endpoint: GET /api/v1/e-events/claims/event/{eventId}/revenue-summary
Access: 🔒 ROLE_SUPER_ADMIN or ROLE_STAFF_ADMIN
Purpose: Returns a full financial summary of an event including gross sales, platform fees, refunds, claimed amounts, and escrow balance. Used for admin audit and oversight.
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
eventId |
UUID |
Yes | The event's unique identifier |
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Event revenue summary retrieved",
"data": {
"eventId": "uuid",
"eventTitle": "Dar Jazz Night",
"grossRevenue": 100000.00,
"totalRefunded": 5000.00,
"platformFees": 10000.00,
"netOrganizerRevenue": 85000.00,
"totalClaimed": 0.00,
"totalPendingClaims": 0.00,
"escrowBalance": 85000.00,
"currency": "TZS"
}
}
Possible Errors:
| HTTP Status | Description |
|---|---|
401 UNAUTHORIZED |
Invalid or expired JWT |
403 FORBIDDEN |
Caller does not have admin role |
404 NOT_FOUND |
Event not found |
7. Submit Claim (Organizer)
Endpoint: POST /api/v1/e-events/claims/event/{eventId}
Access: 🔒 Authenticated organizer — must own the event
Purpose: Organizer submits a fund claim for the full current claimable amount. The claim amount is computed server-side — organizer cannot override it.
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
eventId |
UUID |
Yes | The event to submit a claim for |
Request Body (optional):
{
"organizerNote": "Requesting first partial claim before event ends"
}
Request Body Fields:
| Field | Type | Required | Description | Validation |
|---|---|---|---|---|
organizerNote |
string |
No | Optional note from the organizer | Max 1000 characters |
Guard Rules Applied Server-Side:
- Event must belong to the authenticated organizer
- Event must have ended OR
pastRefundDeadline = true(otherwise only admin can claim) claimableAmountmust be > 0- No existing
PENDINGclaim for this event
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Fund claim submitted successfully",
"data": { /* EventFundClaimResponse with status: PENDING */ }
}
Possible Errors:
| HTTP Status | Description |
|---|---|
400 BAD_REQUEST |
A pending claim already exists for this event |
400 BAD_REQUEST |
Claimable amount is zero — nothing to claim |
400 BAD_REQUEST |
Event has not ended and refund deadline has not passed — only admin can claim |
401 UNAUTHORIZED |
Invalid or expired JWT |
403 FORBIDDEN |
Authenticated user does not own this event |
404 NOT_FOUND |
Event not found |
8. Admin-Initiate Claim
Endpoint: POST /api/v1/e-events/claims/event/{eventId}/admin-initiate
Access: 🔒 ROLE_SUPER_ADMIN or ROLE_STAFF_ADMIN
Purpose: Admin submits a fund claim on behalf of the organizer. Used when the event is still active (ticket sales ongoing). The claim is flagged adminInitiated = true with a mandatory audit note.
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
eventId |
UUID |
Yes | The event to claim funds for |
Request Body:
{
"adminNote": "Organizer requested early release. Event ending tomorrow, no pending refunds."
}
Request Body Fields:
| Field | Type | Required | Description | Validation |
|---|---|---|---|---|
adminNote |
string |
Yes | Mandatory reason for admin-initiated claim | Not blank |
Guard Rules Applied Server-Side:
claimableAmountmust be > 0- No existing
PENDINGclaim for this event - Claim is capped at
claimableAmount(never fulltotalRevenue)
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Admin-initiated claim created",
"data": {
"claimId": "uuid",
"claimNumber": "EFC-2026-000003",
"status": "PENDING",
"adminInitiated": true,
"adminId": "uuid-of-admin",
"adminNote": "Organizer requested early release. Event ending tomorrow, no pending refunds.",
"claimedAmount": 64000.00,
"currency": "TZS"
}
}
Possible Errors:
| HTTP Status | Description |
|---|---|
400 BAD_REQUEST |
A pending claim already exists for this event |
400 BAD_REQUEST |
Claimable amount is zero — nothing to claim |
401 UNAUTHORIZED |
Invalid or expired JWT |
403 FORBIDDEN |
Caller does not have admin role |
404 NOT_FOUND |
Event not found |
422 UNPROCESSABLE_ENTITY |
adminNote is blank |
9. Approve Claim (Admin)
Endpoint: POST /api/v1/e-events/claims/{claimId}/approve
Access: 🔒 ROLE_SUPER_ADMIN or ROLE_STAFF_ADMIN
Purpose: Admin approves a pending claim. This atomically acquires a pessimistic write lock on the EscrowAccount, fetches all escrows currently in HELD status for the event, releases every one of them, and credits the full released amount to the organizer's wallet.
Design note: Approval releases ALL
HELDescrows — not just theclaimedAmountsubset. Each escrow is atomic (one buyer's payment for one checkout session) so partial release has no valid business meaning. TheclaimedAmounton the claim is a snapshot estimate taken at submission time. TheactualReleasedAmountstored on approval is the authoritative number — it reflects the liveHELDbalance at the moment the admin approves, and may differ if refunds were processed in between.
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
claimId |
UUID |
Yes | The claim to approve |
Request Body (optional):
{
"reviewNote": "Verified escrow balance. Approved for release."
}
Request Body Fields:
| Field | Type | Required | Description |
|---|---|---|---|
reviewNote |
string |
No | Optional note from reviewing admin |
Critical: Approval acquires a
PESSIMISTIC_WRITElock on theEscrowAccountrow. Concurrent refund operations for the same event will block until the approval transaction completes, preventing escrow from going negative.
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Fund claim approved and funds released",
"data": {
"claimId": "uuid",
"claimNumber": "EFC-2026-000001",
"status": "APPROVED",
"claimedAmount": 50000.00,
"actualReleasedAmount": 48000.00,
"escrowsReleasedCount": 12,
"escrowsSkippedCount": 1,
"reviewedById": "admin-uuid",
"reviewerName": "Admin John",
"reviewNote": "Verified escrow balance. Approved for release.",
"reviewedAt": "2026-04-27T14:30:00"
}
}
Note on
actualReleasedAmount: May be less thanclaimedAmountif one or more escrow entries were refunded between claim submission and admin approval. TheescrowsSkippedCountindicates how many entries were skipped for this reason.
Possible Errors:
| HTTP Status | Description |
|---|---|
400 BAD_REQUEST |
Claim is not in PENDING status |
400 BAD_REQUEST |
Escrow balance insufficient to cover claim amount |
401 UNAUTHORIZED |
Invalid or expired JWT |
403 FORBIDDEN |
Caller does not have admin role |
404 NOT_FOUND |
Claim not found |
10. Reject Claim (Admin)
Endpoint: POST /api/v1/e-events/claims/{claimId}/reject
Access: 🔒 ROLE_SUPER_ADMIN or ROLE_STAFF_ADMIN
Purpose: Admin rejects a pending claim. No funds are moved. The organizer may submit a new claim after rejection.
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
claimId |
UUID |
Yes | The claim to reject |
Request Body (optional):
{
"reviewNote": "Pending dispute investigation. Please resubmit after resolution."
}
Request Body Fields:
| Field | Type | Required | Description |
|---|---|---|---|
reviewNote |
string |
No | Reason for rejection (shown to organizer) |
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Fund claim rejected",
"data": {
"claimId": "uuid",
"claimNumber": "EFC-2026-000001",
"status": "REJECTED",
"reviewedById": "admin-uuid",
"reviewerName": "Admin John",
"reviewNote": "Pending dispute investigation. Please resubmit after resolution.",
"reviewedAt": "2026-04-27T14:35:00"
}
}
Possible Errors:
| HTTP Status | Description |
|---|---|
400 BAD_REQUEST |
Claim is not in PENDING status |
401 UNAUTHORIZED |
Invalid or expired JWT |
403 FORBIDDEN |
Caller does not have admin role |
404 NOT_FOUND |
Claim not found |
11. Cancel Claim (Organizer)
Endpoint: DELETE /api/v1/e-events/claims/{claimId}
Access: 🔒 Authenticated organizer — must be the claim owner
Purpose: Organizer cancels their own pending claim before any admin action. This frees up the pending amount, allowing a new claim to be submitted.
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
claimId |
UUID |
Yes | The claim to cancel |
No request body.
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Fund claim cancelled",
"data": null
}
Possible Errors:
| HTTP Status | Description |
|---|---|
400 BAD_REQUEST |
Claim is not in PENDING status (already reviewed) |
401 UNAUTHORIZED |
Invalid or expired JWT |
403 FORBIDDEN |
Organizer does not own this claim |
404 NOT_FOUND |
Claim not found |
Financial Safety: Refund & Claim Concurrency
This section explains how the system prevents money from being over-released when a refund and a claim approval happen simultaneously, and why partial escrow release is not supported.
Why Partial Release Is Not Supported
Each escrow is atomic — it represents one buyer's payment for one checkout session. To release "80% of claimable funds" the service would have to arbitrarily pick specific escrow entries to release and leave others HELD. There is no business logic that justifies which individual buyer's payment stays locked — the organizer delivered the event to all of them equally.
10 escrows × 950 TZS = 9,500 TZS claimable
"Release 80%" = 7,600 TZS = release 8 escrows, hold 2
→ WHY are those 2 buyers' payments still locked?
→ No valid answer exists at the escrow layer.
The correct model is:
Claim approval → ALL HELD escrows released → Organizer wallet (full amount lands)
↓
Organizer controls staged
payouts via DisbursementService
The organizer's wallet is the right layer for staged payouts. The claim domain's job is simply: verify eligibility, get admin approval, atomically convert all HELD escrows into wallet balance.
The Race Condition Risk
Revenue = 10,000 TZS in HELD escrows
PENDING claim submitted
→ Refund of 2,000 processed concurrently
→ Claim approved → releases all HELD (now 8,000)
→ Refund deducts 2,000 from already-decremented escrow
→ Escrow goes negative 💀
Protections in Place
| Layer | Mechanism |
|---|---|
| Refund deadline | No refunds allowed within 3 days of event. Eliminates the race entirely post-deadline. |
| Pessimistic DB lock | Both approveClaim() and processRefund() acquire PESSIMISTIC_WRITE lock on the EscrowAccount row — serializing the two operations. |
| Balance check | Before any deduction, service verifies escrow.balance >= amount. Throws InsufficientEscrowException if not. |
| Snapshot vs actual | claimedAmount is a snapshot estimate at submission time. actualReleasedAmount is the real released amount at approval time — reflects live HELD balance after any intervening refunds. |
escrowsSkippedCount |
Tracks how many escrow entries were already REFUNDED or DISPUTED at approval time and therefore skipped. |
Claimable Amount Formula
claimableAmount = totalRevenue − totalRefunded − totalClaimed − totalPendingClaims
This formula gives an estimate of what's claimable. The actual amount released on approval equals the live sum of all HELD escrow sellerAmount values at that moment.
Endpoint Quick Reference
| # | Method | Path | Access | Description |
|---|---|---|---|---|
| 1 | GET |
/claims |
Admin | List all claims (filter by status) |
| 2 | GET |
/claims/my-claims |
Organizer | Get my own claims |
| 3 | GET |
/claims/{claimId} |
Organizer / Admin | Get single claim by ID |
| 4 | GET |
/claims/event/{eventId} |
Organizer / Admin | Get all claims for an event |
| 5 | GET |
/claims/event/{eventId}/claimable-amount |
Organizer / Admin | Get claimable amount breakdown |
| 6 | GET |
/claims/event/{eventId}/revenue-summary |
Admin | Full event revenue summary |
| 7 | POST |
/claims/event/{eventId} |
Organizer | Submit a new claim |
| 8 | POST |
/claims/event/{eventId}/admin-initiate |
Admin | Admin-initiate claim for active event |
| 9 | POST |
/claims/{claimId}/approve |
Admin | Approve claim + release escrow |
| 10 | POST |
/claims/{claimId}/reject |
Admin | Reject claim |
| 11 | DELETE |
/claims/{claimId} |
Organizer | Cancel pending claim |
Standard Error Reference
| HTTP Status | Scenario |
|---|---|
400 BAD_REQUEST |
Business rule violation (duplicate pending, zero claimable, wrong status) |
401 UNAUTHORIZED |
Missing, expired, or malformed JWT |
403 FORBIDDEN |
Insufficient role or ownership violation |
404 NOT_FOUND |
Event or claim does not exist |
422 UNPROCESSABLE_ENTITY |
Validation failure (e.g. blank adminNote) |
500 INTERNAL_SERVER_ERROR |
Unexpected server error |