API Contracts
:::info Source
Sourced from services/marketplace-service/API_CONTRACTS.md in the documentation repo.
:::
Companion: 05 API Design · APPLICATION_LOGIC
1. Conventions
- Base path:
/api/v1 - All responses use the standard envelope:
{ "success": true, "data": { ... }, "error": null, "meta": { "requestId": "req_...", "tenantId": "ten_..." } }
- Auth:
Authorization: Bearer <JWT>; platform admin routes requirescope: marketplace:admin. - Idempotency:
Idempotency-Key: <uuid>required on mutating endpoints. - Pagination:
?page=<n>&limit=<m>with cursor alternative?cursor=<opaque>; default 20, max 100. - Content-Type:
application/json; charset=utf-8. - Rate limits: see §9.
2. Listings API
2.1 POST /api/v1/listings
Create a listing draft.
Request:
{
"courseId": "crs_01J...",
"courseVersionId": "crv_01J...",
"visibility": "public",
"marketing": { "tagline": "...", "description": "...", "hero": "https://...", "screenshots": [] },
"refundPolicy": { "refundDays": 14 },
"pricingPlans": [
{ "kind": "one_time", "currency": "USD", "price": { "amount": 4900, "currency": "USD" }, "perpetualOfflineAccess": true }
]
}
Response 201:
{ "success": true, "data": { "id": "lst_01J...", "state": "draft", "version": 1, "createdAt": "..." } }
Authorization: tid owns the course (catalog-service cross-check) and has provider scope.
2.2 GET /api/v1/listings
Lists current tenant's listings. Filters: state, q, courseId.
2.3 GET /api/v1/listings/{id}
Returns full listing incl. plans and marketing.
2.4 PATCH /api/v1/listings/{id}
Edit draft or submitted listings. Rejects if state is past submitted unless admin.
Request body matches POST shape; requires If-Match: W/"<version>" for optimistic concurrency.
2.5 POST /api/v1/listings/{id}/submit
Submit for approval. Validates invariants L-INV-1 through L-INV-6.
Response 202:
{ "success": true, "data": { "id": "lst_...", "state": "submitted", "submittedAt": "..." } }
2.6 POST /api/v1/listings/{id}/approve
Admin only. Scope marketplace:admin.
2.7 POST /api/v1/listings/{id}/reject
Admin only. Body: { "reason": "..." }.
2.8 POST /api/v1/listings/{id}/suspend
Admin only. Body: { "reason": "..." }.
2.9 POST /api/v1/listings/{id}/reinstate
Admin only.
2.10 POST /api/v1/listings/{id}/retire
Provider or admin. Checks L-INV-7.
2.11 GET /api/v1/public/listings
Unauthenticated public browse. Filters: q, categoryId, priceMin, priceMax, currency, language.
Returns only state='live' and visibility='public'. Pricing is per-currency; server may perform FX conversion if ?currency= supplied.
3. Orders API
3.1 POST /api/v1/orders
Place an order. Initiates the purchase saga.
Request:
{
"currency": "USD",
"lines": [
{ "listingId": "lst_01J...", "pricingPlanId": "pln_01J...", "quantity": 1 }
],
"couponCodes": ["LAUNCH25"],
"billingDetails": { "name": "...", "email": "...", "address": { ... } }
}
Headers: Idempotency-Key: <uuid>.
Response 201:
{
"success": true,
"data": {
"id": "ord_01J...",
"status": "pending_payment",
"totals": { "amount": 4900, "currency": "USD" },
"paymentIntentClientSecret": "pi_..._secret_...",
"sagaId": "sga_01J..."
}
}
3.2 GET /api/v1/orders/{id}
Buyer or admin. Returns full order incl. lines and status.
3.3 GET /api/v1/orders
Lists current buyer-user's orders (or all tenant orders if ?scope=tenant and caller has tenant:admin).
3.4 POST /api/v1/orders/{id}/refund
Request a refund. Enforces now <= refundDeadline.
Request:
{ "reason": "duplicate_purchase", "note": "customer contacted support" }
Response 202:
{ "success": true, "data": { "id": "ord_01J...", "status": "refunded", "refundedAt": "..." } }
Billing async-reconciles the money movement; a final billing.payment.refunded.v1 event will confirm.
4. Licenses API
4.1 GET /api/v1/licenses
Lists licenses owned by current tenant.
Query: state, courseId, listingId, q.
4.2 GET /api/v1/licenses/{id}
Full license incl. seat allocations.
4.3 POST /api/v1/licenses/{id}/seats
Assign a seat to a user (org admin only).
Request: { "userId": "usr_01J..." }
Response 201: { "success": true, "data": { "allocationId": "...", "status": "active" } }
4.4 DELETE /api/v1/licenses/{id}/seats/{allocationId}
Release a seat. Enrollment is archived, not deleted.
4.5 POST /api/v1/licenses/{id}/revoke
Admin only. Revokes license; releases all active seats.
5. Coupons API
5.1 POST /api/v1/coupons/validate
Non-mutating validation used by checkout UI.
Request: { "code": "LAUNCH25", "currency": "USD", "listingIds": ["lst_..."] }
Response 200:
{ "success": true, "data": { "valid": true, "discount": { "kind": "percent", "value": 25 }, "appliesTo": ["lst_..."] } }
5.2 POST /api/v1/coupons
Provider or admin. Create coupon.
5.3 GET /api/v1/coupons / PATCH /api/v1/coupons/{id} / DELETE /api/v1/coupons/{id}
6. Provider Earnings & Payouts
6.1 GET /api/v1/provider/earnings
Scope: provider:read. Returns earnings summary per period.
Query: from=YYYY-MM&to=YYYY-MM¤cy=USD.
Response:
{
"success": true,
"data": {
"periods": [
{
"periodMonth": "2026-03",
"currency": "USD",
"grossRevenue": { "amount": 1200000, "currency": "USD" },
"platformFee": { "amount": 180000, "currency": "USD" },
"refunds": { "amount": 5000, "currency": "USD" },
"netPayable": { "amount": 1015000, "currency": "USD" },
"state": "ready"
}
]
}
}
6.2 GET /api/v1/provider/payouts
Historical payouts; forwards to billing for per-payout details.
7. Admin / Internal
7.1 GET /api/v1/admin/sagas/{id}
Saga state + history (admin only; used by support).
7.2 POST /api/v1/admin/sagas/{id}/resume
Manually resume a stuck saga (admin, audit-logged).
7.3 POST /api/v1/admin/orders/{id}/force-fulfill
Break-glass fulfillment. Audit-logged; alerts on use.
8. Error Model
Errors use the envelope with success=false, error={...}. Standard codes:
| HTTP | error.code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | DTO violates schema |
| 401 | UNAUTHENTICATED | Missing/invalid JWT |
| 403 | FORBIDDEN | Scope missing |
| 404 | NOT_FOUND | Aggregate doesn't exist in tenant scope |
| 409 | CONFLICT | Optimistic lock / state machine violation |
| 409 | COUPON_EXHAUSTED | Usage cap reached |
| 409 | LISTING_NOT_APPROVABLE | Prereqs (KYC, published, playable) not met |
| 422 | LICENSE_NO_SEATS | remainingSeats === 0 |
| 422 | REFUND_WINDOW_EXPIRED | now > refundDeadline |
| 429 | RATE_LIMITED | |
| 500 | INTERNAL_ERROR | Unhandled |
| 502 | UPSTREAM_ERROR | Billing/catalog unreachable |
| 504 | UPSTREAM_TIMEOUT |
Response body:
{
"success": false,
"data": null,
"error": {
"code": "REFUND_WINDOW_EXPIRED",
"message": "Refund window expired on 2026-04-01T00:00:00Z",
"details": { "refundDeadline": "2026-04-01T00:00:00Z" }
},
"meta": { "requestId": "req_..." }
}
9. Rate Limits
| Route | Limit | Scope |
|---|---|---|
POST /orders | 10/min | per user |
POST /listings/*/submit | 5/min | per tenant |
GET /public/listings | 100/min | per IP |
POST /coupons/validate | 60/min | per user |
POST /admin/* | 30/min | per admin user |
Excess returns 429 with Retry-After.
10. OpenAPI
Full OpenAPI 3.1 spec at /api/v1/openapi.json (public, unauthenticated). Types generated into @ghasi/marketplace-client SDK on build.