DEVELOPERS
API reference.
One REST API for transactional email, mailing lists, currency, push messaging, and social automation. Authenticate with a bearer key and you're sending in minutes.
Introduction
The startupaid API is organized around REST. It accepts JSON request bodies, returns JSON responses, and uses standard HTTP status codes and verbs. The base URL is https://api.startupaid.org.
Every endpoint under /v1 uses an API key. Sending domains, email templates, event webhooks, deep links, and social channels are set up once in your dashboard — then used from the API by reference (address, slug, channel id) or, for webhooks, by receiving events.
Authentication
Every /v1 request requires an API key, passed as a bearer token or the X-API-Key header. Keys are hashed at rest and can be revoked at any time from your dashboard.
Authorization: Bearer sa_live_your_key_here
Endpoints
| Group | Method | Path | Auth | Description |
|---|---|---|---|---|
| Apps | GET | /v1/apps | API key | List your apps (shared across Push & OTP) |
| Apps | POST | /v1/apps | API key | Create an app; returns its unique appId |
| Apps | PATCH | /v1/apps/:appId | API key | Rename an app |
| Apps | DELETE | /v1/apps/:appId | API key | Delete an app and its module resources |
| POST | /v1/send | API key | Queue and send a transactional email | |
| GET | /v1/messages/:id | API key | Fetch a message's delivery status | |
| Mailing lists | GET | /v1/audiences | API key | List your audiences (mailing lists) |
| Mailing lists | POST | /v1/audiences | API key | Create an audience |
| Mailing lists | GET | /v1/audiences/:id | API key | Fetch a single audience |
| Mailing lists | PATCH | /v1/audiences/:id | API key | Rename / edit an audience |
| Mailing lists | DELETE | /v1/audiences/:id | API key | Delete an audience and its contacts |
| Contacts | GET | /v1/audiences/:id/contacts | API key | List contacts (search + paging) |
| Contacts | POST | /v1/audiences/:id/contacts | API key | Add a contact |
| Contacts | POST | /v1/audiences/:id/contacts/import | API key | Bulk import contacts |
| Contacts | PATCH | /v1/contacts/:id | API key | Update a contact / subscription |
| Contacts | DELETE | /v1/contacts/:id | API key | Remove a contact |
| Currency | GET | /v1/convert | API key | Convert an amount between any two currencies |
| Currency | GET | /v1/rates/:base | API key | Cross-rates for a base currency |
| Currency | GET | /v1/currencies | API key | List supported fiat & crypto currencies |
| Currency | GET | /v1/currency/status | API key | Rate freshness by asset class |
| Push | POST | /v1/push/apps/:appId/devices | API key | Register an end-user's device token |
| Push | GET | /v1/push/apps/:appId/devices | API key | List a user's registered devices |
| Push | DELETE | /v1/push/apps/:appId/devices | API key | Remove a device token |
| Push | POST | /v1/push/apps/:appId/send | API key | Send a notification (iOS, Android & web) |
| Push | GET | /v1/push/apps/:appId/messages/:id | API key | Per-device delivery breakdown |
| WhatsApp OTP | POST | /v1/otp/send | API key | Send a one-time passcode over WhatsApp |
| WhatsApp OTP | POST | /v1/otp/verify | API key | Verify a code the recipient submitted |
| Social | POST | /v1/social/schedule | API key | Schedule or publish a post to connected channels |
| Social | GET | /v1/social/posts | API key | List your scheduled & published posts |
| System | GET | /health | Public | Liveness probe |
Apps
Apps & app IDs
An app is the account-wide unit you create once and target from any app-centric product — Push and WhatsApp OTP today (Email is the exception: it targets verified domains, not apps). Create apps in the dashboard or via the API.
Every app has a unique id like app_9f3k2x8a…. Always reference an app by its id, never its name — names are display-only and can repeat across accounts. For OTP the app's name is also the brand shown in the message (“Your Acmeverification code is 123456”); for Push it labels the app in your dashboard.
# Create an app — the returned id (app_…) is what you target everywhere.
curl -X POST https://api.startupaid.org/v1/apps \
-H "Authorization: Bearer sa_live_xxx" \
-H "Content-Type: application/json" \
-d '{ "name": "Acme" }'
# => { "id": "app_9f3k2x8a1b2c3d4e5f6a", "name": "Acme", "createdAt": "…" }
# List your apps
curl https://api.startupaid.org/v1/apps \
-H "Authorization: Bearer sa_live_xxx"Sending domains
You send from your own domain. Add and verify it once under Domains in the dashboard — we generate the DKIM records, you publish them at your DNS host, and hit verify. After that, any from address on that domain is accepted. Sends from an unverified domain return 403.
Send an email
POST /v1/send with a from (on a verified domain), to, subject, and an HTML body. Delivery is asynchronous: you get 202 Accepted immediately with a queued message, and we retry with backoff behind the scenes.
curl -X POST https://api.startupaid.org/v1/send \
-H "Authorization: Bearer sa_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"from": "hello@yourdomain.com",
"to": "user@example.com",
"subject": "Welcome aboard",
"body": "<h1>Hi there 👋</h1><p>Thanks for signing up.</p>"
}'
# => 202 Accepted
# { "id": "…", "status": "queued", "to": "user@example.com" }Send via SMTP
Prefer SMTP, or using software that only speaks it? Point your app at our submission relay — no code changes. Authenticate with any of your API keys (username apikey, password your sa_live_… key). Messages funnel into the exact same pipeline as /v1/send, with the same domain rules, quotas, DKIM, and delivery events.
Host: smtp.startupaid.org Port: 587 Encryption: STARTTLS Username: apikey Password: sa_live_your_key (any API key) From: an address on a verified domain
AUTH is only offered after STARTTLS, so your key is never sent in the clear. Your From must be on a verified domain; unverified senders fall back to the daily-capped sandbox.
import nodemailer from "nodemailer";
const transport = nodemailer.createTransport({
host: "smtp.startupaid.org",
port: 587,
secure: false, // STARTTLS is negotiated on 587
auth: { user: "apikey", pass: "sa_live_your_key" },
});
await transport.sendMail({
from: "hello@yourdomain.com", // a verified domain
to: "user@example.com",
subject: "Hello from SMTP",
html: "<h1>Hi 👋</h1>",
});Attachments
Add an attachments array to any /v1/send request. Each attachment is a base64-encoded file with a filename; contentType is optional (we infer it from the extension). Up to 10 files and 20 MB total per message.
POST /v1/send
{
"from": "hello@acme.com",
"to": "user@example.com",
"subject": "Your invoice",
"body": "<p>Invoice attached.</p>",
"attachments": [
{
"filename": "invoice.pdf",
"contentType": "application/pdf",
"content": "JVBERi0xLjQ…" // base64
}
]
}Send with a template
Create reusable templates in the dashboard (with {{variable}} placeholders), then send by template slug and pass variables. Omit subject and body — they come from the template. Variable values are HTML-escaped for you.
curl -X POST https://api.startupaid.org/v1/send \
-H "Authorization: Bearer sa_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"from": "hello@yourdomain.com",
"to": "user@example.com",
"template": "welcome_onboarding",
"variables": { "name": "Alex", "plan": "Founder" }
}'Idempotency
Send an Idempotency-Key header to safely retry a request without sending twice. A repeated key returns the original queued message (with an Idempotent-Replayed: true response header) instead of sending again.
curl -X POST https://api.startupaid.org/v1/send \
-H "Authorization: Bearer sa_live_xxx" \
-H "Idempotency-Key: order-4192-receipt" \
-H "Content-Type: application/json" \
-d '{ "from": "hello@yourdomain.com", "to": "user@example.com",
"subject": "Receipt", "body": "<p>Thanks!</p>" }'Message status
Poll GET /v1/messages/:id using the id returned by a send. Engagement events (delivered, opened, clicked, bounced, complained) are also visible under Logs in the dashboard.
curl https://api.startupaid.org/v1/messages/MESSAGE_ID \
-H "Authorization: Bearer sa_live_xxx"
# => {
# "id": "…", "to": "user@example.com",
# "status": "delivered", // queued | sending | sent | delivered | failed
# "attempts": 1, "createdAt": "…"
# }Mailing lists
An audience is a mailing list — a named collection of contacts. Manage them from the API or the dashboard; both share the same data. An email address is unique per audience.
curl -X POST https://api.startupaid.org/v1/audiences \
-H "Authorization: Bearer sa_live_xxx" \
-H "Content-Type: application/json" \
-d '{ "name": "Newsletter", "description": "Weekly product updates" }'
# => { "id": "AUDIENCE_ID", "name": "Newsletter", "contactCount": 0, ... }Contacts & import
Add contacts one at a time or bulk-import them. Each contact takes an email, optional firstName/lastName, and a free-form data JSON object for merge attributes. Import de-dupes and skips invalid rows rather than failing the batch.
curl -X POST https://api.startupaid.org/v1/audiences/AUDIENCE_ID/contacts \
-H "Authorization: Bearer sa_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"email": "ada@example.com",
"firstName": "Ada",
"lastName": "Lovelace",
"data": "{\"plan\": \"pro\"}"
}'
# => { "id": "…", "email": "ada@example.com", "subscribed": true, ... }Unsubscribe
Every contact has an opaque unsubscribe token and a hosted one-click page — no login required for the recipient. Link to it from your list emails, or flip a contact's subscribed state via the API.
# Flip a contact's subscription state
curl -X PATCH https://api.startupaid.org/v1/contacts/CONTACT_ID \
-H "Authorization: Bearer sa_live_xxx" \
-H "Content-Type: application/json" \
-d '{ "subscribed": false }'Currency
Convert currency
GET /v1/convert converts an amount between any two supported currencies — fiat or crypto. Rates come from our own engine (institutional reference rates for fiat, a live feed for crypto), and every response includes rateAsOf so you know exactly how fresh the rate is (crypto updates every minute; fiat tracks a daily institutional reference).
# Fiat, crypto, or a mix — from/to any supported code.
curl "https://api.startupaid.org/v1/convert?from=USD&to=NGN&amount=100" \
-H "Authorization: Bearer sa_live_xxx"
# => {
# "from": "USD", "to": "NGN", "amount": 100,
# "result": 137503.95,
# "rateAsOf": "2026-07-09T00:00:00Z" // freshness of THIS pair
# }
# Crypto works the same way:
curl "https://api.startupaid.org/v1/convert?from=BTC&to=USD&amount=1" \
-H "Authorization: Bearer sa_live_xxx"
# => { "result": 63837.0, "rateAsOf": "2026-07-10T08:11:23Z" }
# If you've set a CUSTOM rate on the pair, the response also gives you the
# market rate and your margin — so fintechs book profit with no extra math:
# => {
# "result": 160000, "rate": 1600, // at your rate
# "marketRate": 1375.04, "marketResult": 137503.95,
# "spread": { "rate": 224.96, "percent": 16.36, "amount": 22496.05 }
# }Rates, currencies & freshness
List everything you can convert with /v1/currencies (fiat + crypto, with names and logos), get a full cross-rate table with /v1/rates/:base, and check per-source freshness with /v1/currency/status. Need NGN or another emerging currency, or your own spread? Pin a custom rate under Currencyin the dashboard and it applies to your account's conversions.
curl "https://api.startupaid.org/v1/currencies" \
-H "Authorization: Bearer sa_live_xxx"
# => { "currencies": [
# { "code": "USD", "type": "fiat" },
# { "code": "NGN", "type": "fiat" },
# { "code": "BTC", "type": "crypto", "name": "Bitcoin", "logo": "https://…" },
# … 100+ total
# ]}Push Messaging
Push — register a device
Push is a relay for your own apps. In the dashboard create an app (you can run many — a consumer app, a driver app, staging vs prod) and add its provider keys: an FCM service account for Android/web, an APNs .p8for iOS, or a generated VAPID keypair for browsers. Then register each end-user's device token under that app, keyed by userRef — your own user id. Everything is scoped to /v1/push/apps/:appId. Do this from your backend so the secret key never ships inside the client app.
# Register from YOUR backend so the secret key never ships in the app.
# platform: "fcm" (Android/web) · "apns" (iOS) · "webpush" (browser)
curl -X POST https://api.startupaid.org/v1/push/apps/APP_ID/devices \
-H "Authorization: Bearer sa_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"userRef": "user_42",
"platform": "fcm",
"token": "DEVICE_TOKEN"
}'
# For web push, "token" is the browser PushSubscription JSON (endpoint + keys).Push — send a notification
One send reaches iOS, Android, and browsersat once — we fan out to each platform using that app's credentials and return a per-device result. Target a single user, a list of users, specific tokens, or every device. Attach a link so a tap opens deep in your app or on the web. Tokens a provider rejects are retired automatically. Each notification attempt counts as one unit against your monthly allowance.
# One call reaches iOS, Android, and web — we fan out to each platform.
curl -X POST https://api.startupaid.org/v1/push/apps/APP_ID/send \
-H "Authorization: Bearer sa_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"target": { "userRef": "user_42" },
"title": "Your order shipped 📦",
"body": "Arriving Tuesday.",
"link": "acme://orders/8921"
}'
# target: { userRef } | { userRefs: [...] } | { tokens: [...] } | { all: true }
# => { "messageId": "…", "status": "sent", "recipients": 3, "sent": 3, "failed": 0 }WhatsApp OTP
OTP — send a code
POST /v1/otp/send delivers a one-time passcode over WhatsApp, branded with the name of the app you send from. Pass an appId (create one via POST /v1/apps or in the dashboard) to choose the app — omit it to use your default app. Two modes:
- Managed (default) — omit
codeand we generate it, store a hash, and verify it for you. The code expires in a few minutes and locks out after too many wrong attempts. - Delivery-only — already generate codes yourself? Pass your own
code(4–10 letters/digits) and we simply deliver it. You can still call/verify, or verify on your side.
Each send counts as one unit against your monthly allowance. Numbers must be in E.164 format (e.g. +2348012345678).
# Managed — we generate, deliver, and verify the code.
curl -X POST https://api.startupaid.org/v1/otp/send \
-H "Authorization: Bearer sa_live_xxx" \
-H "Content-Type: application/json" \
-d '{ "to": "+2348012345678", "appId": "app_9f3k2x8a…" }'
# => { "id": "…", "to": "+2348012345678", "status": "pending", "expiresAt": "…" }
# Delivery-only — pass your own code (4–10 letters/digits) and we just deliver it.
curl -X POST https://api.startupaid.org/v1/otp/send \
-H "Authorization: Bearer sa_live_xxx" \
-H "Content-Type: application/json" \
-d '{ "to": "+2348012345678", "code": "482913" }'OTP — verify a code
POST /v1/otp/verify checks the code your user entered against the latest one sent to that number. Returns { "verified": true } on an exact, in-time match — otherwise false. A successful verification consumes the code (it can't be reused), and verification is free — only sends are metered.
curl -X POST https://api.startupaid.org/v1/otp/verify \
-H "Authorization: Bearer sa_live_xxx" \
-H "Content-Type: application/json" \
-d '{ "to": "+2348012345678", "code": "482913" }'
# => { "verified": true }Social Automation
Webhooks
Event webhooks
Register an endpoint to receive your mail's delivery events — delivered, bounced, complained, opened, clicked — as they happen. Add your endpoint under Webhooks in the dashboard; each can be scoped to one sending domain or receive events for all of them.
Every request is signed. Verify authenticity by recomputing an HMAC-SHA256 of the raw request body with your webhook's signing secret and comparing it to the X-startupaid-Signature header. Return a 2xx within 15s; non-2xx or timeouts retry with exponential backoff (up to 8 attempts).
POST https://your-app.com/webhooks/startupaid
X-startupaid-Event: bounced
X-startupaid-Signature: sha256=<hmac>
{
"id": "evt_…",
"type": "bounced",
"messageId": "…",
"recipient": "user@example.com",
"createdAt": "2026-07-11T08:04:00Z"
}
// verify (Node):
// const mac = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
// trusted = ("sha256=" + mac) === req.headers["x-startupaid-signature"];Deep Links
Deep links
Create links that open your mobile app to a specific screen — iOS Universal Links and Android App Links— with a web fallback when the app isn't installed, plus a click counted on every tap. Set your app identifiers and create links under Deep Links in your dashboard — we host the apple-app-site-association and assetlinks.json files for you.
// A created link looks like:
https://links.startupaid.org/l/acme/aX9k2
// Embed it in the email/push you send. Installed apps open straight to the
// app path; everyone else hits the web fallback (and the click is recorded).
// Your app SDK reads the link's deep-data (public, no key):
GET /l/acme/aX9k2/resolve
// → { "appPath": "/orders/8921", "fallback": "https://acme.com/orders/8921" }Reference
Errors
Errors return a JSON body with an error field and a conventional HTTP status code.
| Status | Code | Meaning |
|---|---|---|
| 400 | bad_request | The payload failed validation (missing or malformed fields, bad email). |
| 401 | unauthorized | Missing or invalid API key. |
| 403 | forbidden | The from-address isn't a domain you've verified for this account. |
| 404 | not_found | The requested resource does not exist. |
| 409 | conflict | Duplicate — e.g. a contact with that email already exists in the audience. |
| 429 | rate_limited | You've exceeded your plan's rate limit — back off and retry. |
| 502 | provider_error | An upstream provider (SMTP or FX) failed; safe to retry. |
Official SDKs
Node.js / TypeScript
npm i @startupaid/sdkPython
pip install startupaidGo
go get github.com/StartupAid-Org/startupaid-goMCP server
npx -y @startupaid/mcpNeed a key?
Create an account and generate one in seconds.
Social — schedule a post
Connect your channels — X, LinkedIn, Threads, Facebook, Instagram, or a custom webhook — once in the dashboard, then schedule a post to any of them with one call. Pass the connected
channelsids and yourcontent; add an optionalimageUrl(required for Instagram) and ascheduledFortime, or omit it to publish now. FetchGET /v1/social/postsfor per-channel delivery results.# Schedule a post to your connected channels (connect them in the dashboard). # Omit "scheduledFor" to publish as soon as possible. curl -X POST https://api.startupaid.org/v1/social/schedule \ -H "Authorization: Bearer sa_live_xxx" \ -H "Content-Type: application/json" \ -d '{ "channels": ["chan_9f2", "chan_7a1"], "content": "We just shipped Deep Links 🚀", "imageUrl": "https://cdn.acme.io/launch.png", "scheduledFor": "2026-07-20T09:00:00Z" }' # => 202 { "id": "post_…", "status": "scheduled", "scheduledFor": "…" } # List posts + per-channel results: GET /v1/social/posts