This document describes every backend API endpoint, along with authentication requirements, request/response schemas, example calls, error codes, and rate limiting behavior.
- Base URL prefix:
/api/v1 - Version header:
Accept-Version: 1(optional) - Machine-readable spec:
backend/docs/openapi.json - Public read-only API spec:
backend/docs/public-api.openapi.yaml - Generated from NestJS DTOs defined under
backend/src/**/*.dto.ts
Keep this reference in sync by regenerating the OpenAPI spec after changing DTOs or controllers:
cd backend npm install npm run export:openapi
CarbonLedger uses JWT authentication with Stellar keypair challenge/response. The flow is:
GET /api/v1/auth/challenge?publicKey=<stellar_public_key>- Sign the returned challenge nonce with the Stellar private key
POST /api/v1/auth/verifywith the signed payload- Receive
access_tokenandrefresh_token - Use
Authorization: Bearer <access_token>for protected endpoints - Renew tokens with
POST /api/v1/auth/refresh
- Auth: public
- Rate limit: 10 requests / 60 seconds per IP
- Query parameters:
publicKey(string, required)
Response schema:
{
"nonce": "carbonledger:abc123-def456",
"expiresAt": "2026-06-01T12:34:56.000Z"
}Errors:
400 Bad Request— invalid or missingpublicKey429 Too Many Requests— rate limit exceeded
- Auth: public
- Rate limit: 5 requests / 60 seconds per IP
- Request body schema:
{
"publicKey": "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"signature": "MEUCIQD...",
"nonce": "carbonledger:abc123-def456",
"role": "project_developer"
}- Response schema:
{
"access_token": "eyJhbGci...",
"refresh_token": "eyJhbGci..."
}Errors:
400 Bad Request— malformed request or invalid signature401 Unauthorized— challenge expired, signature invalid, or pubkey mismatch429 Too Many Requests— rate limit exceeded
- Auth: public
- Rate limit: 10 requests / 60 seconds per IP
- Request body schema:
{
"refreshToken": "eyJhbGci..."
}- Response schema:
{
"access_token": "eyJhbGci...",
"refresh_token": "eyJhbGci..."
}Errors:
400 Bad Request— malformed request401 Unauthorized— invalid or expired refresh token429 Too Many Requests— rate limit exceeded
access_tokenexpires in15mby default (JWT_EXPIRY)refresh_tokenexpires in7dby default (JWT_REFRESH_EXPIRY)- Token claims include
sub(publicKey),role, andtype Authorizationheader format:Bearer <access_token>
| Endpoint groups | Rate limit |
|---|---|
| Auth challenge | 10 req / 60s / IP |
| Auth verify | 5 req / 60s / IP |
| Auth refresh | 10 req / 60s / IP |
| Public project / marketplace / stats | 100 req / 60s / IP |
| Public API key endpoints | 1000 req / 24h per API key |
| Retire credits | 10 req / 60s per user |
| Default authenticated endpoints | 60 req / 60s |
| Default unauthenticated endpoints | 60 req / 60s |
Responses exceeding throttling limits return
429 Too Many Requests.
- Auth: none
- Response schema:
{
"status": "ok",
"stellar_network": "testnet",
"timestamp": "2026-06-01T12:34:56.000Z"
}- Auth: none
- Response schema: database pool metrics object
-
Auth: none
-
Query params:
methodology(string)country(string)vintage(string)cursor(string)limit(string)
-
Response: paginated project list
-
Auth: none
-
Request query schema derived from
SearchProjectsDto -
Query parameters include:
search(string)methodology(string[])country(string[])status(Pending|Verified|Rejected|Suspended|Completed|Certified)vintageYear(number[])oracleFreshness(fresh|stale|unknown)cursor(string)limit(number, 1-100)sortBy(createdAt|vintageYear|totalCreditsIssued|name)sortOrder(asc|desc)
-
Response schema: paginated list of carbon projects with metadata
- Auth: none
- Path params:
id(string)
- Response schema: project details object
- Errors:
404 Not Foundif missing
- Auth: Bearer JWT with role
project_developeroradmin - Request body schema (
RegisterProjectDto):
{
"projectId": "proj-001",
"name": "Amazon Reforestation",
"description": "Project description",
"methodology": "VCS",
"country": "BR",
"projectType": "forestry",
"metadataCid": "Qm...",
"verifierAddress": "G...",
"ownerAddress": "G...",
"vintageYear": 2024,
"methodologyScore": 85
}- Response schema: newly created project object
- Errors:
400 Bad Request,401 Unauthorized,403 Forbidden
- Auth: Bearer JWT with role
admin - Request body schema (
UpdateProjectStatusDto):
{
"status": "Verified",
"reason": "Manual review completed"
}- Response schema: updated project object
- Auth: Bearer JWT with role
verifieroradmin - Request body schema:
{
"verifierPublicKey": "G..."
}- Response: verification result
- Auth: Bearer JWT with role
verifieroradmin - Request body schema:
{
"verifierPublicKey": "G...",
"reason": "Documentation incomplete"
}- Response: rejection result
- Auth: none
- Response schema: credit batch details
- Errors:
404 Not Found
- Auth: none
- Response schema: credit retirement details
- Errors:
404 Not Found
- Auth: none
- Response schema: credit lookup result
- Errors:
404 Not Found
- Auth: Bearer JWT with role
admin - Request body schema (
MintCreditsDto):
{
"batchId": "batch-001",
"projectId": "proj-001",
"vintageYear": 2024,
"amount": 1000.00,
"serialStart": "1000001",
"serialEnd": "1001500",
"metadataCid": "Qm..."
}- Response schema: minted credits object
- Auth: Bearer JWT with role
corporationoradmin - Request body schema (
RetireCreditsDto):
{
"batchId": "batch-001",
"amount": 10.5,
"beneficiary": "Acme Corp",
"retirementReason": "2026 ESG offset",
"holderPublicKey": "G..."
}- Note:
holderPublicKeyis overridden by authenticated user public key. - Rate limit: 10 requests / 60 seconds per user
- Response schema: retirement confirmation object
- Auth: none
- Rate limit: 100 requests / 60 seconds per IP
- Query params:
methodology,country,vintage,minPrice,maxPrice,search,cursor,limit
- Response schema: paginated marketplace listing list
- Auth: none
- Rate limit: 100 requests / 60 seconds per IP
- Response schema: listing details
- Errors:
404 Not Found
- Auth: Bearer JWT with role
project_developer,corporation, oradmin - Request body schema (
CreateListingDto):
{
"listingId": "list-001",
"projectId": "proj-001",
"credit_batch_id": "batch-001",
"amount": 100,
"price_per_tonne": "25.00",
"vintageYear": 2024,
"methodology": "VCS",
"country": "BR"
}- Response schema: created listing object
- Auth: Bearer JWT with role
project_developer,corporation, oradmin - Response schema: deletion confirmation
- Errors:
403 Forbiddenif caller does not own the listing
- Auth: Bearer JWT with role
corporationoradmin - Request body schema (
PurchaseDto):
{
"listingId": "list-001",
"amount": 10
}- Note: buyerPublicKey is taken from the authenticated JWT.
- Response schema: purchase result
- Auth: Bearer JWT with role
corporationoradmin - Request body schema (
BulkPurchaseDto):
{
"listingIds": ["list-001", "list-002"],
"amounts": [5, 10]
}- Note: buyerPublicKey is taken from the authenticated JWT.
- Response schema: bulk purchase result
- Auth: none
- Response schema:
{
"projectId": "proj-001",
"lastSubmittedAt": "2026-05-31T12:00:00.000Z",
"isCurrent": true,
"latestScore": 92
}- Auth: Oracle keypair signature via
OracleGuard - Request body schema (
SubmitMonitoringDto):
{
"projectId": "proj-001",
"period": "2026-Q1",
"tonnesVerified": 125,
"methodologyScore": 94,
"satelliteCid": "Qm...",
"submittedBy": "oracle-keypair-public"
}- Response schema: monitoring record object
- Auth: Oracle keypair signature via
OracleGuard - Request body schema (
UpdatePriceDto):
{
"methodology": "VCS",
"vintageYear": 2024,
"priceUsdc": "15.00"
}- Response schema:
{
"received": true,
"oracleUpdateId": "price:VCS:2024"
}- Auth: Oracle keypair signature via
OracleGuard - Request body schema (
FlagProjectDto):
{
"projectId": "proj-001",
"reason": "Credible monitoring anomaly detected"
}- Response schema:
{
"flagged": true,
"projectId": "proj-001",
"reason": "Credible monitoring anomaly detected"
}- Auth: Bearer JWT with role
admin - Request body schema (
HoldPriceUpdateDto):
{
"methodology": "VCS",
"vintageYear": 2024,
"priceStroops": "100"
}- Response schema: pending price approval object
- Auth: Bearer JWT with role
admin - Response schema: list of pending price approvals
- Auth: Bearer JWT with role
admin - Response schema: updated approval object
- Auth: Bearer JWT with role
admin - Request body:
{"reason": "Incorrect vintage year"}- Response schema: rejected approval object
- Auth: Bearer JWT required
- Query params:
cursor,limit - Response schema: paginated retirements list scoped to requesting user
- Auth: Bearer JWT required
- Response schema: retirement detail
- Errors:
403 Forbiddenif caller is not the owner or admin
- Auth: Bearer JWT with role
corporationoradmin - Request body:
{
"retirementId": "ret-001"
}- Response: PDF generation result
- Auth: Bearer JWT with role
corporationoradmin - Query params: export filter fields plus authenticated
retiredBy - Response: CSV file download
- Auth: Bearer JWT with role
corporationoradmin - Query params: export filter fields plus authenticated
retiredBy - Response: PDF file download
- Auth: none
- Request body schema:
{
"retirementId": "ret-001",
"content": "..."
}- Response schema: integrity verification result
- Auth: none
- Response schema: retirement certificate metadata and project reference
- Auth: Bearer JWT with role
project_developeroradmin - Content-Type:
multipart/form-data - Request payload: file field named
file - Supported file types:
application/pdf,application/json - Max size: 50 MB
- Response schema: uploaded file metadata and IPFS gateway URL
- Auth: Bearer JWT with role
corporationoradmin - Content-Type:
multipart/form-data - Supported file type:
application/pdf - Max size: 50 MB
- Response schema: uploaded certificate metadata
- Auth: public
- Request body: arbitrary webhook payload from Pinata
- Response schema:
{
"success": true,
"message": "Webhook processed"
}- Auth: Bearer JWT with role
admin - Query params:
pinStatus,linkedEntityType,linkedEntityId - Response schema: file listing
- Auth: none
- Response schema: file metadata
- Errors:
404 Not Found
- Auth: public
- Request body schema (
ApplyVerifierDto):
{
"publicKey": "G...",
"organizationName": "Verifier Inc",
"accreditationBody": "SDS",
"accreditationId": "ACC-123",
"contactEmail": "contact@example.com",
"documentsCid": "Qm..."
}- Response schema: verifier application confirmation
- Auth: Bearer JWT with role
adminorverifier - Query param:
status - Response schema: list of verifier applications
- Auth: Bearer JWT with role
adminorverifier - Response schema: verifier application details
- Auth: Bearer JWT with role
admin - Request body schema (
ReviewVerifierDto):
{
"adminPublicKey": "G...",
"decision": "approved",
"rejectionReason": "optional reason"
}- Response schema: review result
- Auth: Bearer JWT with role
verifieroradmin - Response schema: pending project list for the verifier
- Auth: Bearer JWT required
- Response schema:
{
"projectApproved": true,
"creditsMinted": false,
"purchaseConfirmed": true,
"retirementConfirmed": true
}- Auth: Bearer JWT required
- Request body schema (
UpdateNotificationPreferencesDto):
{
"projectApproved": true,
"creditsMinted": false
}- Response schema: updated preferences object
All /api/v1/admin/* endpoints require Authorization: Bearer <JWT> with role admin.
- Response schema: list of whitelisted verifier addresses
- Request body schema (
VerifierWhitelistDto):
{
"address": "G..."
}- Response schema: deletion confirmation
- Response schema: treasury address and balance metadata
- Request body schema (
UpdateTreasuryDto):
{
"address": "G..."
}- Response schema: oracle health status
- Response schema: reindex trigger confirmation
- Query params:
limit,offset,action - Response schema: audit log list
- Auth: Bearer JWT with role
admin - Query params: filters and
format=json|csv - Response: JSON array or CSV download
- Auth: Bearer JWT with role
admin - Query params: filters and
format=json|csv - Response: JSON array or CSV download
- Auth: Bearer JWT with role
admin - Request body schema (
EnqueueJobDto):
{
"type": "CERTIFICATE_GENERATION",
"payload": { "projectId": "proj-001", "amount": 100 }
}- Response schema: job enqueue confirmation
- Auth: Bearer JWT with role
admin - Response schema: job status and result
- Auth: Bearer JWT with role
admin - Response schema: queue statistics
- Auth: Bearer JWT with role
admin - Query params:
limit,offset,userId,action - Response schema: audit log list
- Auth: none
- Rate limit: 100 requests / 60 seconds per IP
- Response schema: platform statistics
- Auth: none
- Rate limit: 100 requests / 60 seconds per IP
- Response schema: aggregated platform metrics
- Auth: none
- Response schema: cache metrics
- Auth: none
- Request body schema:
{
"level": "error",
"message": "Something failed",
"trace_id": "abc123",
"user_id": "G...",
"url": "https://app.example.com/page"
}- Response:
204 No Content
- Auth: none
- Response schema: dashboard metrics object
The public, read-only API uses X-Api-Key authentication and lives under /v1/*.
Reference the generated spec at backend/docs/public-api.openapi.yaml.
Most errors return JSON with statusCode, message, and optionally error.
Common HTTP statuses:
400 Bad Request— invalid request shape, missing fields, or validation failure401 Unauthorized— missing or invalid JWT / API key403 Forbidden— role does not permit this action404 Not Found— resource not found409 Conflict— duplicate or invalid business state429 Too Many Requests— rate limit exceeded500 Internal Server Error— server-side failure
All request schema definitions are derived from DTO classes in backend/src/**/.dto.ts.
When you change a DTO, regenerate the OpenAPI spec:
cd backend
npm run export:openapiThen review backend/docs/openapi.json and update this Markdown reference as needed.