{"openapi":"3.0.3","info":{"title":"Sendeet API","version":"1.0.0","description":"Sendeet WhatsApp campaign platform API"},"servers":[{"url":"http://localhost:4000/api/v1","description":"Local development"}],"components":{"securitySchemes":{"cookieAuth":{"type":"apiKey","in":"cookie","name":"token"}}},"paths":{"/admin/businesses":{"get":{"summary":"Get all businesses","tags":["Admin"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"pageSize","schema":{"type":"integer"}},{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"verified","schema":{"type":"string","enum":["pending","accepted","rejected"]}}],"responses":{"200":{"description":"Paginated business list"},"403":{"description":"Forbidden"}}}},"/admin/businesses/{id}":{"get":{"summary":"Get a single business with full details","tags":["Admin"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Business with account, wallet, plan, and campaign/contact counts"},"404":{"description":"Not found"}}}},"/admin/businesses/{id}/verify":{"patch":{"summary":"Accept or reject a business verification request","description":"Updates businessVerified status and creates a Notification for the business.","tags":["Admin"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["accepted","rejected"]},"reason":{"type":"string","description":"Rejection reason shown in the notification"}}}}}},"responses":{"200":{"description":"Business verification updated and notification sent"},"400":{"description":"Invalid status"}}}},"/admin/businesses/{id}/promote-affiliate":{"post":{"summary":"Promote a business to affiliate type","description":"Sets type to affiliate, assigns percentage, and generates a referral code.","tags":["Admin"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"affiliatePercentage":{"type":"integer","default":10}}}}}},"responses":{"200":{"description":"Business promoted to affiliate"}}}},"/admin/accounts":{"get":{"summary":"Get all business accounts","tags":["Admin"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"pageSize","schema":{"type":"integer"}},{"in":"query","name":"search","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated account list"}}}},"/admin/settings":{"get":{"summary":"Get platform AppSettings (credit rates, thresholds)","description":"Falls back to the DEFAULT_CREDIT_RATES constant if no settings have been saved yet.","tags":["Admin"],"security":[{"cookieAuth":[]}],"responses":{"200":{"description":"AppSettings record or default rates"}}},"post":{"summary":"Create or update platform AppSettings","description":"Superadmin only. Upserts the singleton AppSettings record.","tags":["Admin"],"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"marketingRate":{"type":"number"},"utilityRate":{"type":"number"},"serviceRate":{"type":"number"},"authenticationRate":{"type":"number"},"pricePerCredit":{"type":"number"},"lowCreditThreshold":{"type":"number"},"affiliatePercentage":{"type":"integer"}}}}}},"responses":{"200":{"description":"Settings saved"},"403":{"description":"Forbidden"}}}},"/affiliate/referrals":{"get":{"summary":"Get businesses referred by this affiliate","tags":["Affiliate"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"pageSize","schema":{"type":"integer"}}],"responses":{"200":{"description":"Paginated list of referred businesses"},"403":{"description":"Not an affiliate account"}}}},"/affiliate/earnings":{"get":{"summary":"Get affiliate earnings summary","description":"Returns current wallet balance, total earned, and total withdrawn.","tags":["Affiliate"],"security":[{"cookieAuth":[]}],"responses":{"200":{"description":"Earnings summary"},"403":{"description":"Not an affiliate account"}}}},"/affiliate/transactions":{"get":{"summary":"Get affiliate transaction history","tags":["Affiliate"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"pageSize","schema":{"type":"integer"}}],"responses":{"200":{"description":"Paginated transaction list"}}}},"/affiliate/bank-accounts":{"get":{"summary":"Get saved bank accounts for the affiliate business","tags":["Affiliate"],"security":[{"cookieAuth":[]}],"responses":{"200":{"description":"List of bank accounts"}}},"post":{"summary":"Add a bank account","tags":["Affiliate"],"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["bankName","accountNumber","accountName","code"],"properties":{"bankName":{"type":"string"},"accountNumber":{"type":"string"},"accountName":{"type":"string"},"code":{"type":"string","description":"Paystack bank code"}}}}}},"responses":{"201":{"description":"Bank account added"},"400":{"description":"Missing required fields"}}}},"/affiliate/bank-accounts/verify":{"get":{"summary":"Verify a bank account via Paystack","description":"Resolves the account name from Paystack before saving. Call this before POST /bank-accounts.","tags":["Affiliate"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"query","name":"accountNumber","required":true,"schema":{"type":"string"}},{"in":"query","name":"bankCode","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Resolved account name and details from Paystack"},"400":{"description":"Missing parameters"}}}},"/affiliate/withdraw":{"post":{"summary":"Request a withdrawal to a saved bank account","description":"Deducts from the wallet immediately, initiates a Paystack transfer, and creates a\npending Transaction record. The transaction is updated to successful/failed when\nPaystack fires the transfer.success or transfer.failed webhook.\n","tags":["Affiliate"],"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["bankAccountId","amount"],"properties":{"bankAccountId":{"type":"string"},"amount":{"type":"number","description":"Amount in NGN"}}}}}},"responses":{"200":{"description":"Withdrawal initiated. Returns the pending Transaction record."},"400":{"description":"Insufficient balance or missing fields"},"403":{"description":"Not an affiliate account"}}}},"/auth/sign-up":{"post":{"summary":"Register a new account","description":"Creates a new account and sends a welcome email. Returns access and refresh tokens.","tags":["Auth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","email","whatsappNumber"],"properties":{"name":{"type":"string"},"email":{"type":"string","format":"email"},"whatsappNumber":{"type":"string"}}}}}},"responses":{"201":{"description":"Account created successfully. Returns user object, accessToken and refreshToken."},"400":{"description":"Missing fields or email already registered"},"500":{"description":"Internal server error"}}}},"/auth/sign-in":{"post":{"summary":"Sign in to an existing account","description":"Authenticates with email and password. Returns access and refresh tokens.","tags":["Auth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string","format":"email"},"password":{"type":"string","format":"password"}}}}}},"responses":{"200":{"description":"Signed in successfully. Returns user object, accessToken and refreshToken."},"400":{"description":"Missing email or password"},"401":{"description":"Invalid credentials"},"500":{"description":"Internal server error"}}}},"/auth/sign-out":{"post":{"summary":"Sign out the current user","description":"Clears the auth cookie.","tags":["Auth"],"security":[{"cookieAuth":[]}],"responses":{"200":{"description":"Signed out successfully"},"401":{"description":"Unauthorized"},"500":{"description":"Internal server error"}}}},"/auth/set-password":{"post":{"summary":"Set a password for the authenticated account","description":"Used when an account was created without a password (e.g. via OTP flow) and needs one set.","tags":["Auth"],"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["password"],"properties":{"password":{"type":"string","format":"password"}}}}}},"responses":{"200":{"description":"Password set successfully"},"400":{"description":"Missing password or bad request"},"401":{"description":"Unauthorized"},"500":{"description":"Internal server error"}}}},"/auth/refresh-token":{"post":{"summary":"Refresh the access token","description":"Accepts a valid refresh token and returns a new access token.","tags":["Auth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["refreshToken"],"properties":{"refreshToken":{"type":"string"}}}}}},"responses":{"200":{"description":"Returns a new accessToken"},"401":{"description":"Invalid or missing refresh token"},"500":{"description":"Internal server error"}}}},"/auth/send-code":{"post":{"summary":"Send a verification OTP to an email address","description":"Generates a 6-digit OTP and emails it to the provided address. Creates or updates the verification token record.","tags":["Auth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"}}}}}},"responses":{"200":{"description":"OTP sent successfully"},"400":{"description":"Missing email"},"500":{"description":"Internal server error"}}}},"/auth/verify-code":{"post":{"summary":"Verify an OTP code","description":"Checks the code against the stored verification token without consuming it. Used to validate the code before proceeding to reset password.","tags":["Auth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["code"],"properties":{"code":{"type":"string","example":"482910"}}}}}},"responses":{"200":{"description":"OTP is valid"},"400":{"description":"Missing code"},"401":{"description":"Invalid or expired OTP"},"500":{"description":"Internal server error"}}}},"/auth/check-email":{"post":{"summary":"Check if an email is already registered","description":"Returns 400 if the email exists, 200 if it is available.","tags":["Auth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"}}}}}},"responses":{"200":{"description":"Email is available"},"400":{"description":"Email already registered or missing"},"500":{"description":"Internal server error"}}}},"/auth/verify-email":{"post":{"summary":"Verify the authenticated user's email with an OTP","description":"Consumes the OTP for the authenticated user's email and marks it as verified.","tags":["Auth"],"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["code"],"properties":{"code":{"type":"string","example":"482910"}}}}}},"responses":{"200":{"description":"Email verified successfully"},"400":{"description":"Missing code"},"401":{"description":"Invalid OTP or unauthorized"},"500":{"description":"Internal server error"}}}},"/auth/forgot-password":{"post":{"summary":"Request a password reset OTP","description":"Sends a password reset OTP to the provided email if an account exists.","tags":["Auth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"}}}}}},"responses":{"200":{"description":"Reset OTP sent if account exists"},"400":{"description":"Missing email or account not found"},"500":{"description":"Internal server error"}}}},"/auth/reset-password":{"post":{"summary":"Reset password using an OTP","description":"Validates the OTP and sets a new password. Rejects if the new password is the same as the current one.","tags":["Auth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","code","password"],"properties":{"email":{"type":"string","format":"email"},"code":{"type":"string","example":"482910"},"password":{"type":"string","format":"password"}}}}}},"responses":{"200":{"description":"Password reset successfully"},"400":{"description":"Missing fields, invalid OTP, or same password used"},"401":{"description":"Invalid OTP"},"500":{"description":"Internal server error"}}}},"/auth/change-password":{"post":{"summary":"Change password for the authenticated user","description":"Requires the current password. Rejects if the new password is the same as the current one.","tags":["Auth"],"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["password","newPassword"],"properties":{"password":{"type":"string","format":"password","description":"Current password"},"newPassword":{"type":"string","format":"password","description":"New password"}}}}}},"responses":{"200":{"description":"Password changed successfully"},"400":{"description":"Missing fields or same password used"},"401":{"description":"Current password incorrect or unauthorized"},"500":{"description":"Internal server error"}}}},"/business":{"get":{"summary":"Get the authenticated user's business profile","description":"Returns the business linked to the current account. Sensitive credential fields are masked as \"true\" or \"false\".","tags":["Business"],"security":[{"cookieAuth":[]}],"responses":{"200":{"description":"Business profile with wallet and active plan"},"401":{"description":"Unauthorized"},"404":{"description":"Business not found"}}},"patch":{"summary":"Update the business profile","description":"Accepts company details. Only whitelisted fields are updated (companyName, companyLogo, address, website, businessRegistrationNumber, countryCode, industry, noOfEmployees, noOfSubscribers).","tags":["Business"],"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"companyName":{"type":"string"},"companyLogo":{"type":"string"},"address":{"type":"string"},"website":{"type":"string"},"businessRegistrationNumber":{"type":"string"},"countryCode":{"type":"string","example":"NG"},"industry":{"type":"string"},"noOfEmployees":{"type":"integer"},"noOfSubscribers":{"type":"integer"}}}}}},"responses":{"200":{"description":"Profile updated"},"401":{"description":"Unauthorized"},"404":{"description":"Business not found"}}}},"/business/verification":{"post":{"summary":"Submit the business profile for admin verification","description":"Notifies all admin accounts that this business is ready to be reviewed. Has no effect if already verified.","tags":["Business"],"security":[{"cookieAuth":[]}],"responses":{"200":{"description":"Verification request submitted"},"400":{"description":"Business is already verified"},"401":{"description":"Unauthorized"},"404":{"description":"Business not found"}}}},"/business/referral-code":{"post":{"summary":"Generate (or regenerate) a referral code for an affiliate business","description":"Only available to businesses with type \"affiliate\". Generates a new 10-character uppercase referral code.","tags":["Business"],"security":[{"cookieAuth":[]}],"responses":{"200":{"description":"Referral code generated. Returns updated business profile."},"401":{"description":"Unauthorized"},"403":{"description":"Not an affiliate account"},"404":{"description":"Business not found"}}}},"/business/provider":{"post":{"summary":"Set WhatsApp provider credentials","description":"Saves Gupshup credentials for the business. Business must be verified. Passwords are encrypted at rest (AES-256-GCM) and returned as \"true\"/\"false\" in all responses.","tags":["Business"],"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["provider","wabaNumber","gupshupHSMID","gupshupHSMPassword"],"properties":{"provider":{"type":"string","example":"gupShup"},"wabaNumber":{"type":"string"},"wabaProviderId":{"type":"string"},"gupshupHSMID":{"type":"string"},"gupshupHSMPassword":{"type":"string"},"gupshupDashboardLogin":{"type":"string"},"gupshupDashboardPassword":{"type":"string"},"gupshupTwowayLogin":{"type":"string"},"gupshupTwowayPassword":{"type":"string"}}}}}},"responses":{"200":{"description":"Credentials saved"},"400":{"description":"Missing required fields"},"401":{"description":"Unauthorized"},"403":{"description":"Business not verified"},"404":{"description":"Business not found"}}}},"/campaigns":{"get":{"summary":"List all campaigns for the business","tags":["Campaigns"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"pageSize","schema":{"type":"integer"}},{"in":"query","name":"search","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated campaign list"},"401":{"description":"Unauthorized"}}},"post":{"summary":"Create a campaign","description":"Creates a campaign draft or publishes it immediately.\n- Omit `paymentMethod` to save as draft (no billing, no send).\n- Include `paymentMethod` (\"credit\" or \"plan\") to publish. Immediate campaigns are sent right away; scheduled campaigns wait for the cron job.\n- `scheduledAt` must be a UTC ISO 8601 datetime. Client is responsible for converting local time to UTC before sending.\n- `isScheduled: true` requires `scheduledAt`.\n","tags":["Campaigns"],"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","message","sendToAllContact","isScheduled"],"properties":{"name":{"type":"string"},"description":{"type":"string"},"messageType":{"type":"string","enum":["marketing","utility","authentication"]},"sendToAllContact":{"type":"boolean"},"selectedContactId":{"type":"array","items":{"type":"string"}},"message":{"type":"string"},"uploadFileUrl":{"type":"string"},"header":{"type":"string"},"isScheduled":{"type":"boolean"},"scheduledAt":{"type":"string","format":"date-time","description":"UTC datetime for scheduled delivery"},"timeZone":{"type":"string","description":"IANA timezone string, stored for display only (e.g. \"Africa/Lagos\")"},"paymentMethod":{"type":"string","enum":["credit","plan"]}}}}}},"responses":{"201":{"description":"Campaign created"},"400":{"description":"Validation error or insufficient credits"},"401":{"description":"Unauthorized"},"403":{"description":"Business not verified"}}}},"/campaigns/stats":{"get":{"summary":"Get campaign summary stats for the business","tags":["Campaigns"],"security":[{"cookieAuth":[]}],"responses":{"200":{"description":"Returns total, sent, pending, scheduled, and draft counts"},"401":{"description":"Unauthorized"}}}},"/campaigns/{id}":{"get":{"summary":"Get a single campaign","tags":["Campaigns"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Campaign record"},"401":{"description":"Unauthorized"},"404":{"description":"Not found"}}},"patch":{"summary":"Update a draft campaign","description":"Only unpublished, unsent campaigns can be edited.","tags":["Campaigns"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Campaign updated"},"400":{"description":"Campaign already published or sent"},"401":{"description":"Unauthorized"}}},"delete":{"summary":"Delete a campaign","tags":["Campaigns"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Campaign deleted"},"401":{"description":"Unauthorized"}}}},"/campaigns/{id}/publish":{"post":{"summary":"Publish a draft campaign","description":"Bills the business and triggers immediate send or schedules based on `isScheduled`.","tags":["Campaigns"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["paymentMethod"],"properties":{"paymentMethod":{"type":"string","enum":["credit","plan"]}}}}}},"responses":{"200":{"description":"Campaign published"},"400":{"description":"Already published, already sent, or insufficient credits"},"401":{"description":"Unauthorized"}}}},"/campaigns/qr-code":{"get":{"summary":"Generate a WhatsApp opt-in QR code for the business","description":"Returns a deep link to wa.me and a base64-encoded QR code image. Business must be verified and have a WABA number configured.","tags":["Campaigns"],"security":[{"cookieAuth":[]}],"responses":{"200":{"description":"QR code generated. Returns link and imageBase64 (PNG data URL)."},"400":{"description":"WABA number not configured"},"401":{"description":"Unauthorized"},"403":{"description":"Business not verified"}}}},"/contacts":{"get":{"summary":"List all contacts for the business","tags":["Contacts"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"pageSize","schema":{"type":"integer"}},{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"optIn","schema":{"type":"boolean"}}],"responses":{"200":{"description":"Paginated contact list"},"401":{"description":"Unauthorized"}}},"post":{"summary":"Create a new contact","tags":["Contacts"],"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["firstName","lastName","phoneNumber"],"properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"phoneNumber":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"optIn":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Contact created"},"400":{"description":"Missing required fields"},"401":{"description":"Unauthorized"}}}},"/contacts/export":{"get":{"summary":"Export contacts as CSV or XLSX","description":"Streams the file directly as a download. Use the X-Record-Count response header to get the total row count.","tags":["Contacts"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"query","name":"format","schema":{"type":"string","enum":["csv","xlsx"],"default":"csv"}},{"in":"query","name":"tag","schema":{"type":"string"},"description":"Filter by a specific tag"},{"in":"query","name":"optIn","schema":{"type":"boolean"},"description":"Filter by opt-in status"}],"responses":{"200":{"description":"File download (text/csv or application/xlsx)"},"401":{"description":"Unauthorized"}}}},"/contacts/import-file":{"post":{"summary":"Import contacts from an uploaded CSV or XLSX file URL","description":"Use this when the frontend cannot parse the file itself.\nFlow: get a presigned URL from POST /upload/presigned-url, upload the file to S3, then\npass the resulting fileUrl here. The server fetches the file, detects the format from the\nfile extension (.csv or .xlsx), parses it, and bulk-inserts. Returns the same report as\nthe JSON import endpoint.\n","tags":["Contacts"],"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["fileUrl"],"properties":{"fileUrl":{"type":"string","description":"Public or presigned S3 URL of the uploaded CSV or XLSX file"}}}}}},"responses":{"200":{"description":"Import complete. Returns imported, duplicatesSkipped, and failed counts."},"400":{"description":"Missing fileUrl"},"401":{"description":"Unauthorized"}}}},"/contacts/bulk-delete":{"post":{"summary":"Delete multiple contacts by ID","tags":["Contacts"],"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["ids"],"properties":{"ids":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"Contacts deleted"},"400":{"description":"Invalid or empty ids array"},"401":{"description":"Unauthorized"}}}},"/contacts/import":{"post":{"summary":"Bulk import contacts from a pre-parsed array","description":"The frontend parses the CSV or Excel file (e.g. with PapaParse or SheetJS) and sends the\nextracted rows as a JSON array. The server validates each row individually, inserts valid\nones, and silently skips duplicates (same phoneNumber + business). Returns a summary of\nhow many were imported, how many were skipped as duplicates, and details on any rows that\nfailed validation.\n","tags":["Contacts"],"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["contacts"],"properties":{"contacts":{"type":"array","items":{"type":"object","required":["firstName","lastName","phoneNumber"],"properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"phoneNumber":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"optIn":{"type":"boolean"}}}}}}}}},"responses":{"200":{"description":"Import complete. Returns imported count, duplicatesSkipped count, and failed row details.","content":{"application/json":{"example":{"imported":95,"duplicatesSkipped":3,"failed":[{"row":2,"reason":"Missing phoneNumber"},{"row":7,"reason":"Missing firstName"}]}}}},"400":{"description":"contacts array is missing or empty"},"401":{"description":"Unauthorized"}}}},"/contacts/{id}":{"get":{"summary":"Get a single contact by ID","tags":["Contacts"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Contact record"},"401":{"description":"Unauthorized"},"404":{"description":"Not found"}}},"patch":{"summary":"Update a contact","tags":["Contacts"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"phoneNumber":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"Contact updated"},"401":{"description":"Unauthorized"},"404":{"description":"Not found"}}},"delete":{"summary":"Delete a contact","tags":["Contacts"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Contact deleted"},"401":{"description":"Unauthorized"}}}},"/exchange-rates/countries":{"get":{"summary":"Get the full list of supported countries","description":"Returns the static list of countries from the server constant. No database call.","tags":["ExchangeRates"],"responses":{"200":{"description":"Array of country name and ISO code pairs"}}}},"/exchange-rates":{"get":{"summary":"Get all configured exchange rates","tags":["ExchangeRates"],"responses":{"200":{"description":"All exchange rate records"}}},"post":{"summary":"Admin - create or update an exchange rate","tags":["ExchangeRates"],"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["countryCode","currency","rate"],"properties":{"countryCode":{"type":"string","example":"NG"},"currency":{"type":"string","example":"NGN"},"rate":{"type":"number","example":1550}}}}}},"responses":{"200":{"description":"Exchange rate created or updated"},"400":{"description":"Missing required fields"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}}}},"/exchange-rates/{code}":{"get":{"summary":"Get exchange rate for a specific country code","tags":["ExchangeRates"],"parameters":[{"in":"path","name":"code","required":true,"schema":{"type":"string","example":"NG"}}],"responses":{"200":{"description":"Exchange rate record"},"404":{"description":"Not configured for this country"}}},"delete":{"summary":"Admin - delete an exchange rate","tags":["ExchangeRates"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"path","name":"code","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Exchange rate deleted"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}}}},"/notifications":{"get":{"summary":"Get all notifications for the business","description":"Returns paginated notifications along with an unread count.","tags":["Notifications"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"pageSize","schema":{"type":"integer"}}],"responses":{"200":{"description":"Paginated notification list with unread count"},"401":{"description":"Unauthorized"}}}},"/notifications/settings":{"get":{"summary":"Get notification settings for the business","description":"Returns a flat key-value object of notification preferences.","tags":["Notifications"],"security":[{"cookieAuth":[]}],"responses":{"200":{"description":"Notification settings"},"401":{"description":"Unauthorized"}}},"patch":{"summary":"Update notification settings","description":"Accepts a flat object of key-value pairs (e.g. { \"campaignCompletionPush\": \"false\" }).","tags":["Notifications"],"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"}}}}},"responses":{"200":{"description":"Settings updated"},"401":{"description":"Unauthorized"}}}},"/notifications/read-all":{"post":{"summary":"Mark all notifications as read","tags":["Notifications"],"security":[{"cookieAuth":[]}],"responses":{"200":{"description":"All notifications marked as read"},"401":{"description":"Unauthorized"}}}},"/notifications/{id}/read":{"post":{"summary":"Mark a single notification as read","tags":["Notifications"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Notification marked as read"},"401":{"description":"Unauthorized"}}}},"/oauth/google-sign-in":{"post":{"summary":"Sign in or register with Google","description":"Exchanges a Google authorization code for an access token, fetches the user's\nGoogle profile, then either creates a new account or updates the existing one.\nOn success, sets an httpOnly auth cookie and returns the user object with\naccess and refresh tokens.\n\nIf the account is new, a welcome email is sent automatically.\n","tags":["OAuth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["code"],"properties":{"code":{"type":"string","description":"Google authorization code returned from the OAuth consent screen"}}}}}},"responses":{"200":{"description":"Signed in successfully. Sets httpOnly auth cookie.\nReturns user object (without role), accessToken, refreshToken, and isAdmin flag.\n","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"},"avatarUrl":{"type":"string"},"isAdmin":{"type":"boolean"}}},"accessToken":{"type":"string"},"refreshToken":{"type":"string"}}}}}},"500":{"description":"Internal server error or Google OAuth failure"}}}},"/payments/credit-pricing":{"get":{"summary":"Get the NGN price for a given number of credits","tags":["Payments"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"query","name":"totalCredit","required":true,"schema":{"type":"integer"},"description":"Number of credits to price"}],"responses":{"200":{"description":"Returns priceUSD, priceNGN, currency, and per-message rates"},"400":{"description":"Missing totalCredit"},"401":{"description":"Unauthorized"}}}},"/payments/credit-link":{"post":{"summary":"Generate a Paystack payment link for a credit purchase","description":"Creates a pending Transaction record and returns a Paystack checkout URL.","tags":["Payments"],"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["totalCredit"],"properties":{"totalCredit":{"type":"integer"}}}}}},"responses":{"200":{"description":"Paystack authorization URL and pricing breakdown"},"400":{"description":"Missing totalCredit"},"401":{"description":"Unauthorized"}}}},"/payments/plan-link":{"post":{"summary":"Generate a Paystack payment link for a plan purchase","description":"Creates a pending Transaction record and returns a Paystack checkout URL.","tags":["Payments"],"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["planId"],"properties":{"planId":{"type":"string"}}}}}},"responses":{"200":{"description":"Paystack authorization URL and plan details"},"400":{"description":"Missing planId or plan not found"},"401":{"description":"Unauthorized"}}}},"/payments/verify":{"post":{"summary":"Manually verify a Paystack transaction","description":"Use this after the Paystack redirect. Calls the Paystack verify API and credits the wallet or assigns the plan. Idempotent — safe to call multiple times for the same reference.","tags":["Payments"],"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reference"],"properties":{"reference":{"type":"string"}}}}}},"responses":{"200":{"description":"Transaction verified and processed"},"400":{"description":"Transaction not successful or already processed"},"401":{"description":"Unauthorized"}}}},"/payments/wallet":{"get":{"summary":"Get the business wallet balance","tags":["Payments"],"security":[{"cookieAuth":[]}],"responses":{"200":{"description":"Wallet record with current balance"},"401":{"description":"Unauthorized"}}}},"/payments/transactions":{"get":{"summary":"Get the business transaction history","tags":["Payments"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"pageSize","schema":{"type":"integer"}}],"responses":{"200":{"description":"Paginated transaction list"},"401":{"description":"Unauthorized"}}}},"/plans":{"get":{"summary":"Get all active plans","description":"Returns active plans. If the business has a country set, prices are converted to local currency.","tags":["Plans"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"pageSize","schema":{"type":"integer"}},{"in":"query","name":"search","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated plan list with optional converted pricing"},"401":{"description":"Unauthorized"}}}},"/plans/my-plan":{"get":{"summary":"Get the current active plan for the authenticated business","tags":["Plans"],"security":[{"cookieAuth":[]}],"responses":{"200":{"description":"Active UserPlan with plan details, or null if none"},"401":{"description":"Unauthorized"}}}},"/plans/{id}":{"get":{"summary":"Get a single plan by ID","tags":["Plans"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Plan record"},"401":{"description":"Unauthorized"},"404":{"description":"Not found"}}}},"/plans/admin/all":{"get":{"summary":"Admin - get all plans including inactive","tags":["Plans"],"security":[{"cookieAuth":[]}],"responses":{"200":{"description":"All plans"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}}}},"/plans/admin":{"post":{"summary":"Admin - create a new plan","tags":["Plans"],"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","duration","marketingRate","utilityRate","serviceRate","authenticationRate","freeCredit","price"],"properties":{"name":{"type":"string"},"duration":{"type":"integer","description":"Duration in days"},"description":{"type":"string"},"marketingRate":{"type":"number"},"utilityRate":{"type":"number"},"serviceRate":{"type":"number"},"authenticationRate":{"type":"number"},"freeCredit":{"type":"integer"},"price":{"type":"integer","description":"Price in USD"}}}}}},"responses":{"201":{"description":"Plan created"},"400":{"description":"Missing required fields"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}}}},"/plans/admin/{id}":{"patch":{"summary":"Admin - update a plan","tags":["Plans"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Plan updated"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}}}},"/plans/admin/{id}/deactivate":{"post":{"summary":"Admin - deactivate a plan","tags":["Plans"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Plan deactivated"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}}}},"/tickets":{"get":{"summary":"Get own support tickets","tags":["Tickets"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"pageSize","schema":{"type":"integer"}}],"responses":{"200":{"description":"Paginated ticket list with comments"},"401":{"description":"Unauthorized"}}},"post":{"summary":"Create a support ticket","tags":["Tickets"],"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["issueType","question"],"properties":{"issueType":{"type":"string","enum":["organizationAccount","userSubscription","paymentAndCredit","bugReports","featureRequests","campaignDelivery"]},"question":{"type":"string"},"uploadFileUrl":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"201":{"description":"Ticket created"},"400":{"description":"Missing required fields"},"401":{"description":"Unauthorized"}}}},"/tickets/admin":{"get":{"summary":"Admin - get all tickets","tags":["Tickets"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"query","name":"status","schema":{"type":"string","enum":["pending","open","closed","resolved"]}},{"in":"query","name":"search","schema":{"type":"string"}}],"responses":{"200":{"description":"All tickets"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}}}},"/tickets/admin/{id}/status":{"patch":{"summary":"Admin - update ticket status and priority","tags":["Tickets"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["pending","open","closed","resolved"]},"priority":{"type":"string","enum":["high","medium","low"]}}}}}},"responses":{"200":{"description":"Ticket updated"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}}}},"/tickets/{id}":{"get":{"summary":"Get a single ticket by ID","tags":["Tickets"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Ticket with comments"},"401":{"description":"Unauthorized"},"404":{"description":"Not found"}}}},"/tickets/{id}/comments":{"post":{"summary":"Add a comment to a ticket","tags":["Tickets"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["content"],"properties":{"content":{"type":"string"}}}}}},"responses":{"201":{"description":"Comment added"},"400":{"description":"Missing content"},"401":{"description":"Unauthorized"}}}},"/upload/presigned-url":{"post":{"summary":"Generate a presigned S3 upload URL","description":"Returns a short-lived presigned URL the client uses to upload a file directly to S3, plus the final public URL to store in the database.","tags":["Uploads"],"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["fileName","contentType"],"properties":{"fileName":{"type":"string","example":"brochure.pdf"},"contentType":{"type":"string","example":"application/pdf"},"folder":{"type":"string","example":"campaigns"}}}}}},"responses":{"200":{"description":"Returns uploadUrl (presigned), fileUrl (final S3 URL), and key"},"400":{"description":"Missing fileName or contentType"},"401":{"description":"Unauthorized"}}}},"/upload/confirm":{"post":{"summary":"Confirm a completed upload and create a file record","description":"Called by the client after successfully uploading to S3. Creates an UploadFile record linked to the business.","tags":["Uploads"],"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url","fileName"],"properties":{"url":{"type":"string"},"fileName":{"type":"string"},"size":{"type":"string"},"type":{"type":"string"}}}}}},"responses":{"201":{"description":"File record created"},"400":{"description":"Missing required fields"},"401":{"description":"Unauthorized"}}}},"/upload":{"get":{"summary":"List all uploaded files for the business","tags":["Uploads"],"security":[{"cookieAuth":[]}],"responses":{"200":{"description":"List of uploaded files"},"401":{"description":"Unauthorized"}}}},"/upload/{id}":{"delete":{"summary":"Delete an uploaded file from S3 and the database","tags":["Uploads"],"security":[{"cookieAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"File deleted"},"401":{"description":"Unauthorized"},"404":{"description":"File not found"}}}},"/users/profile":{"get":{"summary":"Get the current user's profile","description":"Returns the authenticated account. Role is omitted and replaced with an isAdmin boolean.","tags":["Users"],"security":[{"cookieAuth":[]}],"responses":{"200":{"description":"Account profile"},"401":{"description":"Unauthorized"}}},"patch":{"summary":"Update the current user's profile","description":"Only name and avatarUrl can be updated. All other fields are ignored.","tags":["Users"],"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"avatarUrl":{"type":"string"}}}}}},"responses":{"200":{"description":"Profile updated"},"401":{"description":"Unauthorized"}}}},"/users/email":{"patch":{"summary":"Update the authenticated user's email address","description":"Requires an OTP that was sent to the **new** email address.\nFlow: call `POST /auth/send-code` with the new email first, then confirm here.\n","tags":["Users"],"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["newEmail","code"],"properties":{"newEmail":{"type":"string","format":"email"},"code":{"type":"string","example":"482910"}}}}}},"responses":{"200":{"description":"Email updated successfully"},"400":{"description":"Same email, email already in use, or missing fields"},"401":{"description":"Invalid or expired OTP"}}}},"/users/account":{"delete":{"summary":"Deactivate (soft-delete) the current account","description":"Sets deleted=true and deletedAt on the account. The account can be restored by logging in again.","tags":["Users"],"security":[{"cookieAuth":[]}],"responses":{"200":{"description":"Account deactivated"},"401":{"description":"Unauthorized"}}}},"/webhooks/paystack":{"post":{"summary":"Paystack webhook receiver","description":"Receives Paystack events. Validates the HMAC-SHA512 signature from the\nx-paystack-signature header before processing.\nHandles: charge.success, transfer.success, transfer.failed, transfer.reversed.\nAlways responds 200 immediately before processing to prevent Paystack retries.\n","tags":["Webhooks"],"responses":{"200":{"description":"Received"}}}},"/webhooks/gupshup":{"post":{"summary":"Gupshup inbound message receiver","description":"Receives inbound WhatsApp messages from Gupshup. Matches the waNumber to a\nbusiness and handles opt-in/opt-out requests (text or button). On opt-in, the\ncontact is upserted, a confirmation is sent, and the most recently sent campaign\nis delivered to them. Always responds 200 immediately.\n","tags":["Webhooks"],"responses":{"200":{"description":"Received"}}}}},"tags":[{"name":"Admin","description":"Admin-only platform management"},{"name":"Affiliate","description":"Affiliate program management (affiliate-type businesses only)"},{"name":"Auth","description":"Authentication and account management"},{"name":"Business","description":"Business profile management"},{"name":"Campaigns","description":"WhatsApp campaign management"},{"name":"Contacts","description":"Contact management for the business"},{"name":"ExchangeRates","description":"Country list and exchange rate management"},{"name":"Notifications","description":"In-app notifications and preference settings"},{"name":"OAuth","description":"Social authentication"},{"name":"Payments","description":"Credit purchases, plan purchases, wallet, and transaction history"},{"name":"Plans","description":"Subscription plan management"},{"name":"Tickets","description":"Support ticket management"},{"name":"Uploads","description":"AWS S3 file upload via presigned URLs"},{"name":"Users","description":"Authenticated account profile management"},{"name":"Webhooks","description":"Inbound webhooks from third-party providers"}]}