{"openapi":"3.0.3","info":{"title":"Affiliatops API","version":"1.3.0","description":"Affiliate and referral tracking for e-commerce, SaaS and service businesses.\n\nThe API is organized around five surfaces:\n\n- **Tracking** (`/api/track/*`) — called from your website or app with a store **public key**. Records clicks, conversions, sign-ups and analytics events.\n- **SaaS** (`/api/saas/events`, Stripe webhook) — server-to-server subscription and billing events that drive recurring partner commissions.\n- **Webhooks** (`/api/webhook/*`) — server-to-server receivers for conversions and refunds, authenticated with an **API key** or an **HMAC signature**.\n- **Affiliate** (`/api/affiliate/*`) — the partner portal, authenticated with a **session cookie**.\n- **Admin** (`/api/admin/*`) — the merchant dashboard, authenticated with a **session cookie**. All data is automatically scoped to the caller’s merchant.\n\n### Conventions\n- Money is stored and returned in **integer minor units** (`amountCents`, `amount_cents`): `150000` = Rp 1.500 / $1,500.00. Some inputs also accept `amount` in major units — each field says which.\n- The platform is multi-tenant: admin endpoints only ever read or write rows belonging to the authenticated merchant.\n- Timestamps are ISO-8601 strings (UTC).\n- Errors return `{ \"error\": string }` or `{ \"success\": false, \"message\": string }` with a 4xx/5xx status.\n- Default market is Indonesia (`ID` / `IDR`).\n\n### For AI agents\n- `/docs/agent.md` is this reference as one Markdown file, with integration recipes. `/api/docs` is the raw OpenAPI JSON.","contact":{"email":"hello@affiliatops.com"},"license":{"name":"MIT","url":"https://opensource.org/licenses/MIT"}},"servers":[{"url":"https://affiliatops.com","description":"This deployment"}],"tags":[{"name":"Auth","description":"Login, registration, OTP and session handling."},{"name":"Tracking","description":"Public storefront tracking — clicks, conversions and analytics events. Authenticated with a store API key (X-API-Key)."},{"name":"Webhooks","description":"Server-to-server event receivers. Authenticated with an API key or an HMAC signature."},{"name":"SaaS","description":"Subscription lifecycle and billing events for SaaS workspaces: sign-ups, trials, subscriptions, payments and refunds. Drives recurring partner commissions."},{"name":"Affiliate","description":"Partner portal endpoints, including the SaaS partner dashboard. Authenticated with the session cookie."},{"name":"SaaS (Admin)","description":"SaaS workspace: KPIs and metrics, referred customers and subscriptions, plans, the recurring commission plan, integrations and the billing-event ledger."},{"name":"Affiliates (Admin)","description":"Manage partner accounts and their status."},{"name":"Referrals (Admin)","description":"Review, approve and reject referral leads."},{"name":"Commissions (Admin)","description":"Commission ledger, maturation and commission rules."},{"name":"Payouts (Admin)","description":"Create, complete and automate affiliate payouts; invoices, transactions and refunds."},{"name":"Programs (Admin)","description":"Programs, program settings, partner groups and coupons."},{"name":"Analytics (Admin)","description":"Dashboards, detailed analytics, live sessions and reports."},{"name":"Integrations (Admin)","description":"Store integrations, API keys, usage and outbound webhooks."},{"name":"Team (Admin)","description":"Merchant profile, workspace mode and onboarding, team members and merchant switching."},{"name":"System","description":"Health and status probes for uptime monitors and the deploy platform. Public."},{"name":"Billing (Admin)","description":"Your Affiliatops plan, billed through Stripe: checkout with the free trial, plan changes, cancellation, the Stripe customer portal, billing history, and the billing webhook. Needs STRIPE_SECRET_KEY (and STRIPE_WEBHOOK_SECRET for the webhook)."}],"paths":{"/api/auth/register":{"post":{"tags":["Auth"],"operationId":"register","summary":"Register a partner account or request a business workspace","description":"Two kinds of self sign-up:\n- **Partner** (default): creates an AFFILIATE account. The role is always forced to AFFILIATE server-side. With `program` (a business’s slug, from its join link `/join/<slug>`), the partner joins that business’s program in `PENDING` status until the business approves them; the response has `approvalRequired: true` and the owner is emailed. Without it, the partner has no program until they open a join link while logged in.\n- **Business** (`accountType: \"business\"`): creates an ADMIN account in `PENDING` status that owns a new merchant with the chosen `businessType`. Pending accounts can log in but every admin API rejects them until a platform operator approves the workspace. The response has `reviewRequired: true`.\n\nRate limited to 3 requests/minute per IP.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterRequest"},"example":{"email":"partner@example.com","name":"Jane Partner","password":"sup3r-secret"}}}},"responses":{"200":{"description":"Account created (business sign-ups also return `merchant` and `reviewRequired: true`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthUserResponse"}}}},"400":{"description":"Validation error (e.g. missing company or invalid business type) or email already in use","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too many registration attempts"}}}},"/api/auth/login":{"post":{"tags":["Auth"],"operationId":"login","summary":"Log in with email & password","description":"Verifies credentials and, on success, sets an httpOnly `auth-token` cookie (JWT, 24h). PENDING accounts may log in; INACTIVE / SUSPENDED accounts are rejected with a generic message to prevent account enumeration. Rate limited to 5 requests/minute per IP.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"},"example":{"email":"partner@example.com","password":"sup3r-secret"}}}},"responses":{"200":{"description":"Authenticated. Sets the `auth-token` session cookie.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthUserResponse"}}}},"400":{"description":"Missing email or password"},"401":{"description":"Invalid email or password"},"403":{"description":"Account not allowed to log in"},"429":{"description":"Too many login attempts"}}},"delete":{"tags":["Auth"],"operationId":"loginDelete","summary":"Log out (alias)","description":"Returns a success body. Prefer `POST /api/auth/logout`, which also clears the session cookie.","responses":{"200":{"description":"Logged out"}}}},"/api/auth/logout":{"post":{"tags":["Auth"],"operationId":"logout","summary":"Log out","description":"Clears the `auth-token` session cookie.","responses":{"200":{"description":"Session cookie cleared"}}}},"/api/auth/me":{"get":{"tags":["Auth"],"operationId":"getCurrentUser","summary":"Get the current authenticated user","description":"Reads and verifies the `auth-token` cookie and returns the current user with a `hasAffiliate` flag. The password is never returned.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Current user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeResponse"}}}},"401":{"description":"Missing, invalid or expired session"}}}},"/api/auth/send-otp":{"post":{"tags":["Auth"],"operationId":"sendOtp","summary":"Send a one-time login code","description":"Emails a 6-digit OTP valid for 10 minutes. Limited to one code per minute per account and 3 requests/minute per IP.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"}}},"example":{"email":"partner@example.com"}}}},"responses":{"200":{"description":"OTP sent"},"400":{"description":"Invalid email or account not eligible"},"429":{"description":"Rate limited"}}}},"/api/auth/verify-otp":{"post":{"tags":["Auth"],"operationId":"verifyOtp","summary":"Verify a one-time login code","description":"Exchanges a valid OTP for a session. Sets the `auth-token` cookie. Allows 3 attempts per code; rate limited to 5 requests/minute per IP.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","code"],"properties":{"email":{"type":"string","format":"email"},"code":{"type":"string","example":"123456"}}},"example":{"email":"partner@example.com","code":"123456"}}}},"responses":{"200":{"description":"Verified; session cookie set"},"400":{"description":"Invalid or expired code"},"429":{"description":"Rate limited"}}}},"/api/auth/forgot-password":{"post":{"tags":["Auth"],"operationId":"forgotPassword","summary":"Request a password reset email","description":"Always responds 200 to avoid leaking which emails are registered.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"}}}}}},"responses":{"200":{"description":"If the account exists, a reset email is sent"}}}},"/api/auth/reset-password":{"post":{"tags":["Auth"],"operationId":"resetPassword","summary":"Reset a password with a token","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token","password"],"properties":{"token":{"type":"string"},"password":{"type":"string","minLength":8}}}}}},"responses":{"200":{"description":"Password updated"},"400":{"description":"Invalid or expired token"}}}},"/api/health":{"get":{"tags":["System"],"operationId":"health","summary":"Health check","description":"Public and never cached. Runs a `SELECT 1` against the database with a 2-second timeout. Always 200 while the process is up; `db` is `error` when the database failed or timed out. `commit` is `SOURCE_COMMIT`, else `GIT_SHA`, else null.","security":[],"responses":{"200":{"description":"Process is up","content":{"application/json":{"schema":{"type":"object","required":["status","startedAt","commit","db"],"properties":{"status":{"type":"string","enum":["ok"]},"startedAt":{"type":"string","format":"date-time","description":"When the server process started."},"commit":{"type":"string","nullable":true,"description":"Deployed commit SHA, if the platform provides one."},"db":{"type":"string","enum":["ok","error"]}}},"example":{"status":"ok","startedAt":"2026-09-24T09:00:00.000Z","commit":"3657cbf","db":"ok"}}}}}}},"/r/{code}":{"get":{"tags":["Tracking"],"operationId":"referralRedirect","summary":"Referral short link redirect","description":"Public referral link. Records attribution and 302-redirects the visitor to the destination, appending `ref` and `attr` query params and setting a first-party attribution cookie. Supports deep links via `dest`/`target`.","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string"},"description":"The affiliate referral code."},{"name":"dest","in":"query","schema":{"type":"string","format":"uri"},"description":"Destination URL to deep-link to (alias: `target`)."}],"responses":{"302":{"description":"Redirect to the destination with attribution attached"}}}},"/api/track/referral":{"post":{"tags":["Tracking"],"operationId":"trackReferral","summary":"Track a referral click / page view","description":"Records a referral click from your storefront. Authenticate with the store **public API key** in the `X-API-Key` header (`pk_store_…`). The referral code is resolved within the key’s merchant; inactive affiliates are rejected.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrackReferralRequest"},"example":{"referralCode":"JANE-4F2A","url":"https://shop.example.com/product/42","referrer":"https://instagram.com/"}}}},"responses":{"200":{"description":"Click tracked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrackReferralResponse"}}}},"400":{"description":"Missing referral code"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Affiliate is not active"},"404":{"description":"Unknown referral code"}}}},"/api/track/conversion":{"post":{"tags":["Tracking"],"operationId":"trackConversion","summary":"Track a conversion / sale","description":"Records a sale attributed to a referral code or a code the buyer typed. Send `referralCode` (from the tracker cookie), `coupon_code` (alias `code`), or both. A code the buyer typed at checkout (`coupon_code`, alias `code`) resolves, within your business only, to the partner who owns an active partner coupon with that code, else to the partner whose referral code it is (trimmed, case-insensitive). When it resolves it wins over the cookie/referral code and the conversion records `attributionMethod: \"coupon\"`; when it does not, the referral code is used. Send `amount` in **major units** (e.g. `99.99`) — it is converted to `amountCents` server-side with `Math.round(amount * 100)`. Send `orderId`: a retry with the same `orderId` returns the conversion already recorded (`duplicate: true`) instead of counting the sale twice. Authenticate with `X-API-Key`.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrackConversionRequest"},"example":{"referralCode":"JANE-4F2A","customerEmail":"buyer@example.com","customerName":"A. Buyer","amount":99.99,"currency":"IDR","orderId":"ORD-1024"}}}},"responses":{"200":{"description":"Conversion tracked, or `duplicate: true` when this `orderId` was already recorded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrackConversionResponse"}}}},"400":{"description":"Missing referral code and coupon code"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Affiliate is not active"},"404":{"description":"Unknown referral code, or a coupon code that matches no active partner"}}}},"/api/track/events":{"post":{"tags":["Tracking"],"operationId":"trackEvents","summary":"Ingest a batch of analytics events","description":"Accepts up to **50** behavioral events per request (page views, product views, add-to-cart, purchase, etc.). Drives the live view and analytics dashboards. Raw emails inside `metadata` are redacted server-side. Authenticate with `X-API-Key`.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsBatchRequest"},"example":{"events":[{"visitorId":"v_8f3a","sessionId":"s_19c2","eventName":"page_view","pageUrl":"https://shop.example.com/","occurredAt":"2026-06-01T10:00:00.000Z","referralCode":"JANE-4F2A"},{"visitorId":"v_8f3a","sessionId":"s_19c2","eventName":"purchase","orderId":"ORD-1024","amountCents":250000,"currency":"IDR"}]}}}},"responses":{"200":{"description":"Events accepted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"accepted":{"type":"integer","description":"Number of events persisted"}}}}}},"400":{"description":"Validation error (batch too large, missing ids, metadata > 8KB, …)"},"401":{"description":"Missing or invalid API key"}}}},"/api/track/signup":{"post":{"tags":["Tracking","SaaS"],"operationId":"trackSignup","summary":"Attribute a sign-up to the referring partner (browser)","description":"Called by `Affiliatops.trackSignup()` on your sign-up page. Links the new account (`customerId` and/or `email`) to the partner in the referral cookie. Authenticated with the store **public** key, so it only records attribution — sign-up bounties and recurring commissions are created from server-verified events (`/api/saas/events` or Stripe). Repeated calls for the same account are idempotent.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrackSignupRequest"},"example":{"referralCode":"JANE-4F2A","customerId":"acct_1042","email":"owner@example.com","name":"Owner Name"}}}},"responses":{"200":{"description":"Processed. `attributed: false` when the referral code did not resolve to an active partner.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"status":{"type":"string","enum":["processed","duplicate","ignored"]},"attributed":{"type":"boolean"}}}}}},"400":{"description":"Missing referral code, or neither customerId nor email"},"401":{"description":"Missing or invalid API key"}}}},"/api/saas/events":{"post":{"tags":["SaaS"],"operationId":"ingestSaasEvents","summary":"Send SaaS lifecycle & billing events (server-to-server)","description":"Send one event, or `{ \"events\": [...] }` with up to **100** events processed in order. Event `type`: `signup`, `trial_started`, `subscription_created`, `subscription_updated`, `subscription_canceled`, `payment_succeeded`, `payment_failed`, `refund`. Amounts are **major units** in `amount` (converted with `Math.round(amount * 100)`) or integer `amount_cents`. Pass a stable `id` per event: replays with the same id are no-ops (`status: duplicate`). Customers are matched by `customer.id`, then `customer.billing_id`, then `customer.email`; the first code that resolves to an active partner owns the customer: `coupon_code` (alias `code`, the partner coupon or referral code the customer typed — it wins over `referral_code`), then `referral_code`. Codes are trimmed, case-insensitive and scoped to your business; a later code never reassigns an attributed customer. Events for customers with no partner attribution are acknowledged with `status: ignored`. Authenticate with the integration **secret** key (`sk_store_…`) or a merchant API key with `write` scope — public keys are rejected.","security":[{"ServerKeyAuth":[]},{"ServerKeyHeader":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/SaasEvent"},{"type":"object","required":["events"],"properties":{"events":{"type":"array","maxItems":100,"items":{"$ref":"#/components/schemas/SaasEvent"}}}}]},"example":{"id":"inv_2026_0412","type":"payment_succeeded","customer":{"id":"acct_1042","email":"owner@example.com"},"subscription":{"id":"sub_77"},"payment":{"id":"inv_2026_0412","amount":349000,"currency":"IDR"}}}}},"responses":{"200":{"description":"All events processed","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"results":{"type":"array","items":{"$ref":"#/components/schemas/SaasEventResult"}},"result":{"$ref":"#/components/schemas/SaasEventResult"}}}}}},"400":{"description":"Validation failed — `errors: [{ index, errors: string[] }]`; nothing was processed"},"401":{"description":"Missing/invalid server key, or a public key was used"},"500":{"description":"One or more events failed (`status: error` in `results`). Retry them with the same `id`."}}}},"/api/webhook/conversion":{"post":{"tags":["Webhooks"],"operationId":"webhookConversion","summary":"Receive a conversion event (server-to-server)","description":"Server-side conversion ingestion for payment providers and backends. Authenticate with a **server secret** — the store integration secret (`sk_store_…`) or a merchant API key (`aft_…`), as `Authorization: Bearer` or `X-API-Key` — **or** an HMAC-SHA256 signature in `X-Webhook-Signature` (computed over the raw body using `WEBHOOK_SECRET`, optional `sha256=` prefix). Store public keys (`pk_…`) are rejected: they are visible in browser code. Send `external_id` (your order or invoice id): a retry with the same id returns the conversion already recorded (`duplicate: true`). A commission is created using the merchant’s default rule and held until it matures. A code the buyer typed at checkout (`coupon_code`, alias `code`) resolves, within your business only, to the partner who owns an active partner coupon with that code, else to the partner whose referral code it is (trimmed, case-insensitive). When it resolves it wins over `referral_code` and the conversion records `attributionMethod: \"coupon\"`; when it does not, `referral_code` is used. Coupon codes need a merchant-scoped secret key; with signature auth (platform-wide) they are not resolved.","security":[{"ServerKeyAuth":[]},{"ServerKeyHeader":[]},{"WebhookSignature":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversionWebhook"},"example":{"event_type":"PURCHASE","external_id":"ORD-1024","amount_cents":250000,"currency":"IDR","customer_email":"buyer@example.com","referral_code":"JANE-4F2A"}}}},"responses":{"200":{"description":"Processed. `attributed: false` when no affiliate matched; `duplicate: true` when this `external_id` was already recorded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversionWebhookResponse"}}}},"400":{"description":"Missing required fields"},"401":{"description":"Missing/invalid server key or signature, or a store public key was used"}}}},"/api/webhook/refund":{"post":{"tags":["Webhooks"],"operationId":"webhookRefund","summary":"Receive a refund / clawback event","description":"Reverses the commission for a refunded order. Send `order_id` — the `external_id` you sent to `/api/webhook/conversion`, or the tracker’s `orderId` — to reverse only that order; it is matched within your merchant, so orders tracked in the browser are found too. Without `order_id`, every conversion recorded for `customer_email` is reversed (the response then carries a `warning`). `amount_cents` makes it a partial refund when one order matched: its commission is reversed in proportion to the refunded share, and several partial refunds add up to the whole commission. Omit it to refund what is left. PENDING commissions are reduced or cancelled (no balance impact); APPROVED ones are reduced or cancelled and taken off the balance; PAID ones stay paid for partial refunds, and the reversed part becomes a negative balance for the next payout. Send `external_id` (the provider’s refund id): a redelivered refund with the same id changes nothing. Authenticate like `/api/webhook/conversion`: a server secret (`sk_store_…` or `aft_…`) or `X-Webhook-Signature`. Store public keys are rejected.","security":[{"ServerKeyAuth":[]},{"ServerKeyHeader":[]},{"WebhookSignature":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefundWebhook"},"example":{"customer_email":"buyer@example.com","order_id":"ORD-1024","amount_cents":250000,"reason":"Customer refund","external_id":"rf_123"}}}},"responses":{"200":{"description":"Refund processed: `reversed` count, `matchedBy` (`order_id` or `customer_email`), and a `warning` when no order_id was sent and several conversions matched"},"400":{"description":"`customer_email` is required"},"401":{"description":"Missing/invalid server key or signature, or a store public key was used"}}}},"/api/webhook/stripe/{integrationId}":{"post":{"tags":["Webhooks","SaaS"],"operationId":"webhookStripe","summary":"Stripe webhook endpoint (per Stripe integration)","description":"Point a Stripe webhook endpoint at this URL (shown on Admin → Integrations). Verified with the endpoint signing secret (`whsec_…`) saved on the integration via the `Stripe-Signature` header (5-minute tolerance). Handled events: `checkout.session.completed`, `customer.created`, `customer.updated`, `customer.subscription.created|updated|deleted`, `invoice.paid`, `invoice.payment_failed`, `charge.refunded`. Other events are acknowledged and ignored (`invoice.payment_succeeded` is ignored on purpose: it duplicates `invoice.paid`). The first verified event activates a draft integration.\n\nAttribution comes from Stripe metadata: set `affiliatops_ref` (the partner referral code) and `affiliatops_customer_id` (your account id) on the Checkout Session, subscription or customer.","security":[{"StripeSignature":[]}],"parameters":[{"name":"integrationId","in":"path","required":true,"schema":{"type":"string"},"description":"The Stripe integration id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"A Stripe Event object."},"example":{"id":"evt_test_invoice_paid","type":"invoice.paid","created":1767225600,"data":{"object":{"id":"in_test_0412","object":"invoice","customer":"cus_test_1042","customer_email":"owner@example.com","currency":"idr","amount_paid":34900000,"subscription":"sub_test_77","metadata":{"affiliatops_ref":"JANE-4F2A","affiliatops_customer_id":"acct_1042"}}}}}}},"responses":{"200":{"description":"`{ received: true }` — with `ignored: true` for unhandled event types, otherwise per-event `results`"},"400":{"description":"Invalid/missing signature, stale timestamp, or no signing secret configured"},"404":{"description":"Unknown or disabled Stripe integration"},"500":{"description":"Processing failed; Stripe will retry (idempotent by event id)"}}}},"/api/webhook/midtrans/{integrationId}":{"post":{"tags":["Webhooks","SaaS"],"operationId":"webhookMidtrans","summary":"Midtrans payment notification endpoint (per Midtrans integration)","description":"Set this URL as the Payment Notification URL in Midtrans (shown on Admin → Integrations). There is no header signature: the body’s `signature_key` must equal SHA512(order_id + status_code + gross_amount + ServerKey), using the server key saved on the integration. `transaction_status` `settlement`, or `capture` with `fraud_status: accept`, records a payment (externalPaymentId = `order_id`); `refund` / `partial_refund` reverses commission against that order using the cumulative `refund_amount`. `pending`, `expire`, `cancel`, `deny` and challenged captures are acknowledged and ignored. Idempotent on `order_id` + `transaction_status` (+ cumulative refund amount for refunds). `gross_amount` is a major-unit string like `349000.00`, stored as minor units ×100 (34 900 000).\n\nAttribution: `custom_field1` = partner referral code (or `metadata.affiliatops_ref`), `custom_field2` = your user id (or `metadata.affiliatops_customer_id`), `custom_field3` = customer email. Customers already attributed via trackSignup/API are matched by id or email. The first verified notification activates a draft integration.","security":[],"parameters":[{"name":"integrationId","in":"path","required":true,"schema":{"type":"string"},"description":"The Midtrans integration id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"A Midtrans HTTP notification."},"example":{"transaction_time":"2026-09-24 10:15:00","transaction_status":"settlement","transaction_id":"0f1c3a9e-7b1d-4f6a-9d7e-1a2b3c4d5e6f","status_code":"200","signature_key":"<sha512 hex>","settlement_time":"2026-09-24 10:16:02","payment_type":"bank_transfer","order_id":"INV-2026-0412","gross_amount":"349000.00","currency":"IDR","fraud_status":"accept","custom_field1":"JANE-4F2A","custom_field2":"acct_1042","custom_field3":"owner@example.com"}}}},"responses":{"200":{"description":"`{ received: true }` — with `ignored: true` for statuses that are not payments or refunds, otherwise per-event `results`"},"400":{"description":"Invalid JSON, or no server key configured"},"401":{"description":"Missing or wrong signature_key"},"404":{"description":"Unknown or disabled Midtrans integration"},"500":{"description":"Processing failed; Midtrans retries (idempotent)"}}}},"/api/webhook/xendit/{integrationId}":{"post":{"tags":["Webhooks","SaaS"],"operationId":"webhookXendit","summary":"Xendit callback endpoint (per Xendit integration)","description":"Point Xendit callbacks (Settings → Webhooks) at this URL (shown on Admin → Integrations). Verified by comparing the `x-callback-token` header with the callback verification token saved on the integration. Handled: invoice callbacks with status `PAID` or `SETTLED` (payment, externalPaymentId = invoice `id`; SETTLED after PAID is a no-op), `payment.succeeded` / `payment.capture` (Payments API, keyed by payment request id), `recurring.cycle.succeeded`, and `refund.succeeded` (reverses commission against `invoice_id` / `payment_request_id`). Everything else is acknowledged and ignored. Idempotent on the callback object’s `id`. Amounts are major units (IDR 349000) stored as minor units ×100.\n\nAttribution: `metadata.affiliatops_ref` (partner referral code) and `metadata.affiliatops_customer_id` (your user id) on the invoice / payment request / recurring plan; `payer_email` also matches customers already attributed via trackSignup/API. The first verified callback activates a draft integration.","security":[],"parameters":[{"name":"integrationId","in":"path","required":true,"schema":{"type":"string"},"description":"The Xendit integration id."},{"name":"x-callback-token","in":"header","required":true,"schema":{"type":"string"},"description":"Xendit webhook verification token."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"A Xendit invoice callback, or a `{ event, data }` callback."},"example":{"id":"65f1a2b3c4d5e6f7a8b9c0d1","external_id":"INV-2026-0412","status":"PAID","amount":349000,"paid_amount":349000,"currency":"IDR","payer_email":"owner@example.com","payment_method":"BANK_TRANSFER","paid_at":"2026-09-24T03:16:02.000Z","metadata":{"affiliatops_ref":"JANE-4F2A","affiliatops_customer_id":"acct_1042"}}}}},"responses":{"200":{"description":"`{ received: true }` — with `ignored: true` for unhandled callbacks, otherwise per-event `results`"},"400":{"description":"Invalid JSON, or no callback token configured"},"401":{"description":"Missing or wrong x-callback-token"},"404":{"description":"Unknown or disabled Xendit integration"},"500":{"description":"Processing failed; Xendit retries (idempotent)"}}}},"/api/affiliate/profile":{"get":{"tags":["Affiliate"],"operationId":"getAffiliateProfile","summary":"Get my profile, stats, referrals & commissions","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Profile, computed stats and currency symbol"},"401":{"description":"Not authenticated"},"403":{"description":"Not an affiliate"},"404":{"description":"No affiliate profile"}}},"put":{"tags":["Affiliate"],"operationId":"updateAffiliateProfile","summary":"Update my profile & payout details","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProfileRequest"}}}},"responses":{"200":{"description":"Profile updated"},"400":{"description":"Email already in use"}}}},"/api/affiliate/referrals":{"get":{"tags":["Affiliate"],"operationId":"listMyReferrals","summary":"List my referral leads","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Referrals with estimatedValue from metadata"}}},"post":{"tags":["Affiliate"],"operationId":"submitReferral","summary":"Submit a new referral lead","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReferralRequest"},"example":{"leadName":"John Smith","leadEmail":"john@acme.com","company":"Acme","estimatedValue":1000}}}},"responses":{"200":{"description":"Referral created (status PENDING)"},"400":{"description":"Validation error"}}}},"/api/affiliate/payouts":{"get":{"tags":["Affiliate"],"operationId":"listMyPayouts","summary":"List my payout history","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Payouts with amount, status and dates"}}}},"/api/affiliate/generate-code":{"post":{"tags":["Affiliate"],"operationId":"generateReferralCode","summary":"Create or fetch my referral code","description":"Creates the affiliate profile and/or referral code if missing, otherwise returns the existing code.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Affiliate with referral code"},"403":{"description":"Not an affiliate"}}}},"/api/affiliate/resources":{"get":{"tags":["Affiliate"],"operationId":"listMyResources","summary":"List marketing resources","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Active resources for the merchant"}}},"post":{"tags":["Affiliate"],"operationId":"trackResourceDownload","summary":"Increment a resource download counter","security":[{"SessionCookie":[]}],"parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"string"},"description":"Resource id"}],"responses":{"200":{"description":"Counter incremented"},"404":{"description":"Resource not found"}}}},"/api/affiliate/branding":{"get":{"tags":["Affiliate"],"operationId":"getAffiliateBranding","summary":"Get merchant branding (colours & logo)","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Branding settings (may be empty)"}}}},"/api/affiliate/analytics":{"get":{"tags":["Affiliate"],"operationId":"getAffiliateAnalytics","summary":"Get my analytics overview","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Clicks, conversions and earnings over time"}}}},"/api/affiliate/workspace":{"get":{"tags":["Affiliate"],"operationId":"getPartnerWorkspace","summary":"Get my program’s portal mode and commission terms","description":"Which portal to show (`ecommerce` or `saas`), the merchant name and currency, whether customer emails are hidden, and — for SaaS programs — the commission terms as they apply to this partner (partner-group rate included). Partners keep the e-commerce portal until the merchant has picked a business type.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Partner workspace","content":{"application/json":{"example":{"success":true,"workspace":{"mode":"saas","businessType":"SAAS","merchantName":"Kasirku Cloud","currency":"IDR","hideCustomerEmails":true,"commissionTerms":{"commissionType":"PERCENTAGE","rate":15,"duration":"MONTHS","durationMonths":12,"signupBountyCents":0,"trialBountyCents":0,"holdDays":30,"summary":"15% of every payment for 12 months"}}}}}},"401":{"description":"Not authenticated"}}}},"/api/affiliate/saas/overview":{"get":{"tags":["Affiliate"],"operationId":"getPartnerSaasOverview","summary":"Get my SaaS referral KPIs and commission trend","description":"Referred-account counts, referred MRR and lifetime revenue, commission totals by status, projected monthly commission, a monthly commission trend and recent billing activity for the signed-in partner.","security":[{"SessionCookie":[]}],"parameters":[{"name":"months","in":"query","schema":{"type":"integer","default":12,"minimum":3,"maximum":24},"description":"Months in the commission trend (clamped to 3–24)."}],"responses":{"200":{"description":"`{ success, currency, kpis, commissionTrend: [{ month, earnedCents }], recentActivity }`"},"401":{"description":"Not authenticated"}}}},"/api/affiliate/saas/customers":{"get":{"tags":["Affiliate"],"operationId":"listPartnerSaasCustomers","summary":"List my referred SaaS customers","description":"Referred accounts with plan, MRR, commission earned and months of commission left. When the merchant hides customer emails, `email` is null and `label` is masked.","security":[{"SessionCookie":[]}],"parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["SIGNED_UP","TRIALING","ACTIVE","PAST_DUE","CANCELED"]}},{"name":"page","in":"query","schema":{"type":"integer","default":1,"minimum":1}},{"name":"pageSize","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100}}],"responses":{"200":{"description":"`{ success, currency, commissionTerms, customers, total, page, pageSize, summary }`"},"400":{"description":"Invalid status filter"},"401":{"description":"Not authenticated"}}}},"/api/admin/saas/overview":{"get":{"tags":["SaaS (Admin)"],"operationId":"adminSaasOverview","summary":"SaaS dashboard KPIs","description":"MRR, ARR, net new MRR, trial conversion, churn, the partner funnel, top partners, recent billing activity and the setup checklist for the active merchant.","security":[{"SessionCookie":[]}],"parameters":[{"name":"range","in":"query","schema":{"type":"string","enum":["7d","30d","90d","12m"],"default":"30d"}}],"responses":{"200":{"description":"Overview payload with `currency`"},"401":{"description":"Not authenticated"}}}},"/api/admin/saas/metrics":{"get":{"tags":["SaaS (Admin)"],"operationId":"adminSaasMetrics","summary":"SaaS metrics: MRR movements, cohorts, churn","description":"Monthly MRR movements (new, expansion, contraction, churn, reactivation), cohort retention, churn trend, MRR by plan, partner LTV and trial conversion.","security":[{"SessionCookie":[]}],"parameters":[{"name":"months","in":"query","schema":{"type":"integer","default":12,"minimum":3,"maximum":24},"description":"Clamped to 3–24."}],"responses":{"200":{"description":"Metrics payload with `currency`"},"401":{"description":"Not authenticated"}}}},"/api/admin/saas/customers":{"get":{"tags":["SaaS (Admin)"],"operationId":"adminListSaasCustomers","summary":"List referred customers (subscriptions)","description":"Referred accounts with partner, current plan, MRR, lifetime revenue and commission earned. `summary.byStatus` counts ignore the `status` filter. Pass `format=csv` for a CSV download (up to 10,000 rows).","security":[{"SessionCookie":[]}],"parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["SIGNED_UP","TRIALING","ACTIVE","PAST_DUE","CANCELED"]}},{"name":"affiliateId","in":"query","schema":{"type":"string"},"description":"Only customers credited to this partner."},{"name":"planId","in":"query","schema":{"type":"string"},"description":"Only customers with a live subscription on this plan."},{"name":"q","in":"query","schema":{"type":"string"},"description":"Search name, email, your customer id or billing id."},{"name":"sort","in":"query","schema":{"type":"string","enum":["signedUpAt","mrr","revenue","name","firstPaidAt"],"default":"signedUpAt"}},{"name":"dir","in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"page","in":"query","schema":{"type":"integer","default":1,"minimum":1}},{"name":"pageSize","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100}},{"name":"format","in":"query","schema":{"type":"string","enum":["csv"]},"description":"Download as CSV instead of JSON."}],"responses":{"200":{"description":"`{ success, customers, total, page, pageSize, summary: { byStatus, totalMrrCents } }` (or CSV)"},"400":{"description":"Invalid status filter"},"401":{"description":"Not authenticated"}}},"post":{"tags":["SaaS (Admin)"],"operationId":"adminCreateSaasCustomer","summary":"Manually attribute a customer to a partner","description":"For deals closed offline or service businesses. Runs through the billing engine as a MANUAL sign-up, so a sign-up bounty applies when configured. Owner/Admin only.","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","affiliateId"],"properties":{"name":{"type":"string"},"affiliateId":{"type":"string","description":"An active partner in this workspace."},"email":{"type":"string","format":"email"},"externalCustomerId":{"type":"string","description":"Your own account id; generated when omitted."},"signedUpAt":{"type":"string","format":"date-time"}}},"example":{"name":"Toko Sinar Jaya","email":"owner@sinarjaya.id","affiliateId":"aff_123","externalCustomerId":"acct_2001"}}}},"responses":{"201":{"description":"`{ success, customerId, result }`"},"400":{"description":"Missing name/partner, invalid email or date, or partner not active"},"403":{"description":"Owner/Admin required"},"404":{"description":"Partner not found"},"409":{"description":"A customer with this email or id already exists (`customerId` returned)"},"422":{"description":"The engine did not create a customer"}}}},"/api/admin/saas/customers/{id}":{"get":{"tags":["SaaS (Admin)"],"operationId":"adminGetSaasCustomer","summary":"Get a referred customer","description":"Customer profile, partner, subscriptions, payments with refunded totals, the ledger timeline and the commissions this customer generated.","security":[{"SessionCookie":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Customer detail"},"404":{"description":"Customer not found"}}},"patch":{"tags":["SaaS (Admin)"],"operationId":"adminReassignSaasCustomer","summary":"Reassign (or clear) the credited partner","description":"Future commissions follow the new partner; existing commissions are unchanged. Pass `affiliateId: null` to remove attribution. Owner/Admin only.","security":[{"SessionCookie":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["affiliateId"],"properties":{"affiliateId":{"type":"string","nullable":true}}},"example":{"affiliateId":"aff_456"}}}},"responses":{"200":{"description":"Partner reassigned or removed"},"400":{"description":"`affiliateId` missing"},"403":{"description":"Owner/Admin required"},"404":{"description":"Customer or partner not found"}}}},"/api/admin/saas/customers/{id}/actions":{"post":{"tags":["SaaS (Admin)"],"operationId":"adminSaasCustomerAction","summary":"Record a manual payment, refund, subscription or cancellation","description":"Applied through the billing engine as MANUAL events, so the ledger, MRR and commissions stay consistent. Owner/Admin only. `amount` is in major units.\n- `{ action: \"payment\", amount, currency?, occurredAt?, subscriptionId?, note? }`\n- `{ action: \"refund\", paymentEventId, amount?, reason? }` — omit `amount` to refund what is left\n- `{ action: \"subscription\", planId, quantity?, status?: \"ACTIVE\" | \"TRIALING\", trialEndsAt? }`\n- `{ action: \"cancel\", subscriptionId, reason? }`","security":[{"SessionCookie":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["action"],"properties":{"action":{"type":"string","enum":["payment","refund","subscription","cancel"]},"amount":{"type":"number","description":"Major units."},"currency":{"type":"string"},"occurredAt":{"type":"string","format":"date-time"},"subscriptionId":{"type":"string"},"note":{"type":"string"},"paymentEventId":{"type":"string","description":"Ledger id of the payment to refund."},"reason":{"type":"string"},"planId":{"type":"string"},"quantity":{"type":"integer","minimum":1},"status":{"type":"string","enum":["ACTIVE","TRIALING"]},"trialEndsAt":{"type":"string","format":"date-time"}}},"example":{"action":"payment","amount":349000,"currency":"IDR","note":"Bank transfer, invoice 0412"}}}},"responses":{"200":{"description":"`{ success, result }` — the engine result"},"400":{"description":"Invalid action or fields"},"403":{"description":"Owner/Admin required"},"404":{"description":"Customer, subscription, plan or payment not found"}}}},"/api/admin/saas/events":{"get":{"tags":["SaaS (Admin)"],"operationId":"adminListSaasEvents","summary":"Billing-event ledger","description":"Every inbound SaaS event (API, Stripe, tracker, manual), newest first, with its processing status and note. Use it to debug an integration: `IGNORED` rows explain why an event had no effect (e.g. `unattributed customer`).","security":[{"SessionCookie":[]}],"parameters":[{"name":"type","in":"query","schema":{"type":"string","enum":["SIGNUP","TRIAL_STARTED","SUBSCRIPTION_CREATED","SUBSCRIPTION_UPDATED","SUBSCRIPTION_CANCELED","PAYMENT_SUCCEEDED","PAYMENT_FAILED","REFUND"]}},{"name":"status","in":"query","schema":{"type":"string","enum":["PROCESSED","IGNORED","FAILED"]}},{"name":"source","in":"query","schema":{"type":"string","enum":["API","STRIPE","MIDTRANS","XENDIT","TRACKER","MANUAL"]}},{"name":"customerId","in":"query","schema":{"type":"string"}},{"name":"page","in":"query","schema":{"type":"integer","default":1,"minimum":1}},{"name":"pageSize","in":"query","schema":{"type":"integer","default":50,"minimum":1,"maximum":100}}],"responses":{"200":{"description":"`{ success, events, total, page, pageSize }`"},"400":{"description":"Invalid filter"},"401":{"description":"Not authenticated"}}}},"/api/admin/saas/plans":{"get":{"tags":["SaaS (Admin)"],"operationId":"adminListSaasPlans","summary":"List plans","description":"Plan catalog with active/trialing subscription counts and MRR per plan.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"`{ success, plans }`"}}},"post":{"tags":["SaaS (Admin)"],"operationId":"adminCreateSaasPlan","summary":"Create a plan","description":"Owner/Admin only. Plans are also created automatically from Stripe prices and API events.","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaasPlanRequest"},"example":{"name":"Growth","amount":349000,"currency":"IDR","interval":"MONTH","trialDays":14,"externalPriceId":"price_growth_monthly"}}}},"responses":{"201":{"description":"`{ success, plan }`"},"400":{"description":"Validation error"},"403":{"description":"Owner/Admin required"},"409":{"description":"Another plan already uses this external price id"}}}},"/api/admin/saas/plans/{id}":{"put":{"tags":["SaaS (Admin)"],"operationId":"adminUpdateSaasPlan","summary":"Update a plan","description":"Partial update — only the fields you send change. `isActive: false` archives the plan. Owner/Admin only.","security":[{"SessionCookie":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SaasPlanRequest"},{"type":"object","properties":{"isActive":{"type":"boolean"}}}]},"example":{"commissionRateOverride":30}}}},"responses":{"200":{"description":"`{ success, plan }`"},"400":{"description":"Validation error"},"404":{"description":"Plan not found"},"409":{"description":"External price id conflict"}}},"delete":{"tags":["SaaS (Admin)"],"operationId":"adminDeleteSaasPlan","summary":"Delete or archive a plan","description":"Deletes a plan nobody has subscribed to; otherwise archives it so subscription history keeps its plan. The response says which (`deleted` / `archived`). Owner/Admin only.","security":[{"SessionCookie":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"`{ success, deleted, archived, message }`"},"404":{"description":"Plan not found"}}}},"/api/admin/saas/commission-settings":{"get":{"tags":["SaaS (Admin)"],"operationId":"adminGetSaasCommissionSettings","summary":"Get the recurring commission plan","description":"Returns the saved settings, or the defaults (`isDefault: true`): 15% of every payment for 12 months, 30-day hold, clawback on refund.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"`{ success, settings, isDefault }`","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"settings":{"$ref":"#/components/schemas/SaasCommissionSettings"},"isDefault":{"type":"boolean"}}}}}}}},"put":{"tags":["SaaS (Admin)"],"operationId":"adminUpdateSaasCommissionSettings","summary":"Update the recurring commission plan","description":"Partial update; omitted fields keep their current value. Owner/Admin only.","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaasCommissionSettings"},"example":{"recurringRate":25,"duration":"MONTHS","durationMonths":12,"holdDays":30}}}},"responses":{"200":{"description":"Saved settings"},"400":{"description":"Validation failed — `errors: string[]`"},"403":{"description":"Owner/Admin required"}}}},"/api/admin/saas/integrations":{"get":{"tags":["SaaS (Admin)"],"operationId":"adminListSaasIntegrations","summary":"List SaaS integrations","description":"Website & app (CUSTOM) and payment-provider (STRIPE, MIDTRANS, XENDIT) integrations — public key and tracking snippet, or the provider webhook URL to paste in its dashboard — plus billing-event delivery stats for the last 7 days. Secrets are never returned.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"`{ success, integrations, eventStats, apiBaseUrl }`"}}},"post":{"tags":["SaaS (Admin)"],"operationId":"adminCreateSaasIntegration","summary":"Create a Stripe, Midtrans, Xendit or website & app integration","description":"`CUSTOM` (website & app) is active immediately and returns its server key **once** as `createdSecret` (`sk_store_…`). `STRIPE`, `MIDTRANS` and `XENDIT` start as DRAFT: save the provider secret (Stripe signing secret, Midtrans server key, Xendit callback verification token) with PATCH, then the first verified webhook activates it. The response includes `webhookUrl`. Owner/Admin only.","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["provider"],"properties":{"provider":{"type":"string","enum":["STRIPE","MIDTRANS","XENDIT","CUSTOM"]},"name":{"type":"string","maxLength":120},"url":{"type":"string"}}},"example":{"provider":"CUSTOM","name":"Web app","url":"https://app.example.com"}}}},"responses":{"201":{"description":"`{ success, integration, createdSecret? }`"},"400":{"description":"Invalid provider or name"},"403":{"description":"Owner/Admin required"}}}},"/api/admin/saas/integrations/{id}":{"patch":{"tags":["SaaS (Admin)"],"operationId":"adminUpdateSaasIntegration","summary":"Update a SaaS integration","description":"Send any of:\n- `signingSecret`: Stripe endpoint secret (`whsec_…`), Midtrans server key (`Mid-server-…`; client keys are rejected), or Xendit callback verification token. Stored on the integration and never returned.\n- `status`: `ACTIVE` or `DISABLED` (payment providers stay DRAFT until their first verified webhook)\n- `rotateServerKey: true` (website & app): issue a new server key, returned once as `createdSecret`\n- `name`","security":[{"SessionCookie":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"signingSecret":{"type":"string"},"status":{"type":"string","enum":["ACTIVE","DISABLED"]},"rotateServerKey":{"type":"boolean"},"name":{"type":"string"}}},"example":{"signingSecret":"whsec_…"}}}},"responses":{"200":{"description":"`{ success, integration, createdSecret? }`"},"400":{"description":"Invalid field or nothing to update"},"403":{"description":"Owner/Admin required"},"404":{"description":"Integration not found"}}}},"/api/admin/affiliates":{"get":{"tags":["Affiliates (Admin)"],"operationId":"adminListAffiliates","summary":"List affiliates","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Merchant affiliates with referral counts"},"401":{"description":"Not authenticated"}}},"post":{"tags":["Affiliates (Admin)"],"operationId":"adminCreateAffiliate","summary":"Create an affiliate","description":"Creates an active AFFILIATE user and profile. Returns a one-time `temporaryPassword`.","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AffiliateCreateRequest"},"example":{"name":"Jane Partner","email":"jane@example.com"}}}},"responses":{"200":{"description":"Affiliate created"},"400":{"description":"Validation error or email exists"}}}},"/api/admin/affiliates/{id}":{"patch":{"tags":["Affiliates (Admin)"],"operationId":"adminUpdateAffiliateStatus","summary":"Change an affiliate’s status","security":[{"SessionCookie":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["PENDING","ACTIVE","INACTIVE","SUSPENDED"]},"notes":{"type":"string"}}}}}},"responses":{"200":{"description":"Status updated"},"400":{"description":"Invalid status"},"404":{"description":"Affiliate not found"}}},"delete":{"tags":["Affiliates (Admin)"],"operationId":"adminDeleteAffiliate","summary":"Delete an affiliate","security":[{"SessionCookie":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"},"404":{"description":"Not found"}}}},"/api/admin/affiliates/batch":{"post":{"tags":["Affiliates (Admin)"],"operationId":"adminBatchAffiliates","summary":"Batch update affiliates","description":"Bulk `changeStatus`, `changeGroup` or `delete` over a list of affiliate ids.","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["affiliateIds","action"],"properties":{"affiliateIds":{"type":"array","items":{"type":"string"},"minItems":1},"action":{"type":"string","enum":["changeStatus","changeGroup","delete"]},"status":{"type":"string","enum":["PENDING","ACTIVE","INACTIVE","SUSPENDED"]},"group":{"type":"string"}}}}}},"responses":{"200":{"description":"Affiliates updated"},"400":{"description":"Missing or invalid parameters"}}}},"/api/admin/referrals":{"get":{"tags":["Referrals (Admin)"],"operationId":"adminListReferrals","summary":"List referral leads","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Referrals with affiliate and partner-group info"}}},"post":{"tags":["Referrals (Admin)"],"operationId":"adminBatchReferrals","summary":"Batch approve / reject referrals","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["referralIds","action"],"properties":{"referralIds":{"type":"array","items":{"type":"string"},"minItems":1},"action":{"type":"string","enum":["approve","reject"]}}}}}},"responses":{"200":{"description":"Referrals updated"},"400":{"description":"Invalid action"}}}},"/api/admin/referrals/{id}":{"put":{"tags":["Referrals (Admin)"],"operationId":"adminReviewReferral","summary":"Approve or reject a referral","description":"On approval, creates a PENDING conversion and commission using the partner group rate, else the 15% backup rate.","security":[{"SessionCookie":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["action"],"properties":{"action":{"type":"string","enum":["approve","reject"]},"reviewNotes":{"type":"string"}}}}}},"responses":{"200":{"description":"Referral reviewed"},"400":{"description":"Invalid action"},"404":{"description":"Not found"}}},"patch":{"tags":["Referrals (Admin)"],"operationId":"adminUpdateReferral","summary":"Edit a referral lead","description":"Updates `leadName`, `leadEmail`, `status` or `reviewNotes`. Passing `action: \"approve\" | \"reject\"` behaves like PUT.","security":[{"SessionCookie":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"leadName":{"type":"string"},"leadEmail":{"type":"string","format":"email"},"status":{"type":"string","enum":["PENDING","APPROVED","REJECTED"]},"reviewNotes":{"type":"string"},"action":{"type":"string","enum":["approve","reject"]}}}}}},"responses":{"200":{"description":"Referral updated"},"404":{"description":"Not found"}}},"delete":{"tags":["Referrals (Admin)"],"operationId":"adminDeleteReferral","summary":"Delete a referral","security":[{"SessionCookie":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"}}}},"/api/admin/commissions":{"get":{"tags":["Commissions (Admin)"],"operationId":"adminListCommissions","summary":"List commissions","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Commission ledger for the merchant"}}},"patch":{"tags":["Commissions (Admin)"],"operationId":"adminUpdateCommission","summary":"Update a commission (status, approval)","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["id"],"properties":{"id":{"type":"string"},"status":{"type":"string","enum":["PENDING","APPROVED","PAID","CANCELLED","CLAWBACK"]}}}}}},"responses":{"200":{"description":"Commission updated"}}}},"/api/admin/commissions/mature":{"post":{"tags":["Commissions (Admin)"],"operationId":"matureCommissions","summary":"Mature held commissions","description":"Promotes PENDING commissions past their hold period to APPROVED and credits partner balances. The app already runs this every hour on its own; call it to run it now. An admin **session** matures its own merchant’s commissions; a scheduler sending `X-Cron-Secret` (no session needed) matures every merchant’s. Safe to call concurrently: a commission is never credited twice.","security":[{"SessionCookie":[]},{"CronSecret":[]}],"responses":{"200":{"description":"Returns how many commissions matured"},"401":{"description":"Not authorized"}}}},"/api/admin/commission-rules":{"get":{"tags":["Commissions (Admin)"],"operationId":"listCommissionRules","summary":"List commission rules","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Commission rules"}}},"post":{"tags":["Commissions (Admin)"],"operationId":"createCommissionRule","summary":"Create a commission rule","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommissionRuleRequest"},"example":{"name":"Standard","type":"PERCENTAGE","value":15,"isDefault":true}}}},"responses":{"200":{"description":"Rule created"},"400":{"description":"Validation error"}}},"put":{"tags":["Commissions (Admin)"],"operationId":"updateCommissionRule","summary":"Update a commission rule","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"type":"object","required":["id"],"properties":{"id":{"type":"string"}}},{"$ref":"#/components/schemas/CommissionRuleRequest"}]}}}},"responses":{"200":{"description":"Rule updated"},"404":{"description":"Not found"}}},"delete":{"tags":["Commissions (Admin)"],"operationId":"deleteCommissionRule","summary":"Delete a commission rule","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["id"],"properties":{"id":{"type":"string"}}}}}},"responses":{"200":{"description":"Deleted"},"404":{"description":"Not found"}}}},"/api/admin/payouts":{"get":{"tags":["Payouts (Admin)"],"operationId":"adminListPayouts","summary":"List payouts","security":[{"SessionCookie":[]}],"parameters":[{"name":"affiliateId","in":"query","schema":{"type":"string"},"description":"Filter by affiliate"},{"name":"format","in":"query","schema":{"type":"string","enum":["csv"]},"description":"Export as CSV"}],"responses":{"200":{"description":"Payouts (or a CSV attachment)"}}},"post":{"tags":["Payouts (Admin)"],"operationId":"adminCreatePayout","summary":"Create a payout from approved commissions","description":"Aggregates the given APPROVED commissions into a single payout, marks them PAID, and emails the affiliate. Commissions still in their hold period are rejected with 400.","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayoutRequest"},"example":{"affiliateId":"aff_123","commissionIds":["cm_1","cm_2"],"method":"Bank Transfer"}}}},"responses":{"200":{"description":"Payout created"},"400":{"description":"Some commissions are not payable yet"},"404":{"description":"Affiliate or commissions not found"}}},"put":{"tags":["Payouts (Admin)"],"operationId":"adminUpdatePayout","summary":"Update payout status","description":"Transition a payout (e.g. to COMPLETED, which stamps `processedAt` and emails the affiliate).","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayoutUpdateRequest"},"example":{"id":"po_123","status":"COMPLETED"}}}},"responses":{"200":{"description":"Payout updated"},"400":{"description":"Invalid status"}}},"delete":{"tags":["Payouts (Admin)"],"operationId":"adminDeletePayout","summary":"Delete a payout","security":[{"SessionCookie":[]}],"parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"},"404":{"description":"Not found"}}}},"/api/admin/payouts/auto":{"get":{"tags":["Payouts (Admin)"],"operationId":"autoPayoutConfig","summary":"Auto-payout config & eligibility","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Threshold, eligible affiliate count and pending total"}}},"post":{"tags":["Payouts (Admin)"],"operationId":"runAutoPayout","summary":"Run auto-payouts","description":"Creates payouts for every affiliate whose balance is over the minimum threshold and resets their balance. Pass `{ \"dryRun\": true }` to preview without writing.","security":[{"SessionCookie":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"dryRun":{"type":"boolean","default":false}}}}}},"responses":{"200":{"description":"Processed (or dry-run preview)"}}}},"/api/admin/transactions":{"get":{"tags":["Payouts (Admin)"],"operationId":"adminListTransactions","summary":"List transactions","security":[{"SessionCookie":[]}],"parameters":[{"name":"referralId","in":"query","schema":{"type":"string"}},{"name":"affiliateId","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Transactions with referral & affiliate context"}}},"post":{"tags":["Payouts (Admin)"],"operationId":"adminCreateTransaction","summary":"Record a transaction","description":"Creates a transaction for a referral and a matching conversion. `amount` is in major units; commission uses the partner group rate, else the 15% backup rate.","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["referralId","amount"],"properties":{"referralId":{"type":"string"},"amount":{"type":"number"},"description":{"type":"string"},"invoiceId":{"type":"string"},"paymentMethod":{"type":"string"},"paidAt":{"type":"string","format":"date-time"}}}}}},"responses":{"200":{"description":"Transaction created"},"404":{"description":"Referral not found"}}},"put":{"tags":["Payouts (Admin)"],"operationId":"adminUpdateTransaction","summary":"Update a transaction","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Updated"},"404":{"description":"Not found"}}},"delete":{"tags":["Payouts (Admin)"],"operationId":"adminDeleteTransaction","summary":"Delete a transaction","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Deleted"}}}},"/api/admin/refunds":{"get":{"tags":["Payouts (Admin)"],"operationId":"adminListRefunds","summary":"List refunds","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Refund records"}}},"post":{"tags":["Payouts (Admin)"],"operationId":"adminCreateRefund","summary":"Refund a transaction","description":"Marks a transaction REFUNDED and reverses the associated commission, deducting from the affiliate balance where applicable.","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["transactionId"],"properties":{"transactionId":{"type":"string"},"reason":{"type":"string"}}}}}},"responses":{"200":{"description":"Refund processed"},"400":{"description":"Already refunded"},"404":{"description":"Transaction not found"}}}},"/api/admin/invoices":{"get":{"tags":["Payouts (Admin)"],"operationId":"adminListInvoices","summary":"List invoices","security":[{"SessionCookie":[]}],"parameters":[{"name":"affiliateId","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Invoices"}}},"post":{"tags":["Payouts (Admin)"],"operationId":"adminCreateInvoice","summary":"Create an invoice","description":"Generates a merchant-scoped, sequential invoice number (e.g. `INV-ACME-2026-0003`).","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["affiliateId","amountCents"],"properties":{"affiliateId":{"type":"string"},"amountCents":{"type":"integer"},"taxCents":{"type":"integer"},"notes":{"type":"string"},"dueAt":{"type":"string","format":"date-time"}}}}}},"responses":{"200":{"description":"Invoice created"},"404":{"description":"Affiliate not found"}}},"put":{"tags":["Payouts (Admin)"],"operationId":"adminUpdateInvoice","summary":"Update an invoice","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Updated"},"404":{"description":"Not found"}}},"delete":{"tags":["Payouts (Admin)"],"operationId":"adminDeleteInvoice","summary":"Delete a (non-paid) invoice","security":[{"SessionCookie":[]}],"parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"},"400":{"description":"Cannot delete a paid invoice"}}}},"/api/admin/programs":{"get":{"tags":["Programs (Admin)"],"operationId":"listPrograms","summary":"List programs","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Programs"}}},"post":{"tags":["Programs (Admin)"],"operationId":"createProgram","summary":"Create a program","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","slug"],"properties":{"name":{"type":"string"},"slug":{"type":"string"},"commissionRate":{"type":"number"},"commissionType":{"type":"string","enum":["PERCENTAGE","FIXED"]},"currency":{"type":"string"}}}}}},"responses":{"200":{"description":"Program created"},"400":{"description":"Slug already exists"}}},"put":{"tags":["Programs (Admin)"],"operationId":"updateProgram","summary":"Update a program","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Updated"},"404":{"description":"Not found"}}},"delete":{"tags":["Programs (Admin)"],"operationId":"deleteProgram","summary":"Delete a program","security":[{"SessionCookie":[]}],"parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"}}}},"/api/admin/program-settings":{"get":{"tags":["Programs (Admin)"],"operationId":"getProgramSettings","summary":"Get program settings","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Program settings"}}},"put":{"tags":["Programs (Admin)"],"operationId":"updateProgramSettings","summary":"Update program settings","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProgramSettingsRequest"}}}},"responses":{"200":{"description":"Updated"}}}},"/api/admin/settings":{"get":{"tags":["Programs (Admin)"],"operationId":"getSettings","summary":"Get settings & commission rules","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Settings with commission rules"}}},"put":{"tags":["Programs (Admin)"],"operationId":"updateSettings","summary":"Update settings (allow-listed fields)","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Updated"}}},"post":{"tags":["Programs (Admin)"],"operationId":"mutateCommissionRule","summary":"Create / update / delete a commission rule","description":"Body `{ action: \"create\" | \"update\" | \"delete\", ruleData }`.","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["action"],"properties":{"action":{"type":"string","enum":["create","update","delete"]},"ruleData":{"type":"object"}}}}}},"responses":{"200":{"description":"Rule mutated"},"400":{"description":"Invalid action"},"404":{"description":"Rule not found"}}}},"/api/admin/partner-groups":{"get":{"tags":["Programs (Admin)"],"operationId":"listPartnerGroups","summary":"List partner groups","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Partner groups with member counts"}}},"post":{"tags":["Programs (Admin)"],"operationId":"createPartnerGroup","summary":"Create a partner group","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","commissionRate"],"properties":{"name":{"type":"string"},"commissionRate":{"type":"number","minimum":0,"description":"Fraction between 0 and 1 (e.g. 0.2 = 20%)"},"description":{"type":"string"},"isDefault":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Created"},"400":{"description":"Validation error"}}},"put":{"tags":["Programs (Admin)"],"operationId":"updatePartnerGroup","summary":"Update a partner group","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Updated"},"400":{"description":"Missing id"}}},"delete":{"tags":["Programs (Admin)"],"operationId":"deletePartnerGroup","summary":"Delete a partner group","security":[{"SessionCookie":[]}],"parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"}}}},"/api/admin/coupons":{"get":{"tags":["Programs (Admin)"],"operationId":"listCoupons","summary":"List coupons","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Coupons"}}},"post":{"tags":["Programs (Admin)"],"operationId":"createCoupon","summary":"Create a coupon","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["code","discountValue"],"properties":{"code":{"type":"string"},"discountValue":{"type":"number"},"discountType":{"type":"string","enum":["PERCENTAGE","FIXED"]},"affiliateId":{"type":"string"},"maxUses":{"type":"integer"}}}}}},"responses":{"200":{"description":"Created"},"400":{"description":"Code already exists"}}},"put":{"tags":["Programs (Admin)"],"operationId":"updateCoupon","summary":"Update a coupon","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Updated"},"404":{"description":"Not found"}}},"delete":{"tags":["Programs (Admin)"],"operationId":"deleteCoupon","summary":"Delete a coupon","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["id"],"properties":{"id":{"type":"string"}}}}}},"responses":{"200":{"description":"Deleted"}}}},"/api/admin/resources":{"get":{"tags":["Programs (Admin)"],"operationId":"adminListResources","summary":"List marketing resources","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Resources"}}},"post":{"tags":["Programs (Admin)"],"operationId":"adminCreateResource","summary":"Create a resource","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title","type","fileUrl","fileName"],"properties":{"title":{"type":"string"},"type":{"type":"string"},"fileUrl":{"type":"string","format":"uri"},"fileName":{"type":"string"},"category":{"type":"string"}}}}}},"responses":{"200":{"description":"Created"},"400":{"description":"Validation error"}}},"put":{"tags":["Programs (Admin)"],"operationId":"adminUpdateResource","summary":"Update a resource","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Updated"},"404":{"description":"Not found"}}},"delete":{"tags":["Programs (Admin)"],"operationId":"adminDeleteResource","summary":"Delete a resource","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["id"],"properties":{"id":{"type":"string"}}}}}},"responses":{"200":{"description":"Deleted"}}}},"/api/admin/dashboard":{"get":{"tags":["Analytics (Admin)"],"operationId":"adminDashboard","summary":"Dashboard summary stats","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Counts, revenue and estimated commission"}}}},"/api/admin/analytics":{"get":{"tags":["Analytics (Admin)"],"operationId":"adminAnalytics","summary":"Analytics overview","security":[{"SessionCookie":[]}],"parameters":[{"name":"days","in":"query","schema":{"type":"integer","default":30},"description":"Look-back window in days"}],"responses":{"200":{"description":"Top affiliates, conversion rate, daily revenue and commission stats"}}}},"/api/admin/detailed-analytics":{"get":{"tags":["Analytics (Admin)"],"operationId":"adminDetailedAnalytics","summary":"Detailed analytics","description":"Deep analytics derived from tracked events (traffic sources, funnels, products).","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Detailed analytics payload"}}}},"/api/admin/live-sessions":{"get":{"tags":["Analytics (Admin)"],"operationId":"adminLiveSessions","summary":"Live visitor sessions","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Currently active sessions for the live view"}}}},"/api/admin/sessions":{"get":{"tags":["Analytics (Admin)"],"operationId":"adminSessions","summary":"Historical sessions","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Session list"}}}},"/api/admin/reports":{"get":{"tags":["Analytics (Admin)"],"operationId":"adminReports","summary":"Generate a report","description":"Report types via `?type=` — `affiliates`, `referrals`, `commissions` or `summary`. Pass `?format=csv` for a CSV download and optional `startDate`/`endDate`.","security":[{"SessionCookie":[]}],"parameters":[{"name":"type","in":"query","schema":{"type":"string","enum":["summary","affiliates","referrals","commissions"]}},{"name":"format","in":"query","schema":{"type":"string","enum":["json","csv"]}},{"name":"startDate","in":"query","schema":{"type":"string","format":"date"}},{"name":"endDate","in":"query","schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"Report (JSON or CSV)"}}}},"/api/admin/reports/cohort":{"get":{"tags":["Analytics (Admin)"],"operationId":"adminCohortReport","summary":"Affiliate cohort analysis","security":[{"SessionCookie":[]}],"parameters":[{"name":"period","in":"query","schema":{"type":"string","enum":["3m","6m","12m"],"default":"6m"}},{"name":"groupBy","in":"query","schema":{"type":"string","enum":["week","month"],"default":"month"}}],"responses":{"200":{"description":"Cohort analysis"}}}},"/api/admin/reports/email":{"post":{"tags":["Analytics (Admin)"],"operationId":"adminEmailReport","summary":"Email a report","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reportType","recipients"],"properties":{"reportType":{"type":"string"},"recipients":{"type":"array","items":{"type":"string","format":"email"},"minItems":1},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"}}}}}},"responses":{"200":{"description":"Report emailed"},"400":{"description":"Missing reportType or recipients"}}}},"/api/admin/saved-reports":{"get":{"tags":["Analytics (Admin)"],"operationId":"listSavedReports","summary":"List saved reports","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Saved report configs"}}},"post":{"tags":["Analytics (Admin)"],"operationId":"createSavedReport","summary":"Save a report config","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","reportType"],"properties":{"name":{"type":"string"},"reportType":{"type":"string"},"columns":{"type":"array","items":{"type":"string"}},"filters":{"type":"object"}}}}}},"responses":{"200":{"description":"Saved"},"400":{"description":"Validation error"}}},"put":{"tags":["Analytics (Admin)"],"operationId":"updateSavedReport","summary":"Update a saved report","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Updated"},"404":{"description":"Not found"}}},"delete":{"tags":["Analytics (Admin)"],"operationId":"deleteSavedReport","summary":"Delete a saved report","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["id"],"properties":{"id":{"type":"string"}}}}}},"responses":{"200":{"description":"Deleted"}}}},"/api/admin/scheduled-reports":{"get":{"tags":["Analytics (Admin)"],"operationId":"listScheduledReports","summary":"List scheduled reports","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Scheduled reports"}}},"post":{"tags":["Analytics (Admin)"],"operationId":"createScheduledReport","summary":"Schedule a report","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","reportType","frequency"],"properties":{"name":{"type":"string"},"reportType":{"type":"string"},"frequency":{"type":"string","enum":["DAILY","WEEKLY","MONTHLY"]},"recipients":{"type":"array","items":{"type":"string","format":"email"}}}}}}},"responses":{"200":{"description":"Scheduled"},"400":{"description":"Validation error"}}},"put":{"tags":["Analytics (Admin)"],"operationId":"updateScheduledReport","summary":"Update a scheduled report","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Updated"}}},"delete":{"tags":["Analytics (Admin)"],"operationId":"deleteScheduledReport","summary":"Delete a scheduled report","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Deleted"}}}},"/api/admin/store-integrations":{"get":{"tags":["Integrations (Admin)"],"operationId":"listStoreIntegrations","summary":"List store integrations","description":"Returns each integration with a ready-to-paste tracking snippet.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Store integrations"}}},"post":{"tags":["Integrations (Admin)"],"operationId":"createStoreIntegration","summary":"Connect a store","description":"Generates a public key (`pk_store_…`), a secret and a webhook secret. Owner/Admin only.","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["platform","storeName"],"properties":{"platform":{"type":"string","enum":["SHOPIFY","WOOCOMMERCE","TOKOPEDIA","SHOPEE","LAZADA","CUSTOM"]},"storeName":{"type":"string"},"storeUrl":{"type":"string","format":"uri"}}}}}},"responses":{"201":{"description":"Integration created"},"400":{"description":"Invalid platform or missing store name"},"403":{"description":"Owner/Admin required"}}},"put":{"tags":["Integrations (Admin)"],"operationId":"updateStoreIntegration","summary":"Update a store integration","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["id"],"properties":{"id":{"type":"string"},"status":{"type":"string","enum":["DRAFT","ACTIVE","NEEDS_RECONNECT","DISABLED"]},"storeName":{"type":"string"}}}}}},"responses":{"200":{"description":"Updated"},"404":{"description":"Not found"}}},"delete":{"tags":["Integrations (Admin)"],"operationId":"deleteStoreIntegration","summary":"Delete a store integration","description":"Owner only.","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["id"],"properties":{"id":{"type":"string"}}}}}},"responses":{"200":{"description":"Deleted"},"403":{"description":"Owner required"}}}},"/api/admin/api-keys":{"get":{"tags":["Integrations (Admin)"],"operationId":"listApiKeys","summary":"List API keys (masked)","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"API keys with masked values and usage counts"}}},"post":{"tags":["Integrations (Admin)"],"operationId":"createApiKey","summary":"Create an API key","description":"Returns the plaintext key **once**. Only a SHA-256 hash is stored.","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"scopes":{"type":"array","items":{"type":"string","enum":["read","write","admin"]}},"rateLimit":{"type":"integer","default":100},"expiresAt":{"type":"string","format":"date-time"}}}}}},"responses":{"200":{"description":"Key created (plaintext returned once)"},"400":{"description":"Name required"}}},"put":{"tags":["Integrations (Admin)"],"operationId":"updateApiKey","summary":"Update an API key","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Updated"},"404":{"description":"Not found"}}},"delete":{"tags":["Integrations (Admin)"],"operationId":"revokeApiKey","summary":"Revoke an API key","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["id"],"properties":{"id":{"type":"string"}}}}}},"responses":{"200":{"description":"Revoked"}}}},"/api/admin/api-usage":{"get":{"tags":["Integrations (Admin)"],"operationId":"apiUsage","summary":"API usage analytics","security":[{"SessionCookie":[]}],"parameters":[{"name":"period","in":"query","schema":{"type":"string","enum":["1d","7d","30d","90d"],"default":"7d"}},{"name":"apiKeyId","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Request counts, latency, errors and top endpoints"}}}},"/api/admin/integration/generate-key":{"post":{"tags":["Integrations (Admin)"],"operationId":"generateIntegrationKey","summary":"Generate legacy integration keys","description":"Generates `pk_`/`sk_` keys for the legacy IntegrationSettings tracking model.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Keys generated"},"403":{"description":"Admin required"}}}},"/api/admin/integration":{"get":{"tags":["Integrations (Admin)"],"operationId":"getIntegration","summary":"Get legacy integration settings","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Integration settings"}}},"put":{"tags":["Integrations (Admin)"],"operationId":"updateIntegration","summary":"Update legacy integration settings","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Updated"}}}},"/api/admin/settings/integration":{"get":{"tags":["Integrations (Admin)"],"operationId":"getIntegrationSettings","summary":"Get my legacy integration settings","description":"Legacy per-user IntegrationSettings. New integrations use Store integrations or SaaS integrations.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Integration settings (may be null)"}}},"post":{"tags":["Integrations (Admin)"],"operationId":"saveIntegrationSettings","summary":"Create or update legacy integration settings","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["provider"],"properties":{"provider":{"type":"string"},"apiKey":{"type":"string"},"publicKey":{"type":"string"},"webhookUrl":{"type":"string"},"trackingScript":{"type":"string"},"isActive":{"type":"boolean"},"config":{"type":"object"}}}}}},"responses":{"200":{"description":"Saved"},"400":{"description":"Provider is required"}}},"delete":{"tags":["Integrations (Admin)"],"operationId":"deleteIntegrationSettings","summary":"Delete my legacy integration settings","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Deleted"},"404":{"description":"Not found"}}}},"/api/admin/emails/test":{"post":{"tags":["Integrations (Admin)"],"operationId":"sendTestEmail","summary":"Send a test email from a template","description":"Renders the template with sample variables and sends it to `email` (defaults to your own address).","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["templateId"],"properties":{"templateId":{"type":"string"},"email":{"type":"string","format":"email"}}}}}},"responses":{"200":{"description":"Test email sent"},"400":{"description":"Template ID is required"},"404":{"description":"Template not found"}}}},"/api/admin/webhooks":{"get":{"tags":["Integrations (Admin)"],"operationId":"listWebhooks","summary":"List outbound webhooks","description":"Secrets are masked in the list.","security":[{"SessionCookie":[]}],"parameters":[{"name":"includeInactive","in":"query","schema":{"type":"boolean"}},{"name":"includeLogs","in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"Webhooks, delivery stats and available events"}}},"post":{"tags":["Integrations (Admin)"],"operationId":"createOrTestWebhook","summary":"Create, test or trigger a webhook","description":"Body `{ action: \"create\" | \"test\" | \"trigger\", ... }`. `create` validates the URL against SSRF and returns the secret once.","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["action"],"properties":{"action":{"type":"string","enum":["create","test","trigger"]},"name":{"type":"string"},"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"Action completed"},"400":{"description":"Invalid URL (SSRF), bad events or missing fields"}}},"put":{"tags":["Integrations (Admin)"],"operationId":"updateWebhook","summary":"Update a webhook","description":"Pass `regenerateSecret: true` to rotate the signing secret (returned once).","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Updated"},"404":{"description":"Not found"}}},"delete":{"tags":["Integrations (Admin)"],"operationId":"deleteWebhook","summary":"Delete a webhook","security":[{"SessionCookie":[]}],"parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"}}}},"/api/admin/emails":{"get":{"tags":["Integrations (Admin)"],"operationId":"listEmailTemplates","summary":"List email templates","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Templates with sent counts"}}},"post":{"tags":["Integrations (Admin)"],"operationId":"upsertEmailTemplate","summary":"Create or update an email template","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["type","name","subject","body"],"properties":{"id":{"type":"string"},"type":{"type":"string"},"name":{"type":"string"},"subject":{"type":"string"},"body":{"type":"string"}}}}}},"responses":{"200":{"description":"Saved"},"400":{"description":"Missing fields"}}},"put":{"tags":["Integrations (Admin)"],"operationId":"updateEmailTemplate","summary":"Update an email template","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Updated"}}},"delete":{"tags":["Integrations (Admin)"],"operationId":"deleteEmailTemplate","summary":"Delete an email template","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Deleted"}}}},"/api/admin/billing":{"get":{"tags":["Billing (Admin)"],"operationId":"getBilling","summary":"Get the workspace plan, usage, payment method and invoices","description":"Syncs the subscription from Stripe first, then returns the plan and status, usage against plan limits, the default payment method, up to 24 invoices and the next payment. When STRIPE_SECRET_KEY is missing it returns `configured: false` instead of failing. Any workspace member can view; `canManage` is true for Owner/Admin.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"`{ success, billing }`","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"billing":{"$ref":"#/components/schemas/BillingOverview"}}}}}},"401":{"description":"Not authenticated"},"403":{"description":"Admin access required"}}}},"/api/admin/billing/status":{"get":{"tags":["Billing (Admin)"],"operationId":"getBillingStatus","summary":"Get the plan status (database only)","description":"Plan status without calling Stripe, for banners: `{ configured, canManage, status, plan, trialEndsAt, currentPeriodEnd, cancelAtPeriodEnd }`. `status` is `none` before the first subscription.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"`{ success, status }`"},"401":{"description":"Not authenticated"}}}},"/api/admin/billing/checkout":{"post":{"tags":["Billing (Admin)"],"operationId":"startBillingCheckout","summary":"Start Stripe Checkout for a plan","description":"Creates a Stripe Checkout Session for the plan and billing period and returns its `url`. First-time subscribers get the free trial without entering a card; the trial cancels if no payment method is added. Owner/Admin only.","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["plan","billing"],"properties":{"plan":{"type":"string","enum":["starter","growth","scale"]},"billing":{"type":"string","enum":["monthly","yearly"]}}},"example":{"plan":"growth","billing":"yearly"}}}},"responses":{"200":{"description":"`{ success, url, trial }`"},"400":{"description":"Invalid plan or billing period"},"403":{"description":"Owner/Admin required"},"409":{"description":"The workspace already has a plan; use change-plan"},"503":{"description":"Billing is not configured"}}}},"/api/admin/billing/change-plan":{"post":{"tags":["Billing (Admin)"],"operationId":"changeBillingPlan","summary":"Switch plan or billing period","description":"Moves the current subscription to another plan or billing period, with prorations. Owner/Admin only.","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["plan","billing"],"properties":{"plan":{"type":"string","enum":["starter","growth","scale"]},"billing":{"type":"string","enum":["monthly","yearly"]}}},"example":{"plan":"growth","billing":"yearly"}}}},"responses":{"200":{"description":"`{ success, billing }`","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"billing":{"$ref":"#/components/schemas/BillingOverview"}}}}}},"400":{"description":"Invalid plan, or already on this plan"},"403":{"description":"Owner/Admin required"},"409":{"description":"No active plan to change"},"503":{"description":"Billing is not configured"}}}},"/api/admin/billing/cancel":{"post":{"tags":["Billing (Admin)"],"operationId":"cancelBillingPlan","summary":"Cancel the plan at the end of the period","description":"Schedules cancellation at the end of the current billing period (or trial). The plan keeps working until then. Owner/Admin only.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"`{ success, billing }`","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"billing":{"$ref":"#/components/schemas/BillingOverview"}}}}}},"403":{"description":"Owner/Admin required"},"409":{"description":"No active plan to cancel"},"503":{"description":"Billing is not configured"}}}},"/api/admin/billing/resume":{"post":{"tags":["Billing (Admin)"],"operationId":"resumeBillingPlan","summary":"Undo a scheduled cancellation","description":"Keeps the plan running after a cancellation was scheduled, as long as it hasn’t ended yet. Owner/Admin only.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"`{ success, billing }`","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"billing":{"$ref":"#/components/schemas/BillingOverview"}}}}}},"403":{"description":"Owner/Admin required"},"409":{"description":"No scheduled cancellation"},"503":{"description":"Billing is not configured"}}}},"/api/admin/billing/portal":{"post":{"tags":["Billing (Admin)"],"operationId":"openBillingPortal","summary":"Open the Stripe customer portal","description":"Returns a one-time `url` to the Stripe customer portal for payment methods, invoices, billing details, plan changes and cancellation. Owner/Admin only.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"`{ success, url }`"},"403":{"description":"Owner/Admin required"},"503":{"description":"Billing is not configured"}}}},"/api/billing/webhook":{"post":{"tags":["Billing (Admin)"],"operationId":"billingWebhook","summary":"Stripe webhook for Affiliatops plan billing","description":"Receives Stripe events for Affiliatops’ own plan billing (not merchants’ SaaS billing, which goes to /api/webhook/stripe/{integrationId}). There is no API key: each request must carry the `Stripe-Signature` header, verified against STRIPE_WEBHOOK_SECRET. Handled events: `checkout.session.completed`, `customer.subscription.created|updated|deleted|paused|resumed|trial_will_end`, `invoice.paid`, `invoice.payment_failed`, `invoice.finalized`. Each one re-syncs the workspace’s subscription from Stripe; others are acknowledged and ignored.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"A Stripe Event object."}}}},"responses":{"200":{"description":"`{ received: true }`, with `ignored: true` for unhandled events or unknown customers"},"400":{"description":"Missing or invalid Stripe-Signature"},"500":{"description":"Processing failed; Stripe retries"},"503":{"description":"Billing is not configured"}}}},"/api/admin/workspace":{"get":{"tags":["Team (Admin)"],"operationId":"getWorkspace","summary":"Get the active workspace (business type and mode)","description":"The active merchant’s business type, interface mode (`ecommerce` or `saas`; SAAS and OTHER both use `saas`), onboarding state, currency and market. Your membership role is included.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Workspace","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"workspace":{"$ref":"#/components/schemas/WorkspaceInfo"}}}}}},"401":{"description":"Not authenticated, or the account is pending review"}}}},"/api/admin/onboarding":{"post":{"tags":["Team (Admin)"],"operationId":"completeOnboarding","summary":"Complete (or re-run) workspace onboarding","description":"Records the business type — which picks the e-commerce or SaaS workspace — plus optional business details and the commission rate partners earn. `commission` is required the first time (re-runs may omit it to keep the saved rate). For ECOMMERCE it is `{ commissionType: PERCENTAGE|FIXED, value }` (percent, or cents per order) and becomes the default commission rule. For SAAS/OTHER it takes the commission-settings fields, must include `commissionType` and `recurringRate`, and becomes the program rate. The onboarding form prefills 15%. Owner/Admin only.","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["businessType"],"properties":{"businessType":{"type":"string","enum":["ECOMMERCE","SAAS","OTHER"]},"name":{"type":"string"},"websiteUrl":{"type":"string"},"defaultCurrency":{"type":"string","description":"3-letter ISO code."},"market":{"type":"string","description":"2-letter country code."},"commission":{"description":"ECOMMERCE: `{ commissionType, value }`. SAAS/OTHER: SaasCommissionSettings fields (commissionType and recurringRate required on first onboarding).","oneOf":[{"type":"object","required":["commissionType","value"],"properties":{"commissionType":{"type":"string","enum":["PERCENTAGE","FIXED"]},"value":{"type":"number","description":"Percent (0–100] for PERCENTAGE; whole cents per order for FIXED."}}},{"$ref":"#/components/schemas/SaasCommissionSettings"}]}}},"example":{"businessType":"SAAS","name":"Kasirku Cloud","websiteUrl":"https://kasirku.id","defaultCurrency":"IDR","market":"ID","commission":{"commissionType":"PERCENTAGE","recurringRate":15,"duration":"MONTHS","durationMonths":12}}}}},"responses":{"200":{"description":"`{ success, workspace, commissionSettings, commissionRule }`"},"400":{"description":"Validation error, including a missing commission rate"},"403":{"description":"Owner/Admin required"}}}},"/api/admin/commission-setup":{"get":{"tags":["Commissions (Admin)"],"operationId":"getCommissionSetup","summary":"Has the business set its commission rate?","description":"`needsSetup: true` means there is no active commission rule (e-commerce) or saved commission plan (SaaS/Other), so partners earn the backup rate (`defaultPercent`, 15%). `settingsPath` is the admin page where the rate is set.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Setup status","content":{"application/json":{"example":{"success":true,"mode":"ecommerce","needsSetup":true,"defaultPercent":15,"settingsPath":"/admin/program-settings#commission-rules"}}}}}}},"/api/admin/merchant":{"get":{"tags":["Team (Admin)"],"operationId":"getMerchant","summary":"Get the current merchant","description":"Returns the merchant, its members and store-integration count.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Merchant account"}}},"put":{"tags":["Team (Admin)"],"operationId":"updateMerchant","summary":"Update the merchant","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"websiteUrl":{"type":"string","format":"uri"},"market":{"type":"string"},"defaultCurrency":{"type":"string"}}}}}},"responses":{"200":{"description":"Updated"},"403":{"description":"Owner/Admin required"}}}},"/api/admin/merchant/members":{"get":{"tags":["Team (Admin)"],"operationId":"listMerchantMembers","summary":"List merchant members","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Members and your membership"}}},"post":{"tags":["Team (Admin)"],"operationId":"inviteMerchantMember","summary":"Invite a member","description":"Only OWNER/ADMIN may invite; only an OWNER may grant the OWNER role.","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"},"role":{"type":"string","enum":["OWNER","ADMIN","ANALYST"]}}}}}},"responses":{"200":{"description":"Member invited"},"403":{"description":"Insufficient role"}}},"patch":{"tags":["Team (Admin)"],"operationId":"updateMerchantMember","summary":"Update a member role/status","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Updated"},"404":{"description":"Not found"}}},"delete":{"tags":["Team (Admin)"],"operationId":"removeMerchantMember","summary":"Remove a member","security":[{"SessionCookie":[]}],"parameters":[{"name":"memberId","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Removed"},"403":{"description":"Insufficient role"}}}},"/api/admin/merchants":{"get":{"tags":["Team (Admin)"],"operationId":"listMerchants","summary":"List merchants I belong to","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Merchants with my role"}}},"post":{"tags":["Team (Admin)"],"operationId":"createMerchant","summary":"Create a new merchant","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}}}}},"responses":{"200":{"description":"Merchant created"},"400":{"description":"Name required"}}}},"/api/admin/team":{"get":{"tags":["Team (Admin)"],"operationId":"listTeam","summary":"List team members","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Team members"}}},"post":{"tags":["Team (Admin)"],"operationId":"inviteTeamMember","summary":"Invite a team member","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","name"],"properties":{"email":{"type":"string","format":"email"},"name":{"type":"string"},"role":{"type":"string"}}}}}},"responses":{"200":{"description":"Invited"},"400":{"description":"Already invited"}}},"put":{"tags":["Team (Admin)"],"operationId":"updateTeamMember","summary":"Update a team member","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Updated"},"404":{"description":"Not found"}}},"delete":{"tags":["Team (Admin)"],"operationId":"deleteTeamMember","summary":"Remove a team member","security":[{"SessionCookie":[]}],"parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Removed"}}}},"/api/admin/profile":{"get":{"tags":["Team (Admin)"],"operationId":"getAdminProfile","summary":"Get my admin profile","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Profile (no password)"},"403":{"description":"Admin required"}}},"put":{"tags":["Team (Admin)"],"operationId":"updateAdminProfile","summary":"Update my admin profile","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"profilePicture":{"type":"string"}}}}}},"responses":{"200":{"description":"Updated"},"400":{"description":"Name required"}}}},"/api/admin/settings/profile":{"get":{"tags":["Team (Admin)"],"operationId":"getAdminSettingsProfile","summary":"Get my account settings profile","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Profile"},"401":{"description":"Not authenticated"}}},"put":{"tags":["Team (Admin)"],"operationId":"updateAdminSettingsProfile","summary":"Update my name, email and picture","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","email"],"properties":{"name":{"type":"string"},"email":{"type":"string","format":"email"},"profilePicture":{"type":"string"}}}}}},"responses":{"200":{"description":"Updated"},"400":{"description":"Name and email required, or email already in use"}}}}},"components":{"securitySchemes":{"SessionCookie":{"type":"apiKey","in":"cookie","name":"auth-token","description":"Browser session. Obtain it via POST /api/auth/login (or verify-otp), which sets an httpOnly `auth-token` JWT cookie. Used by all /api/affiliate/* and /api/admin/* endpoints."},"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Store public key (pk_store_…) for /api/track/*. Safe to ship in browser code, so server endpoints reject it. Create one under Admin → Store integrations."},"ServerKeyAuth":{"type":"http","scheme":"bearer","description":"Server secret for /api/saas/events and /api/webhook/*: a store integration secret key (sk_store_…, shown once when the integration is created) or a merchant API key (aft_…; /api/saas/events needs `write` scope). Never expose it in a browser."},"ServerKeyHeader":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Alternative to the bearer header for /api/saas/events and /api/webhook/*. Same server secret; public keys (pk_…) are rejected."},"WebhookSignature":{"type":"apiKey","in":"header","name":"X-Webhook-Signature","description":"HMAC-SHA256 of the raw request body using WEBHOOK_SECRET, hex-encoded (an optional `sha256=` prefix is accepted). Alternative auth for /api/webhook/*."},"StripeSignature":{"type":"apiKey","in":"header","name":"Stripe-Signature","description":"Sent by Stripe: `t=<unix>,v1=<HMAC-SHA256 of \"<t>.<raw body>\" keyed with the endpoint signing secret (whsec_…)>`. Save the signing secret on the Stripe integration."},"CronSecret":{"type":"apiKey","in":"header","name":"X-Cron-Secret","description":"Shared secret (CRON_SECRET) for unattended jobs such as commission maturation."}},"schemas":{"ErrorResponse":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"success":{"type":"boolean","enum":[false]}}},"SuccessResponse":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string"}}},"UserSummary":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string","format":"email"},"role":{"type":"string","enum":["ADMIN","AFFILIATE"]},"status":{"type":"string","enum":["PENDING","ACTIVE","INACTIVE","SUSPENDED"]}}},"RegisterRequest":{"type":"object","required":["email","name","password"],"properties":{"email":{"type":"string","format":"email"},"name":{"type":"string","minLength":2},"password":{"type":"string","minLength":8},"accountType":{"type":"string","enum":["partner","business"],"default":"partner","description":"`business` requests a merchant workspace that stays PENDING until approved."},"company":{"type":"string","maxLength":120,"description":"Business sign-ups only (required)."},"businessType":{"type":"string","enum":["ECOMMERCE","SAAS","OTHER"],"description":"Business sign-ups only (required). Picks the e-commerce or SaaS workspace."},"websiteUrl":{"type":"string","description":"Business sign-ups only. `https://` is added when missing."},"program":{"type":"string","description":"Partner sign-ups only: the slug of the business whose program to join (from its /join/<slug> link). Unknown or unapproved businesses return 400."}}},"LoginRequest":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string","format":"email"},"password":{"type":"string","minLength":8}}},"AuthUserResponse":{"type":"object","properties":{"success":{"type":"boolean"},"message":{"type":"string"},"user":{"$ref":"#/components/schemas/UserSummary"}}},"MeResponse":{"type":"object","properties":{"user":{"allOf":[{"$ref":"#/components/schemas/UserSummary"},{"type":"object","properties":{"hasAffiliate":{"type":"boolean"},"profilePicture":{"type":"string"}}}]}}},"TrackReferralRequest":{"type":"object","required":["referralCode"],"properties":{"referralCode":{"type":"string"},"url":{"type":"string","format":"uri","description":"Page URL where the click happened."},"referrer":{"type":"string"},"userAgent":{"type":"string"},"timestamp":{"type":"string","format":"date-time"}}},"TrackReferralResponse":{"type":"object","properties":{"success":{"type":"boolean"},"message":{"type":"string"},"affiliate":{"type":"object","properties":{"name":{"type":"string"},"code":{"type":"string"}}},"trackingContext":{"type":"object","nullable":true,"properties":{"merchantId":{"type":"string"},"storeIntegrationId":{"type":"string"},"platform":{"type":"string"}}}}},"TrackSignupRequest":{"type":"object","required":["referralCode"],"properties":{"referralCode":{"type":"string","description":"From the referral cookie (the tracker fills this in)."},"customerId":{"type":"string","description":"Your own user/account id. customerId or email is required."},"email":{"type":"string","format":"email"},"name":{"type":"string"},"metadata":{"type":"object","description":"Up to 4KB."}}},"SaasEvent":{"type":"object","required":["type"],"properties":{"id":{"type":"string","description":"Idempotency key (e.g. your invoice or event id)."},"type":{"type":"string","enum":["signup","trial_started","subscription_created","subscription_updated","subscription_canceled","payment_succeeded","payment_failed","refund"]},"occurred_at":{"type":"string","format":"date-time","description":"ISO-8601 or unix seconds. Defaults to now."},"referral_code":{"type":"string","description":"Partner referral code, typically captured at sign-up."},"coupon_code":{"type":"string","maxLength":64,"description":"Partner coupon/voucher or referral code the customer typed (alias `code`). Wins over `referral_code` when it resolves."},"customer":{"type":"object","properties":{"id":{"type":"string","description":"Your user/account id (preferred match key)."},"email":{"type":"string","format":"email"},"name":{"type":"string"},"billing_id":{"type":"string","description":"Billing-provider customer id."}}},"subscription":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["trialing","active","past_due","paused","canceled"]},"plan":{"type":"object","properties":{"id":{"type":"string","description":"Price/plan id; plans are matched by id, else name."},"name":{"type":"string"},"amount":{"type":"number"},"amount_cents":{"type":"integer"},"currency":{"type":"string"},"interval":{"type":"string","enum":["day","week","month","year"]},"interval_count":{"type":"integer"}}},"quantity":{"type":"integer"},"amount":{"type":"number","description":"Total per billing interval. Defaults to plan amount × quantity."},"amount_cents":{"type":"integer"},"currency":{"type":"string"},"interval":{"type":"string","enum":["day","week","month","year"]},"interval_count":{"type":"integer"},"trial_ends_at":{"type":"string","format":"date-time"},"current_period_start":{"type":"string","format":"date-time"},"current_period_end":{"type":"string","format":"date-time"},"cancel_reason":{"type":"string"}}},"payment":{"type":"object","properties":{"id":{"type":"string","description":"Payment/invoice id; refunds reference it."},"amount":{"type":"number"},"amount_cents":{"type":"integer"},"currency":{"type":"string"}}},"refund":{"type":"object","properties":{"payment_id":{"type":"string"},"amount":{"type":"number","description":"Defaults to the full remaining payment."},"amount_cents":{"type":"integer"},"reason":{"type":"string"}}},"metadata":{"type":"object","description":"Up to 8KB."}}},"SaasCommissionSettings":{"type":"object","description":"How partners earn on referred subscriptions. Every field is optional on update.","properties":{"commissionType":{"type":"string","enum":["PERCENTAGE","FIXED"],"default":"PERCENTAGE"},"recurringRate":{"type":"number","default":15,"description":"Percent (0–100) for PERCENTAGE; whole cents per payment for FIXED."},"duration":{"type":"string","enum":["FIRST_PAYMENT","MONTHS","LIFETIME"],"default":"MONTHS"},"durationMonths":{"type":"integer","default":12,"minimum":1,"maximum":120,"description":"Used when duration is MONTHS; counted from the first paid payment."},"signupBountyCents":{"type":"integer","default":0,"minimum":0,"description":"One-time bounty per referred sign-up (server-verified sources only)."},"trialBountyCents":{"type":"integer","default":0,"minimum":0,"description":"One-time bounty per referred trial."},"holdDays":{"type":"integer","default":30,"minimum":0,"maximum":365,"description":"Days a commission stays PENDING before it can be paid."},"clawbackOnRefund":{"type":"boolean","default":true}}},"SaasPlanRequest":{"type":"object","required":["name","amount","interval"],"properties":{"name":{"type":"string","maxLength":120},"amount":{"type":"number","minimum":0,"description":"Price per billing interval in major units."},"currency":{"type":"string","description":"Defaults to the workspace currency."},"interval":{"type":"string","enum":["DAY","WEEK","MONTH","YEAR"]},"intervalCount":{"type":"integer","default":1,"minimum":1,"maximum":36},"trialDays":{"type":"integer","default":0,"minimum":0,"maximum":365},"externalPriceId":{"type":"string","description":"Billing-provider price id (e.g. Stripe price_…); unique per workspace."},"commissionRateOverride":{"type":"number","nullable":true,"minimum":0,"maximum":100,"description":"Percent; overrides the program rate for this plan."}}},"BillingOverview":{"type":"object","description":"The workspace’s Affiliatops plan. Money is in US cents.","properties":{"configured":{"type":"boolean","description":"STRIPE_SECRET_KEY is set."},"webhookConfigured":{"type":"boolean","description":"STRIPE_WEBHOOK_SECRET is set."},"canManage":{"type":"boolean","description":"Owner/Admin can start, change or cancel the plan."},"requestedPlan":{"type":"string","nullable":true,"enum":["starter","growth","scale"],"description":"Plan picked at signup."},"requestedBilling":{"type":"string","nullable":true,"enum":["monthly","yearly"]},"trialAvailable":{"type":"boolean"},"trialDays":{"type":"integer"},"subscription":{"type":"object","nullable":true,"properties":{"plan":{"type":"string","nullable":true},"billing":{"type":"string","nullable":true},"status":{"type":"string","enum":["none","trialing","active","past_due","canceled","incomplete","incomplete_expired","unpaid","paused"]},"trialEndsAt":{"type":"string","format":"date-time","nullable":true},"currentPeriodEnd":{"type":"string","format":"date-time","nullable":true},"cancelAtPeriodEnd":{"type":"boolean"},"canceledAt":{"type":"string","format":"date-time","nullable":true},"amountCents":{"type":"integer","nullable":true},"currency":{"type":"string","nullable":true}}},"usage":{"type":"object","properties":{"activePartners":{"type":"integer"},"teamMembers":{"type":"integer"},"partnerLimit":{"type":"integer","nullable":true},"teamLimit":{"type":"integer","nullable":true}}},"paymentMethod":{"type":"object","nullable":true,"properties":{"brand":{"type":"string"},"last4":{"type":"string"},"expMonth":{"type":"integer"},"expYear":{"type":"integer"}}},"invoices":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"number":{"type":"string","nullable":true},"status":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"amountDueCents":{"type":"integer"},"amountPaidCents":{"type":"integer"},"currency":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"periodStart":{"type":"string","format":"date-time","nullable":true},"periodEnd":{"type":"string","format":"date-time","nullable":true},"hostedInvoiceUrl":{"type":"string","nullable":true},"invoicePdf":{"type":"string","nullable":true}}}},"upcoming":{"type":"object","nullable":true,"properties":{"amountDueCents":{"type":"integer"},"currency":{"type":"string"},"nextPaymentAt":{"type":"string","format":"date-time","nullable":true}}}}},"WorkspaceInfo":{"type":"object","properties":{"merchantId":{"type":"string"},"merchantName":{"type":"string"},"businessType":{"type":"string","enum":["ECOMMERCE","SAAS","OTHER"],"nullable":true},"mode":{"type":"string","enum":["ecommerce","saas"]},"onboardingCompleted":{"type":"boolean"},"defaultCurrency":{"type":"string"},"market":{"type":"string"},"websiteUrl":{"type":"string","nullable":true},"membershipRole":{"type":"string","enum":["OWNER","ADMIN","ANALYST"]}}},"SaasEventResult":{"type":"object","properties":{"index":{"type":"integer"},"status":{"type":"string","enum":["processed","duplicate","ignored","error"]},"eventId":{"type":"string"},"customerId":{"type":"string","nullable":true},"subscriptionId":{"type":"string","nullable":true},"affiliateId":{"type":"string","nullable":true},"commissionIds":{"type":"array","items":{"type":"string"}},"message":{"type":"string"}}},"TrackConversionRequest":{"type":"object","description":"Send referralCode, coupon_code (or code), or both.","properties":{"referralCode":{"type":"string","description":"From the tracker cookie."},"coupon_code":{"type":"string","maxLength":64,"description":"Partner coupon/voucher or referral code the buyer typed. Wins over referralCode when it resolves."},"code":{"type":"string","description":"Alias of coupon_code."},"customerEmail":{"type":"string","format":"email"},"customerName":{"type":"string"},"amount":{"type":"number","description":"Major units (e.g. 99.99). Converted to amountCents server-side."},"currency":{"type":"string","default":"IDR"},"orderId":{"type":"string","description":"Your order id. A retry with the same orderId returns the conversion already recorded."},"url":{"type":"string","format":"uri"},"metadata":{"type":"object"},"timestamp":{"type":"string","format":"date-time"}}},"TrackConversionResponse":{"type":"object","properties":{"success":{"type":"boolean"},"duplicate":{"type":"boolean","description":"True when this orderId was already recorded; nothing new was created."},"message":{"type":"string"},"conversion":{"type":"object","properties":{"id":{"type":"string"},"amount":{"type":"number"},"currency":{"type":"string"}}},"affiliate":{"type":"object","properties":{"name":{"type":"string"},"code":{"type":"string"}}},"attributionMethod":{"type":"string","enum":["coupon","referral_code"]}}},"AnalyticsEvent":{"type":"object","required":["visitorId","sessionId","eventName"],"properties":{"visitorId":{"type":"string"},"sessionId":{"type":"string"},"eventName":{"type":"string","maxLength":64,"description":"e.g. page_view, product_view, add_to_cart, purchase"},"occurredAt":{"type":"string","format":"date-time"},"pageUrl":{"type":"string","maxLength":2048},"pageTitle":{"type":"string"},"referrer":{"type":"string"},"productId":{"type":"string"},"productName":{"type":"string"},"orderId":{"type":"string"},"amount":{"type":"number"},"amountCents":{"type":"integer"},"currency":{"type":"string"},"referralCode":{"type":"string"},"metadata":{"type":"object","description":"Arbitrary JSON, max 8KB serialized. Raw emails are redacted."}}},"AnalyticsBatchRequest":{"type":"object","required":["events"],"properties":{"events":{"type":"array","minItems":1,"maxItems":50,"items":{"$ref":"#/components/schemas/AnalyticsEvent"}}}},"ConversionWebhook":{"type":"object","required":["event_type","customer_email"],"properties":{"event_type":{"type":"string","enum":["SIGNUP","PURCHASE","TRIAL","LEAD"]},"external_id":{"type":"string","description":"Your id for this conversion (order, invoice or lead id). A retry with the same id returns the conversion already recorded."},"amount_cents":{"type":"integer","description":"Minor units (cents)."},"currency":{"type":"string","default":"IDR"},"customer_email":{"type":"string","format":"email"},"referral_code":{"type":"string","description":"Used to attribute the conversion."},"coupon_code":{"type":"string","maxLength":64,"description":"Partner coupon/voucher or referral code the buyer typed (alias `code`). Wins over referral_code when it resolves; needs a merchant-scoped key."},"code":{"type":"string","description":"Alias of coupon_code."},"attribution_key":{"type":"string"},"event_metadata":{"type":"object"}}},"ConversionWebhookResponse":{"type":"object","properties":{"success":{"type":"boolean"},"duplicate":{"type":"boolean","description":"True when this external_id was already recorded; nothing new was created."},"message":{"type":"string"},"attributed":{"type":"boolean"},"attributionMethod":{"type":"string","enum":["coupon","referral_code","attribution_key","none"]},"commission":{"type":"object"},"conversion":{"type":"object"}}},"RefundWebhook":{"type":"object","required":["customer_email"],"properties":{"customer_email":{"type":"string","format":"email"},"order_id":{"type":"string","description":"The refunded order: the external_id sent to /api/webhook/conversion, or the tracker orderId. Reverses only that order. With signature auth, also send referral_code (or the email must match the order)."},"referral_code":{"type":"string"},"amount_cents":{"type":"integer","description":"Refunded amount in minor units. Omit to refund whatever is left of the order. Applies when a single order matched."},"reason":{"type":"string"},"external_id":{"type":"string","description":"The payment provider’s refund id. Repeats with the same id are ignored, so retries are safe."}}},"ReferralRequest":{"type":"object","required":["leadName","leadEmail"],"properties":{"leadName":{"type":"string","minLength":2},"leadEmail":{"type":"string","format":"email"},"company":{"type":"string"},"notes":{"type":"string"},"estimatedValue":{"type":"number","minimum":0}}},"AffiliateCreateRequest":{"type":"object","required":["name","email"],"properties":{"name":{"type":"string","minLength":2},"email":{"type":"string","format":"email"},"password":{"type":"string","minLength":8,"description":"Optional — auto-generated if omitted."}}},"UpdateProfileRequest":{"type":"object","properties":{"name":{"type":"string"},"email":{"type":"string","format":"email"},"company":{"type":"string"},"country":{"type":"string"},"paymentMethod":{"type":"string"},"paymentEmail":{"type":"string","format":"email"}}},"CommissionRuleRequest":{"type":"object","required":["name","type","value"],"properties":{"name":{"type":"string"},"type":{"type":"string","enum":["PERCENTAGE","FIXED"]},"value":{"type":"number","description":"Percent (for PERCENTAGE) or cents (for FIXED)."},"isDefault":{"type":"boolean"},"conditions":{"type":"object"}}},"PayoutRequest":{"type":"object","required":["affiliateId","commissionIds"],"properties":{"affiliateId":{"type":"string"},"commissionIds":{"type":"array","items":{"type":"string"},"minItems":1},"method":{"type":"string"},"notes":{"type":"string"}}},"PayoutUpdateRequest":{"type":"object","required":["id"],"properties":{"id":{"type":"string"},"status":{"type":"string","enum":["PENDING","PROCESSING","COMPLETED","FAILED"]},"method":{"type":"string"},"notes":{"type":"string"}}},"ProgramSettingsRequest":{"type":"object","required":["productName","programName","websiteUrl","currency","minPayoutCents","cookieDuration"],"properties":{"productName":{"type":"string","minLength":1},"programName":{"type":"string","minLength":1},"websiteUrl":{"type":"string","format":"uri"},"currency":{"type":"string","minLength":3,"maxLength":3},"minPayoutCents":{"type":"integer","minimum":0},"cookieDuration":{"type":"integer","minimum":1,"description":"Attribution window in days."}}}}}}