Skip to main content

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 require scope: 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&currency=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:

HTTPerror.codeWhen
400VALIDATION_ERRORDTO violates schema
401UNAUTHENTICATEDMissing/invalid JWT
403FORBIDDENScope missing
404NOT_FOUNDAggregate doesn't exist in tenant scope
409CONFLICTOptimistic lock / state machine violation
409COUPON_EXHAUSTEDUsage cap reached
409LISTING_NOT_APPROVABLEPrereqs (KYC, published, playable) not met
422LICENSE_NO_SEATSremainingSeats === 0
422REFUND_WINDOW_EXPIREDnow > refundDeadline
429RATE_LIMITED
500INTERNAL_ERRORUnhandled
502UPSTREAM_ERRORBilling/catalog unreachable
504UPSTREAM_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

RouteLimitScope
POST /orders10/minper user
POST /listings/*/submit5/minper tenant
GET /public/listings100/minper IP
POST /coupons/validate60/minper user
POST /admin/*30/minper 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.