Base URL: http://localhost:5000/api
All request bodies must use Content-Type: application/json.
All error responses follow a consistent envelope (see Error Responses).
POST /api/payments/intent and POST /api/payments/verify require an Idempotency-Key header. Use any unique string (e.g. a UUID). The server caches the response for 24 hours — sending the same key again returns the cached result without re-processing.
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Missing the header returns:
HTTP 400
{ "error": "Idempotency-Key header is required for this request", "code": "MISSING_IDEMPOTENCY_KEY" }GET /health
Response 200:
{ "status": "ok" }POST /api/students
Creates a new student record. If feeAmount is omitted, the fee is automatically
assigned from the active fee structure for the given class.
Request body
| Field | Type | Required | Validation |
|---|---|---|---|
studentId |
string | Yes | 3–20 alphanumeric characters, hyphens, underscores |
name |
string | Yes | Non-empty string |
class |
string | Yes | Non-empty string |
feeAmount |
number | No | Positive number; auto-assigned from fee structure if omitted |
Example request
curl -X POST http://localhost:5000/api/students \
-H "Content-Type: application/json" \
-d '{
"studentId": "STU001",
"name": "Alice Johnson",
"class": "Grade 5A"
}'Response 201 Created
{
"studentId": "STU001",
"name": "Alice Johnson",
"class": "Grade 5A",
"feeAmount": 250,
"feePaid": false,
"totalPaid": 0,
"remainingBalance": null,
"createdAt": "2026-03-24T10:00:00.000Z",
"updatedAt": "2026-03-24T10:00:00.000Z"
}Validation errors 400
{
"errors": [
"studentId must be 3–20 alphanumeric characters",
"name is required"
]
}No fee structure found 400
{
"error": "No fee amount provided and no fee structure found for class \"Grade 5A\". Please create a fee structure first or provide feeAmount.",
"code": "VALIDATION_ERROR"
}GET /api/students
Response 200: array of student objects, sorted by createdAt descending.
Returns all registered students, sorted by most recently created.
Example request
curl http://localhost:5000/api/studentsResponse 200 OK
[
{
"studentId": "STU001",
"name": "Alice Johnson",
"class": "Grade 5A",
"feeAmount": 250,
"feePaid": true,
"totalPaid": 250,
"remainingBalance": null,
"createdAt": "2026-03-24T10:00:00.000Z",
"updatedAt": "2026-03-24T10:00:00.000Z"
}
]GET /api/students/:studentId
Response 200: student object.
Errors:
400— invalidstudentIdformat (must be 3–20 alphanumeric characters)404— student not found
POST /api/fees
Body:
{ "className": "5A", "feeAmount": 250, "description": "Grade 5A annual fees", "academicYear": "2026" }description and academicYear are optional. If a fee structure for the class already exists it is updated (upsert).
Response 201:
{ "className": "5A", "feeAmount": 250, "description": "Grade 5A annual fees", "academicYear": "2026", "isActive": true }Errors:
400—classNameorfeeAmountmissing/invalid
GET /api/fees
Response 200: array of active fee structures, sorted by className.
GET /api/fees/:className
Response 200: fee structure object.
Errors:
404— no active fee structure found for the class
DELETE /api/fees/:className
Response 200:
{ "message": "Fee structure for class 5A deactivated" }Errors:
404— fee structure not found
Path parameter
| Parameter | Validation |
|---|---|
studentId |
3–20 alphanumeric characters, hyphens, underscores |
Example request
curl http://localhost:5000/api/students/STU001Response 200 OK
{
"studentId": "STU001",
"name": "Alice Johnson",
"class": "Grade 5A",
"feeAmount": 250,
"feePaid": true,
"totalPaid": 250,
"remainingBalance": null,
"createdAt": "2026-03-24T10:00:00.000Z",
"updatedAt": "2026-03-24T10:00:00.000Z"
}Not found 404
{ "error": "Student not found", "code": "NOT_FOUND" }POST /api/fees
Creates a fee structure for a class. If a structure already exists for the class,
it is updated (upsert). The new fee applies to students registered after this point;
existing students retain their assigned feeAmount.
Request body
| Field | Type | Required | Validation |
|---|---|---|---|
className |
string | Yes | Non-empty string |
feeAmount |
number | Yes | Positive number |
description |
string | No | Free text |
academicYear |
string | No | Defaults to current year |
Example request
curl -X POST http://localhost:5000/api/fees \
-H "Content-Type: application/json" \
-d '{
"className": "Grade 5A",
"feeAmount": 250,
"description": "Annual tuition fees",
"academicYear": "2026"
}'Response 201 Created
{
"_id": "...",
"className": "Grade 5A",
"feeAmount": 250,
"description": "Annual tuition fees",
"academicYear": "2026",
"isActive": true,
"createdAt": "2026-03-24T10:00:00.000Z",
"updatedAt": "2026-03-24T10:00:00.000Z"
}Validation errors 400
{
"errors": [
"className is required",
"feeAmount must be a positive number"
]
}GET /api/fees
Returns all active fee structures sorted alphabetically by class name.
Example request
curl http://localhost:5000/api/feesResponse 200 OK
[
{
"_id": "...",
"className": "Grade 5A",
"feeAmount": 250,
"description": "Annual tuition fees",
"academicYear": "2026",
"isActive": true,
"createdAt": "2026-03-24T10:00:00.000Z",
"updatedAt": "2026-03-24T10:00:00.000Z"
}
]GET /api/fees/:className
Path parameter
| Parameter | Description |
|---|---|
className |
URL-encoded class name (e.g. Grade%205A) |
Example request
curl http://localhost:5000/api/fees/Grade%205AResponse 200 OK
{
"_id": "...",
"className": "Grade 5A",
"feeAmount": 250,
"description": "Annual tuition fees",
"academicYear": "2026",
"isActive": true,
"createdAt": "2026-03-24T10:00:00.000Z",
"updatedAt": "2026-03-24T10:00:00.000Z"
}Not found 404
{ "error": "No fee structure found for class Grade 5A", "code": "NOT_FOUND" }DELETE /api/fees/:className
Soft-deletes the fee structure by setting isActive: false. The record is retained
for audit purposes.
Example request
curl -X DELETE http://localhost:5000/api/fees/Grade%205AResponse 200 OK
{ "message": "Fee structure for class Grade 5A deactivated" }Not found 404
{ "error": "Fee structure not found", "code": "NOT_FOUND" }POST /api/payments/submit
Body:
{ "xdr": "AAAA..." }The backend decodes this XDR, records a
SUBMITTEDtrail, and pushes it to the Stellar ecosystem.
Response 200:
{ "verified": true, "hash": "abc...", "ledger": 1234, "status": "SUCCESS" }GET /api/payments/instructions/:studentId
Returns the school wallet address, the memo the parent must include, accepted assets, and current payment limits. Share this with the parent before they send a Stellar transaction.
Path parameter
| Parameter | Validation |
|---|---|
studentId |
3–20 alphanumeric characters, hyphens, underscores |
Example request
curl http://localhost:5000/api/payments/instructions/STU001Response 200 OK
{
"walletAddress": "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"memo": "STU001",
"acceptedAssets": [
{ "code": "XLM", "type": "native", "displayName": "Stellar Lumens" },
{ "code": "USDC", "type": "credit_alphanum4", "displayName": "USD Coin" }
],
"paymentLimits": {
"min": 0.01,
"max": 100000
},
"note": "Include the payment intent memo exactly when sending payment to ensure your fees are credited."
}POST /api/payments/intent
Generates a unique, time-limited memo for a student's payment. The intent expires
after 2 hours. Use the returned memo instead of the raw studentId when a
one-time reference is preferred.
Request body
| Field | Type | Required |
|---|---|---|
studentId |
string | Yes |
Example request
curl -X POST http://localhost:5000/api/payments/intent \
-H "Content-Type: application/json" \
-d '{ "studentId": "STU001" }'Response 201 Created
{
"_id": "...",
"studentId": "STU001",
"amount": 250,
"memo": "A3F1C2B4",
"status": "pending",
"expiresAt": "2026-03-24T12:00:00.000Z",
"createdAt": "2026-03-24T10:00:00.000Z",
"updatedAt": "2026-03-24T10:00:00.000Z"
}Student not found 404
{ "error": "Student not found", "code": "NOT_FOUND" }Fee outside payment limits 400
{ "error": "Payment amount exceeds maximum limit", "code": "AMOUNT_TOO_HIGH" }POST /api/payments/verify
Looks up a Stellar transaction by hash, validates it against the school wallet and accepted assets, records the payment, and returns the verification result.
If the Stellar network is temporarily unavailable, the transaction is queued for
automatic retry and a 202 Accepted response is returned.
Request body
| Field | Type | Required | Validation |
|---|---|---|---|
txHash |
string | Yes | 64-character lowercase hex string |
Example request
curl -X POST http://localhost:5000/api/payments/verify \
-H "Content-Type: application/json" \
-d '{ "txHash": "a1b2c3d4e5f6...64charhex...a1b2c3d4e5f6" }'Response 200 OK
{
"verified": true,
"hash": "a1b2c3d4e5f6...64charhex...a1b2c3d4e5f6",
"memo": "STU001",
"studentId": "STU001",
"amount": 250,
"assetCode": "XLM",
"assetType": "native",
"feeAmount": 250,
"feeValidation": {
"status": "valid",
"excessAmount": 0,
"message": "Payment matches the required fee"
},
"date": "2026-03-24T10:00:00Z"
}feeValidation.status values:
| Status | Meaning | feePaid updated? |
|---|---|---|
valid |
Amount exactly matches the required fee | Yes |
overpaid |
Amount exceeds the required fee | Yes |
underpaid |
Amount is less than required | No |
unknown |
Student not found or memo missing | No |
Queued for retry 202 Accepted (transient Stellar network error)
{
"message": "Stellar network is temporarily unavailable. Your transaction has been queued and will be verified automatically once the network recovers.",
"txHash": "a1b2c3d4...",
"status": "queued_for_retry"
}Validation error 400
{ "error": "txHash must be a 64-character hex string" }Already recorded 409
{ "error": "Transaction a1b2c3d4... has already been processed", "code": "DUPLICATE_TX" }POST /api/payments/sync
Fetches the 20 most recent transactions to the school wallet from the Stellar Horizon API, matches memos to registered students, validates amounts, and records any new payments. Safe to call repeatedly — duplicate transactions are skipped.
Example request
curl -X POST http://localhost:5000/api/payments/syncResponse 200 OK
{ "message": "Sync complete" }Stellar network unavailable 502
{ "error": "Stellar network error: ...", "code": "STELLAR_NETWORK_ERROR" }POST /api/payments/finalize
Promotes payments with confirmationStatus: "pending_confirmation" to "confirmed"
after verifying their ledger status. Called automatically by the background polling
service; can also be triggered manually.
Example request
curl -X POST http://localhost:5000/api/payments/finalizeResponse 200 OK
{ "message": "Finalization complete" }GET /api/payments/:studentId
Response 200: array of payment objects, sorted by confirmedAt descending.
Returns all recorded payments for a student, sorted by most recently confirmed.
Path parameter
| Parameter | Validation |
|---|---|
studentId |
3–20 alphanumeric characters, hyphens, underscores |
Example request
curl http://localhost:5000/api/payments/STU001Response 200 OK
[
{
"_id": "...",
"studentId": "STU001",
"txHash": "a1b2c3d4...",
"amount": 250,
"feeAmount": 250,
"feeValidationStatus": "valid",
"excessAmount": 0,
"status": "confirmed",
"memo": "STU001",
"senderAddress": "GPARENT...",
"isSuspicious": false,
"suspicionReason": null,
"ledger": 12345678,
"confirmationStatus": "confirmed",
"transactionHash": "a1b2c3d4...",
"confirmedAt": "2026-03-24T10:00:00.000Z",
"verifiedAt": "2026-03-24T10:01:00.000Z",
"createdAt": "2026-03-24T10:01:00.000Z",
"updatedAt": "2026-03-24T10:01:00.000Z"
}
]GET /api/payments/balance/:studentId
Returns a real-time balance summary for a student, aggregated across all confirmed payments.
Example request
curl http://localhost:5000/api/payments/balance/STU001Response 200 OK
{
"studentId": "STU001",
"feeAmount": 250,
"totalPaid": 250,
"remainingBalance": 0,
"excessAmount": 0,
"feePaid": true,
"installmentCount": 1
}Not found 404
{ "error": "Student not found", "code": "NOT_FOUND" }
### Get student balance
GET /api/payments/balance/:studentId
Returns the cumulative payment summary for a student, aggregated across all confirmed payments.
Returns the list of Stellar assets the school wallet accepts.
**Example request**
```bash
curl http://localhost:5000/api/payments/accepted-assets
Response 200 OK
{
"assets": [
{ "code": "XLM", "type": "native", "displayName": "Stellar Lumens" },
{ "code": "USDC", "type": "credit_alphanum4", "displayName": "USD Coin" }
]
}GET /api/payments/limits
Returns the configured minimum and maximum payment amounts.
Example request
curl http://localhost:5000/api/payments/limitsResponse 200 OK
{
"min": 0.01,
"max": 100000,
"message": "Payment amounts must be between 0.01 and 100000"
}GET /api/payments/overpayments
Returns payments flagged as suspicious (memo collision or unusual amount).
Returns all payments where the amount exceeded the required fee, along with the total excess collected.
Example request
curl http://localhost:5000/api/payments/overpaymentsResponse 200 OK
{
"count": 2,
"totalExcess": 15.5,
"overpayments": [
{
"studentId": "STU002",
"txHash": "b2c3d4...",
"amount": 260,
"feeAmount": 250,
"feeValidationStatus": "overpaid",
"excessAmount": 10,
"confirmedAt": "2026-03-24T10:00:00.000Z"
}
]
}GET /api/payments/suspicious
Returns payments flagged as suspicious (e.g. duplicate sender, unusual amount patterns). Intended for admin review.
Example request
curl http://localhost:5000/api/payments/suspiciousReturns payments with confirmationStatus: "pending_confirmation".
Response 200 OK
{
"count": 1,
"suspicious": [
{
"studentId": "STU003",
"txHash": "c3d4e5...",
"amount": 0.001,
"isSuspicious": true,
"suspicionReason": "Amount below expected threshold",
"confirmedAt": "2026-03-24T10:00:00.000Z"
}
]
}GET /api/payments/pending
Returns payments with confirmationStatus: "pending_confirmation" — transactions
seen on the network but not yet finalized.
Example request
curl http://localhost:5000/api/payments/pendingResponse 200 OK
{
"count": 1,
"pending": [
{
"studentId": "STU001",
"txHash": "d4e5f6...",
"amount": 250,
"confirmationStatus": "pending_confirmation",
"createdAt": "2026-03-24T10:00:00.000Z"
}
]
}GET /api/payments/retry-queue
Observability endpoint for the automatic retry worker. Shows transactions that failed due to transient Stellar network errors and are awaiting re-verification.
Example request
curl http://localhost:5000/api/payments/retry-queueResponse 200 OK
{
"pending": {
"count": 1,
"items": [
{
"txHash": "e5f6a7...",
"studentId": "STU001",
"status": "pending",
"attempts": 2,
"nextRetryAt": "2026-03-24T10:05:00.000Z",
"lastError": "Stellar network timeout"
}
]
},
"dead_letter": {
"count": 0,
"items": []
},
"recently_resolved": {
"count": 1,
"items": [
{
"txHash": "f6a7b8...",
"status": "resolved",
"resolvedAt": "2026-03-24T09:50:00.000Z"
}
]
}
}GET /api/reports
Returns a payment summary report aggregated by date. Optionally filter by date range. Supports JSON (default) and CSV output.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
startDate |
string | No | ISO date string, e.g. 2026-01-01. Inclusive. |
endDate |
string | No | ISO date string, e.g. 2026-12-31. Inclusive (full day). |
format |
string | No | json (default) or csv |
Example requests
# JSON report for a date range
curl "http://localhost:5000/api/reports?startDate=2026-01-01&endDate=2026-03-31"
# CSV download
curl "http://localhost:5000/api/reports?format=csv" -o report.csv
# All-time report
curl http://localhost:5000/api/reportsResponse 200 OK (JSON)
{
"generatedAt": "2026-03-24T10:00:00.000Z",
"period": {
"startDate": "2026-01-01",
"endDate": "2026-03-31"
},
"summary": {
"totalAmount": 1250,
"paymentCount": 5,
"validCount": 4,
"overpaidCount": 1,
"underpaidCount": 0,
"fullyPaidStudentCount": 4
},
"byDate": [
{
"date": "2026-03-24",
"totalAmount": 500,
"paymentCount": 2,
"validCount": 2,
"overpaidCount": 0,
"underpaidCount": 0,
"uniqueStudentCount": 2
}
]
}Response 200 OK (CSV) — Content-Disposition: attachment; filename="school-payment-report_2026-01-01_2026-03-31.csv"
Generated At,2026-03-24T10:00:00.000Z
Period Start,2026-01-01
Period End,2026-03-31
--- Summary ---
Total Amount,1250
Total Payments,5
Valid Payments,4
Overpaid,1
Underpaid,0
Fully Paid Students,4
--- Daily Breakdown ---
Date,Total Amount,Payment Count,Valid,Overpaid,Underpaid,Unique Students
2026-03-24,500,2,2,0,0,2
Validation errors 400
{ "error": "startDate must be before or equal to endDate", "code": "VALIDATION_ERROR" }GET /health
Simple liveness probe. Returns 200 when the server is running.
Example request
curl http://localhost:5000/healthResponse 200 OK
{ "status": "ok" }All errors return a JSON body with error (human-readable message) and code
(machine-readable identifier).
{ "error": "Human-readable description", "code": "ERROR_CODE" }| Code | HTTP Status | Description |
|---|---|---|
VALIDATION_ERROR |
400 | Request body or query parameter failed validation |
TX_FAILED |
400 | Stellar transaction failed on-chain |
MISSING_MEMO |
400 | Transaction has no memo field |
INVALID_DESTINATION |
400 | Transaction was sent to a different wallet |
UNSUPPORTED_ASSET |
400 | Payment made in an asset not in the accepted list |
AMOUNT_TOO_LOW |
400 | Payment amount is below MIN_PAYMENT_AMOUNT |
AMOUNT_TOO_HIGH |
400 | Payment amount exceeds MAX_PAYMENT_AMOUNT |
NOT_FOUND |
404 | Requested resource does not exist |
DUPLICATE_TX |
409 | Transaction hash has already been recorded |
STELLAR_NETWORK_ERROR |
502 | Stellar Horizon API is unreachable or returned an error |
INTERNAL_ERROR |
500 | Unexpected server error |
Middleware validation failures (invalid body fields) return an errors array
instead of a single error string:
{
"errors": [
"studentId must be 3–20 alphanumeric characters",
"name is required"
]
}| Code | HTTP Status | Description |
|---|---|---|
VALIDATION_ERROR |
400 | Invalid request body or params |
MISSING_IDEMPOTENCY_KEY |
400 | Idempotency-Key header missing |
TX_FAILED |
400 | Stellar transaction failed on-chain |
MISSING_MEMO |
400 | Transaction has no memo |
INVALID_DESTINATION |
400 | Payment not sent to school wallet |
UNSUPPORTED_ASSET |
400 | Asset not in accepted list |
NOT_FOUND |
404 | Resource not found |
DUPLICATE_TX |
409 | Transaction already processed |
STELLAR_NETWORK_ERROR |
502 | Horizon API unreachable |
INTERNAL_ERROR |
500 | Unexpected server error |