Draft REST API specification. No controller code yet — this is the contract to implement against.
Base URL: http://localhost:8080/api/v1
Common Headers:
| Header | Required | Description |
|---|---|---|
Authorization |
Protected routes | Bearer <jwt> |
Idempotency-Key |
Commitment endpoint | UUID v4, unique per investor |
X-Correlation-Id |
Optional | Request trace ID; generated if absent |
Content-Type |
POST/PUT | application/json |
Common Error Envelope:
{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable description",
"correlationId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-06-29T10:00:00Z"
}
}POST /auth/register
Request:
{
"email": "investor@example.com",
"password": "SecurePass123!",
"role": "INVESTOR",
"fullName": "Jane Investor"
}role values: INVESTOR, ISSUER. Admin accounts are seeded, not registerable.
Response 201:
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"email": "investor@example.com",
"role": "INVESTOR",
"fullName": "Jane Investor",
"createdAt": "2026-06-29T10:00:00Z"
}POST /auth/login
Request:
{
"email": "investor@example.com",
"password": "SecurePass123!"
}Response 200:
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"tokenType": "Bearer",
"expiresIn": 3600,
"user": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"email": "investor@example.com",
"role": "INVESTOR",
"fullName": "Jane Investor"
}
}GET /auth/me
Authorization: Bearer <token>
Response 200:
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"email": "investor@example.com",
"role": "INVESTOR",
"fullName": "Jane Investor",
"walletBalance": 500000.00
}walletBalance present only for INVESTOR role.
POST /issuers/profile
Authorization: Bearer <token> (ISSUER role)
Request:
{
"companyName": "Acme Corp",
"registrationNumber": "REG-12345"
}Response 201:
{
"id": 1,
"companyName": "Acme Corp",
"registrationNumber": "REG-12345",
"userId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}POST /opportunities
Authorization: Bearer <token> (ISSUER or ADMIN)
Request:
{
"title": "Invoice #INV-2026-001",
"faceValue": 1000000.00,
"discountRate": 8.5,
"tenureDays": 90,
"minimumLot": 10000.00,
"riskGrade": "A",
"totalUnits": 100,
"unitPrice": 10000.00,
"description": "Payment receivable from verified buyer"
}Response 201:
{
"id": 42,
"title": "Invoice #INV-2026-001",
"faceValue": 1000000.00,
"discountRate": 8.5,
"tenureDays": 90,
"minimumLot": 10000.00,
"riskGrade": "A",
"totalUnits": 100,
"remainingUnits": 100,
"unitPrice": 10000.00,
"status": "DRAFT",
"issuerId": 1,
"createdAt": "2026-06-29T10:00:00Z"
}PUT /opportunities/{id}
Authorization: Bearer <token> (ISSUER owner or ADMIN)
Request: Same fields as create (partial update supported).
Response 200: Updated opportunity object.
POST /opportunities/{id}/submit
Authorization: Bearer <token> (ISSUER owner)
Transitions: DRAFT → UNDER_REVIEW
Response 200:
{
"id": 42,
"status": "UNDER_REVIEW",
"updatedAt": "2026-06-29T10:05:00Z"
}POST /opportunities/{id}/review
Authorization: Bearer <token> (ADMIN)
Request:
{
"action": "APPROVE",
"comment": "Documentation verified"
}action values: APPROVE (→ APPROVED), REJECT (→ DRAFT)
Response 200:
{
"id": 42,
"status": "APPROVED",
"reviewedAt": "2026-06-29T10:10:00Z",
"reviewComment": "Documentation verified"
}POST /opportunities/{id}/publish
Authorization: Bearer <token> (ADMIN)
Transitions: APPROVED → LIVE
Response 200:
{
"id": 42,
"status": "LIVE",
"publishedAt": "2026-06-29T10:15:00Z"
}GET /opportunities?status=LIVE&riskGrade=A&page=0&size=20&sort=maturityDate,asc
Authorization: Bearer <token>
Response 200:
{
"content": [
{
"id": 42,
"title": "Invoice #INV-2026-001",
"faceValue": 1000000.00,
"discountRate": 8.5,
"tenureDays": 90,
"riskGrade": "A",
"remainingUnits": 100,
"unitPrice": 10000.00,
"status": "LIVE",
"maturityDate": "2026-09-27T00:00:00Z"
}
],
"page": 0,
"size": 20,
"totalElements": 1,
"totalPages": 1
}GET /opportunities/{id}
Authorization: Bearer <token>
Response 200:
{
"id": 42,
"title": "Invoice #INV-2026-001",
"faceValue": 1000000.00,
"discountRate": 8.5,
"tenureDays": 90,
"minimumLot": 10000.00,
"riskGrade": "A",
"totalUnits": 100,
"remainingUnits": 60,
"unitPrice": 10000.00,
"status": "LIVE",
"description": "Payment receivable from verified buyer",
"issuerId": 1,
"issuerName": "Acme Corp",
"maturityDate": "2026-09-27T00:00:00Z",
"createdAt": "2026-06-29T10:00:00Z",
"publishedAt": "2026-06-29T10:15:00Z"
}POST /opportunities/{id}/transition
Authorization: Bearer <token> (ADMIN)
Request:
{
"targetStatus": "MATURED"
}Allowed admin transitions: LIVE → MATURED (manual demo trigger), FULLY_SUBSCRIBED → MATURED, MATURED → SETTLED
Response 200:
{
"id": 42,
"status": "MATURED",
"updatedAt": "2026-09-27T00:00:00Z"
}POST /opportunities/{id}/commitments
Authorization: Bearer <token> (INVESTOR)
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Request:
{
"unitsRequested": 10,
"amount": 100000.00
}amount must equal unitsRequested × unitPrice. Server validates against opportunity's unitPrice and minimumLot.
Response 201 — Full Fill:
{
"orderId": 101,
"opportunityId": 42,
"unitsRequested": 10,
"unitsAllocated": 10,
"amountRequested": 100000.00,
"amountAllocated": 100000.00,
"fillStatus": "FULL",
"status": "CONFIRMED",
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
"createdAt": "2026-06-29T11:00:00Z"
}Response 201 — Partial Fill:
{
"orderId": 102,
"opportunityId": 42,
"unitsRequested": 10,
"unitsAllocated": 4,
"amountRequested": 100000.00,
"amountAllocated": 40000.00,
"fillStatus": "PARTIAL",
"status": "CONFIRMED",
"idempotencyKey": "660e8400-e29b-41d4-a716-446655440001",
"createdAt": "2026-06-29T11:00:01Z"
}Response 200 — Idempotent Replay:
Same body as original response. HTTP 200 instead of 201.
Response 422 — Rejected:
{
"error": {
"code": "INSUFFICIENT_UNITS",
"message": "No units available for allocation",
"correlationId": "abc-123",
"timestamp": "2026-06-29T11:00:02Z"
}
}Other rejection codes: OPPORTUNITY_NOT_LIVE, INSUFFICIENT_FUNDS, BELOW_MINIMUM_LOT
GET /commitments/{orderId}
Authorization: Bearer <token> (INVESTOR owner or ADMIN)
Response 200:
{
"orderId": 101,
"opportunityId": 42,
"opportunityTitle": "Invoice #INV-2026-001",
"unitsRequested": 10,
"unitsAllocated": 10,
"amountAllocated": 100000.00,
"fillStatus": "FULL",
"status": "CONFIRMED",
"allocation": {
"allocationId": 201,
"units": 10,
"amount": 100000.00,
"discountAmount": 8500.00,
"expectedReturn": 108500.00,
"allocatedAt": "2026-06-29T11:00:00Z"
},
"createdAt": "2026-06-29T11:00:00Z"
}GET /commitments?page=0&size=20
Authorization: Bearer <token> (INVESTOR)
Response 200:
{
"content": [
{
"orderId": 101,
"opportunityId": 42,
"opportunityTitle": "Invoice #INV-2026-001",
"unitsAllocated": 10,
"amountAllocated": 100000.00,
"fillStatus": "FULL",
"status": "CONFIRMED",
"createdAt": "2026-06-29T11:00:00Z"
}
],
"page": 0,
"size": 20,
"totalElements": 1,
"totalPages": 1
}GET /portfolio
Authorization: Bearer <token> (INVESTOR)
Response 200:
{
"summary": {
"totalInvested": 250000.00,
"totalExpectedReturn": 271250.00,
"activePositions": 2,
"realizedYield": null
},
"positions": [
{
"positionId": 301,
"opportunityId": 42,
"opportunityTitle": "Invoice #INV-2026-001",
"investedAmount": 100000.00,
"expectedReturn": 108500.00,
"discountRate": 8.5,
"maturityDate": "2026-09-27T00:00:00Z",
"status": "ACTIVE",
"realizedYield": null
},
{
"positionId": 302,
"opportunityId": 43,
"opportunityTitle": "Invoice #INV-2026-002",
"investedAmount": 150000.00,
"expectedReturn": 162750.00,
"discountRate": 8.5,
"maturityDate": "2026-10-15T00:00:00Z",
"status": "ACTIVE",
"realizedYield": null
}
]
}GET /portfolio/positions/{positionId}
Authorization: Bearer <token> (INVESTOR owner)
Response 200:
{
"positionId": 301,
"opportunityId": 42,
"opportunityTitle": "Invoice #INV-2026-001",
"investedAmount": 100000.00,
"expectedReturn": 108500.00,
"discountRate": 8.5,
"tenureDays": 90,
"maturityDate": "2026-09-27T00:00:00Z",
"status": "ACTIVE",
"realizedYield": null,
"allocationId": 201,
"allocatedAt": "2026-06-29T11:00:00Z"
}GET /audit?entityType=Opportunity&entityId=42&page=0&size=50
Authorization: Bearer <token> (ADMIN)
Response 200:
{
"content": [
{
"id": 1001,
"actorId": "admin-uuid",
"actorRole": "ADMIN",
"action": "OPPORTUNITY_PUBLISHED",
"entityType": "Opportunity",
"entityId": 42,
"beforeState": { "status": "APPROVED" },
"afterState": { "status": "LIVE" },
"correlationId": "abc-123",
"createdAt": "2026-06-29T10:15:00Z"
},
{
"id": 1002,
"actorId": "investor-uuid",
"actorRole": "INVESTOR",
"action": "ALLOCATION_CREATED",
"entityType": "InvestmentOrder",
"entityId": 101,
"beforeState": null,
"afterState": {
"unitsAllocated": 10,
"amountAllocated": 100000.00,
"fillStatus": "FULL"
},
"correlationId": "def-456",
"createdAt": "2026-06-29T11:00:00Z"
}
],
"page": 0,
"size": 50,
"totalElements": 2,
"totalPages": 1
}GET /audit/{entityType}/{entityId}
Authorization: Bearer <token> (ADMIN)
Response 200:
{
"entityType": "Opportunity",
"entityId": 42,
"timeline": [
{
"id": 1000,
"action": "OPPORTUNITY_CREATED",
"actorRole": "ISSUER",
"afterState": { "status": "DRAFT" },
"createdAt": "2026-06-29T10:00:00Z"
},
{
"id": 1001,
"action": "OPPORTUNITY_PUBLISHED",
"actorRole": "ADMIN",
"afterState": { "status": "LIVE" },
"createdAt": "2026-06-29T10:15:00Z"
},
{
"id": 1002,
"action": "ALLOCATION_CREATED",
"actorRole": "INVESTOR",
"afterState": { "unitsAllocated": 10, "amountAllocated": 100000.00 },
"createdAt": "2026-06-29T11:00:00Z"
}
]
}GET /admin/outbox?status=PENDING&page=0&size=20
Authorization: Bearer <token> (ADMIN)
Response 200:
{
"content": [
{
"id": 501,
"eventType": "INVESTMENT_SUCCESSFUL",
"aggregateType": "Allocation",
"aggregateId": 201,
"payload": {
"investorId": "investor-uuid",
"opportunityId": 42,
"amountAllocated": 100000.00,
"unitsAllocated": 10
},
"status": "PENDING",
"createdAt": "2026-06-29T11:00:00Z"
}
],
"page": 0,
"size": 20,
"totalElements": 1,
"totalPages": 1
}POST /admin/outbox/poll
Authorization: Bearer <token> (ADMIN)
Manually triggers the outbox poller for demo purposes.
Response 200:
{
"processed": 3,
"published": 3,
"failed": 0
}GET /actuator/health
Response 200:
{
"status": "UP",
"components": {
"db": { "status": "UP" },
"redis": { "status": "UP" }
}
}| Code | Usage |
|---|---|
200 |
Successful read or idempotent replay |
201 |
Resource created (commitment, opportunity, registration) |
400 |
Validation error |
401 |
Missing or invalid JWT |
403 |
Insufficient role permissions |
404 |
Resource not found |
409 |
Conflict (e.g., invalid state transition) |
422 |
Business rule violation (insufficient units/funds) |
429 |
Rate limit exceeded |
| Value | Meaning |
|---|---|
FULL |
All requested units allocated |
PARTIAL |
Fewer units allocated than requested |
REJECTED |
Zero units allocated |
DRAFT | UNDER_REVIEW | APPROVED | LIVE | FULLY_SUBSCRIBED | MATURED | SETTLED
INVESTMENT_SUCCESSFUL | MATURITY_DUE | SETTLEMENT_COMPLETE