Sendeet API — Developer Guide

This guide walks through the key flows a frontend or mobile client needs to implement. All endpoints are prefixed with https://dev-api.sendeet.online/api/v1.

For interactive exploration use Swagger UI. For the full schema import openapi.json into Postman.


Authentication

Sign up

POST /auth/sign-up
Body: { name, email, password, referralCode? }

Verify email

POST /auth/send-code
Body: { email }                    ← resend OTP

POST /auth/verify-email            ← requires Authorization header
Body: { code }

Sign in

POST /auth/sign-in
Body: { email, password }
Returns: { accessToken, refreshToken, user }

Refresh token

POST /auth/refresh-token
Body: { refreshToken }
Returns: { accessToken }

Password recovery

POST /auth/forgot-password
Body: { email }                    ← sends OTP to email

POST /auth/verify-code
Body: { code }                     ← validate OTP before showing new-password screen

POST /auth/reset-password
Body: { email, code, password }    ← set new password

Account Profile

GET  /users/profile                ← get current account
PATCH /users/profile
Body: { name?, avatarUrl? }        ← only these two fields are accepted

PATCH /users/email
Body: { newEmail, code }           ← OTP must first be sent to newEmail via /auth/send-code

DELETE /users/account              ← soft-delete (recoverable by logging in again)

Business Setup

After sign-up, the business profile is empty. Guide the user through these steps before they can send campaigns.

Step 1 — Fill in company details

PATCH /business
Body: {
  companyName, companyLogo, address, website,
  businessRegistrationNumber, countryCode,
  industry, noOfEmployees, noOfSubscribers
}

countryCode is an ISO code (e.g. "NG"). Get the full list from GET /exchange-rates/countries.

Step 2 — Submit for verification

POST /business/verification

Notifies all admin accounts. No action needed from the business until approved.

Step 3 — Set provider credentials (after admin approval)

POST /business/provider
Body: {
  provider: "gupShup",
  wabaNumber,
  gupshupHSMID,
  gupshupHSMPassword,
  gupshupDashboardLogin?,
  gupshupDashboardPassword?,
  gupshupTwowayLogin?,
  gupshupTwowayPassword?
}

Passwords are encrypted at rest. The response returns "true" or "false" for each password field — never the actual value.

Get own business profile

GET /business
Returns: { ...profile, wallet, userPlan }

Contacts

List and search

GET /contacts?page=1&pageSize=25&search=Ada&optIn=true

Create one

POST /contacts
Body: { firstName, lastName, phoneNumber, tags?, optIn? }

Bulk import — preferred (client parses file)

Parse the CSV or Excel file in the browser with PapaParse or SheetJS, then send the rows:

POST /contacts/import
Body: {
  contacts: [
    { firstName, lastName, phoneNumber, tags?, optIn? },
    ...
  ]
}
Returns: { imported, duplicatesSkipped, failed: [{ row, reason }] }

Duplicates (same phone number for this business) are silently skipped. Each row that fails validation is reported individually — the rest still import.

Bulk import — fallback (server parses file)

// 1. Get a presigned upload URL
POST /upload/presigned-url
Body: { fileName: "contacts.csv", contentType: "text/csv", folder: "imports" }
Returns: { uploadUrl, fileUrl, key }

// 2. PUT the file bytes directly to uploadUrl (client-side, no Authorization header needed)

// 3. Hand the fileUrl to the server
POST /contacts/import-file
Body: { fileUrl }
Returns: { imported, duplicatesSkipped, failed: [{ row, reason }] }

Export

GET /contacts/export?format=csv        ← downloads contacts.csv
GET /contacts/export?format=xlsx       ← downloads contacts.xlsx
GET /contacts/export?optIn=true        ← only opted-in contacts
GET /contacts/export?tag=vip           ← filter by tag

File Uploads (Campaign Attachments)

// 1. Get presigned URL
POST /upload/presigned-url
Body: { fileName: "brochure.pdf", contentType: "application/pdf", folder: "campaigns" }
Returns: { uploadUrl, fileUrl, key }

// 2. PUT file bytes to uploadUrl (no Authorization header)

// 3. Confirm the upload
POST /upload/confirm
Body: { url: fileUrl, fileName, size, type }
Returns: UploadFile record

Use the fileUrl from step 1 as uploadFileUrl when creating a campaign.


Campaigns

Check wallet / plan before creating

GET /payments/wallet              ← credit balance
GET /plans/my-plan                ← active plan details

Create a campaign

POST /campaigns
Body: {
  name,
  message,
  messageType: "marketing" | "utility" | "authentication",
  sendToAllContact: true,         ← or false + selectedContactId[]
  isScheduled: false,             ← immediate send
  paymentMethod: "credit" | "plan",
  uploadFileUrl?,                 ← from /upload/confirm
  header?,
  actionButtonTitle?,
  actionButtonUrl?
}

Omit paymentMethod to save as draft (no billing, no send).

Schedule a campaign

POST /campaigns
Body: {
  ...same fields,
  isScheduled: true,
  scheduledAt: "2025-09-15T14:00:00.000Z",   ← UTC ISO 8601
  timeZone: "Africa/Lagos",                   ← stored for display only
  paymentMethod: "credit"
}

Convert the user's local time to UTC before sending scheduledAt. The timeZone field is just for showing the user their original selection — all delivery logic uses UTC.

Publish a draft

POST /campaigns/:id/publish
Body: { paymentMethod: "credit" | "plan" }

Stats

GET /campaigns/stats
Returns: { total, sent, pending, scheduled, draft }

QR code opt-in link

GET /campaigns/qr-code
Returns: { link, imageBase64 }

imageBase64 is a PNG data URL — render it directly in an <img src="..."> tag.


Payments & Credits

Price credits before purchase

GET /payments/credit-pricing?totalCredit=100
Returns: { totalCredit, priceUSD, priceNGN, currency, rates }

Buy credits

POST /payments/credit-link
Body: { totalCredit: 100 }
Returns: { authorization_url, reference, ... }

Redirect the user to authorization_url. After payment, Paystack fires the webhook automatically. You can also manually verify:

POST /payments/verify
Body: { reference }

Buy a plan

GET /plans                                 ← list active plans (prices in NGN)
POST /payments/plan-link
Body: { planId }
Returns: { authorization_url, ... }

Wallet and history

GET /payments/wallet
GET /payments/transactions?page=1&pageSize=25

Notifications

GET  /notifications?page=1&pageSize=25     ← includes unread count
POST /notifications/:id/read
POST /notifications/read-all

GET  /notifications/settings               ← key/value object
PATCH /notifications/settings
Body: { campaignCompletionPush: "false", lowCreditsEmail: "true" }

Real-time (Socket.io)

Connect to the server and join the appropriate room after sign-in:

import { io } from "socket.io-client";

const socket = io(BASE_URL);

// Business users
socket.emit("joinBusinessRoom", businessId);

// Admin users
socket.emit("joinAdminRoom", accountId);

// On reconnect
socket.emit("rejoinRooms", { businessId });

// Listen for notifications
socket.on("notification", (notification) => {
  // { id, type, title, message, isRead, createdAt, ... }
  showToast(notification.title);
  refreshNotificationList();
});

// Confirm room joined
socket.on("joinedRoom", ({ roomType, roomId }) => {
  console.log(`Joined ${roomType} room ${roomId}`);
});

The notification event fires on: campaign sent, contact opt-in/out, payment success, business verified/rejected.


Support Tickets

POST /tickets
Body: { issueType, question, uploadFileUrl? }

GET  /tickets
GET  /tickets/:id
POST /tickets/:id/comments
Body: { content }

issueType options: organizationAccount, userSubscription, paymentAndCredit, bugReports, featureRequests, campaignDelivery.


Affiliate Program (affiliate businesses only)

GET  /affiliate/referrals
GET  /affiliate/earnings          ← { balance, totalEarned, totalWithdrawn }
GET  /affiliate/transactions

GET  /affiliate/bank-accounts/verify?accountNumber=&bankCode=   ← resolve name
POST /affiliate/bank-accounts
Body: { bankName, accountNumber, accountName, code }

POST /affiliate/withdraw
Body: { bankAccountId, amount }   ← amount in NGN

Error Responses

All errors follow this shape:

{
  "apiObject": "Campaign",
  "code": 400,
  "status": "failure",
  "message": "Insufficient credits",
  "errorMessage": {}
}

Common status codes:

Code Meaning
400 Bad request — missing field or business rule violation
401 Unauthenticated — missing or invalid token
403 Forbidden — wrong role or unverified account
404 Resource not found
500 Internal server error