DEVELOPERS
API reference.
One REST API for transactional email, mailing lists, currency, push messaging, social automation, and AI video and images, with an MCP endpoint for AI agents. 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 (including YouTube and TikTok) 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 | GET | /v1/social/channels | API key | List connected channels and their ids |
| 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 |
| Loyalty | GET | /v1/loyalty/programs | API key | List your rewards programs |
| Loyalty | POST | /v1/loyalty/programs | API key | Create a program (tiers, referral bonuses, expiry, points per spend) |
| Loyalty | POST | /v1/loyalty/programs/:id/award | API key | Credit points to a member by email or phone, by count or by activity key |
| Loyalty | GET | /v1/loyalty/programs/:id/contacts | API key | Look up a member's balance, tier and expiry by ?email= or ?phone= |
| Loyalty | POST | /v1/loyalty/programs/:id/redeem | API key | Spend a member's points on a catalog reward (till or app) |
| Loyalty | POST | /v1/loyalty/programs/:id/referrals | API key | Record that one member referred another; both sides earn |
| Loyalty | POST | /v1/loyalty/programs/:id/codes | API key | Mint a code: once per person or recurring, with start/end and a QR |
| Loyalty | GET | /v1/loyalty/programs/:id/leaderboard | API key | Top earners in a program |
| Loyalty | POST | /hooks/loyalty/:programId/shopify | Public | Shopify order-paid webhook: paid orders earn points |
| Loyalty | POST | /hooks/loyalty/:programId/stripe | Public | Stripe checkout / payment webhook: payments earn points |
| Outcomes | POST | /v1/events | API key | Record a conversion (signup, purchase…) and attribute it to the email that drove it |
| AI Video | GET | /v1/video/config | API key | Models, rates, lengths, resolutions and the pricing schedule |
| AI Video | POST | /v1/video/quote | API key | Price a clip in credits without making it |
| AI Video | POST | /v1/video/jobs | API key | Generate a clip (charges credits, returns the queued job) |
| AI Video | GET | /v1/video/jobs | API key | List recent jobs |
| AI Video | GET | /v1/video/jobs/:id | API key | Job status, video and thumbnail URLs |
| AI Images | GET | /v1/images/config | API key | Image models, per-image prices and sizes |
| AI Images | POST | /v1/images/generate | API key | Generate 1–4 images or edit reference images |
| AI Images | GET | /v1/images | API key | List generated images |
| AI credits | GET | /v1/account/ai-credits | API key | Credits used, allowed and remaining this month |
| Deep links | POST | /v1/deeplinks | API key | Create a link that opens your app (web fallback, click and open counts) |
| Deep links | GET | /v1/deeplinks | API key | List your deep links |
| Deep links | GET | /l/:key/:slug/resolve | Public | The link's in-app path and data, read by your app after it opens |
| AI agents | POST | /mcp | OAuth or API key | Model Context Protocol endpoint (Streamable HTTP) |
| 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, namely 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, since 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, with 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, since 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, and 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, or 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, since 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
Loyalty & Rewards
Loyalty: programs & rules
A program is a points ledger with rules: tiers a member crosses and keeps, referral bonuses for both sides, expiry after a number of days without earning, and points per 1.00 spent for purchases. Every point is earned deterministically, never by a draw. Build one in the dashboard or from your app with the same API, so a platform can run a program for each of its own customers. Programs start as drafts; activate them when the rules are right.
curl -X POST https://api.startupaid.org/v1/loyalty/programs \
-H "Authorization: Bearer sa_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Moses Rewards",
"tiers": [{ "name": "Bronze", "minPoints": 0 }, { "name": "Gold", "minPoints": 500, "reward": "Free delivery for a year" }],
"referralPoints": 50, "refereePoints": 20,
"pointsExpireDays": 365, "pointsPerSpend": 1
}'
# => { "id": "…", "widgetKey": "wgt_…", "status": "draft", … }
# Programs start as drafts. Activate when the rules are right:
curl -X POST https://api.startupaid.org/v1/loyalty/programs/PROGRAM_ID/status \
-H "Authorization: Bearer sa_live_xxx" -H "Content-Type: application/json" \
-d '{ "status": "active" }'Loyalty: award points
POST /v1/loyalty/programs/:id/award credits a member, found or created by email. Pass a points count, or an activity key defined in the dashboard so the value can change without a deploy. Pass phone instead of email for a member who added their number in the widget: it finds them but never enrols anyone. An eventId (or an Idempotency-Key header) makes retries safe: a repeat returns the original result with Idempotent-Replayed: true. New members count against your plan's participant allowance.
# By count. eventId (or an Idempotency-Key header) makes retries safe.
curl -X POST https://api.startupaid.org/v1/loyalty/programs/PROGRAM_ID/award \
-H "Authorization: Bearer sa_live_xxx" \
-H "Content-Type: application/json" \
-d '{ "email": "ada@example.com", "points": 25, "reason": "Completed profile", "eventId": "profile:u_123" }'
# => { "contactId": "…", "status": { "balance": 25, "tier": { "name": "Bronze" }, "nextTier": { … } } }
# By activity key, so the value lives in the dashboard, not your code:
curl -X POST https://api.startupaid.org/v1/loyalty/programs/PROGRAM_ID/award \
-H "Authorization: Bearer sa_live_xxx" -H "Content-Type: application/json" \
-d '{ "email": "ada@example.com", "activity": "daily_login", "eventId": "login:u_123:2026-09-11" }'
# At the till, by the phone the member added in the widget:
curl -X POST https://api.startupaid.org/v1/loyalty/programs/PROGRAM_ID/award \
-H "Authorization: Bearer sa_live_xxx" -H "Content-Type: application/json" \
-d '{ "phone": "+447911123456", "points": 10, "reason": "In-store purchase" }'Loyalty: look up & redeem
GET /v1/loyalty/programs/:id/contacts?email= (or ?phone=) returns balance, tier, the date the balance would expire, and recent activity. The reward catalog is what points are spent on: a coupon reward issues a unique code the moment it is redeemed (your own codes if you uploaded some, else generated with your prefix), a link reward unlocks a URL, and a manual reward lands in your inbox to fulfil; dismissing it refunds the points. POST /v1/loyalty/programs/:id/redeem spends on behalf of a member, the shape a till or an app uses. Insufficient balance returns 402, sold out 409.
# Who is this, and where do they stand?
curl "https://api.startupaid.org/v1/loyalty/programs/PROGRAM_ID/contacts?phone=%2B447911123456" \
-H "Authorization: Bearer sa_live_xxx"
# => { "contactId": "…", "email": "ada@example.com", "status": { "balance": 340, "tier": { … }, "expiresAt": "…" } }
# What can they spend on?
curl https://api.startupaid.org/v1/loyalty/programs/PROGRAM_ID/rewards -H "Authorization: Bearer sa_live_xxx"
# => { "rewards": [ { "id": "rw_…", "name": "Free coffee", "cost": 50, "kind": "coupon", "stock": -1, … } ] }
# Spend. A coupon reward returns its code right here.
curl -X POST https://api.startupaid.org/v1/loyalty/programs/PROGRAM_ID/redeem \
-H "Authorization: Bearer sa_live_xxx" -H "Content-Type: application/json" \
-d '{ "email": "ada@example.com", "rewardId": "rw_…" }'
# => { "redemption": { "code": "CAFE-7K2M9Q4A", "status": "fulfilled", "pointsSpent": 50 }, "status": { "balance": 290 } }Loyalty: referrals & purchases
Two earn moments need no code from the member. Referrals: the widget carries a personal link, and POST /v1/loyalty/programs/:id/referrals lets your own app report one. Both are one referral per new member, credited only once the newcomer is real. Purchases: paste a Shopify or Stripe signing secret into the program and point their webhook at /hooks/loyalty/:programId/shopify (Order payment) or /hooks/loyalty/:programId/stripe (checkout.session.completed, payment_intent.succeeded). Every paid order credits the buyer's email at your points-per-1.00 rate; the platform's retries never double-credit.
# Your app knows who brought whom. Tell us once; both sides earn what the program promises.
curl -X POST https://api.startupaid.org/v1/loyalty/programs/PROGRAM_ID/referrals \
-H "Authorization: Bearer sa_live_xxx" -H "Content-Type: application/json" \
-d '{ "referrerEmail": "moses@example.com", "refereeEmail": "new@example.com" }'
# => { "referrer": { "balance": 50 }, "referee": { "balance": 20 }, "replayed": false }
# A second call for the same new member replays the first: nobody is paid twice.Loyalty: codes, QR & the widget
A code is the earn moment you can print: typed in the widget, tapped as a claim link, or scanned as a QR from a bottle cap, a receipt or a poster. repeatAfterHours makes it recurring (24 for a daily check-in), maxRedemptions caps the total, startsAt and expiresAt set the window. GET /v1/loyalty/codes/:id/qr returns a print-ready PNG. The member-facing widget is one script tag or a link: members sign in with a code emailed to them, see balance, tier, rank and expiry, spend on the catalog, redeem codes, share their referral link and add a phone for the till. Every event (points awarded or deducted, tier reached, reward redeemed, points expired) is available as a signed webhook.
# A daily check-in QR by the door: 5 points, once every 24 hours, all summer.
curl -X POST https://api.startupaid.org/v1/loyalty/programs/PROGRAM_ID/codes \
-H "Authorization: Bearer sa_live_xxx" -H "Content-Type: application/json" \
-d '{ "code": "DOOR", "name": "Door check-in", "pointsValue": 5, "repeatAfterHours": 24, "expiresAt": "2026-09-30T00:00:00Z" }'
# A one-per-person campaign code for the first 500 people:
curl -X POST https://api.startupaid.org/v1/loyalty/programs/PROGRAM_ID/codes \
-H "Authorization: Bearer sa_live_xxx" -H "Content-Type: application/json" \
-d '{ "code": "LAUNCH", "pointsValue": 100, "maxRedemptions": 500, "reward": "Launch-day bonus" }'
# Print it: a PNG QR of the code's claim link.
curl https://api.startupaid.org/v1/loyalty/codes/CODE_ID/qr -H "Authorization: Bearer sa_live_xxx"
# => { "dataUrl": "data:image/png;base64,…", "link": "https://www.startupaid.org/claim/wgt_…/DOOR" }Outcomes
Conversion events
Tell startupaid when something happened in your product, keyed by the person’s email: a signup, a purchase with its value, a booking, anything you name. Each event is attributed to the last email that person clicked in the past week (or opened in the past day), so a campaign or automation reports customers and revenue, not just opens, and the monthly goal on the Campaigns page counts up. An event also starts any automation whose trigger is that event, creating the contact if the list has not seen them, so signup can begin an onboarding sequence for a brand-new user.
curl -X POST https://api.startupaid.org/v1/events \
-H "Authorization: Bearer sa_live_xxx" \
-H "Content-Type: application/json" \
-d '{ "type": "purchase", "email": "sam@lumen.dev", "value": 49.00, "currency": "USD", "data": { "plan": "growth" } }'
# => 202 { "id": "…", "type": "purchase", "email": "sam@lumen.dev",
# "attributed": true, "campaignId": "cmp_…", "touch": "clicked" }
# type: letters, digits, _ or -. value is in major units; omit it for events without one.AI Video & Images
Video: models & pricing
Clips are made by Veo, Seedance and Kling and paid for in AI credits. GET /v1/video/config returns every model you can use with its mode (text, image to animate a still, element to put a subject from reference images into a new scene), its credit rate per second, the lengths and resolutions it accepts, and the schedule that scales the price by resolution. The price of a clip is perSecond × seconds × resolution%, plus a small surcharge per extra reference image, and POST /v1/video/quote works it out for you without charging anything. Credits are taken when a job is created and refunded in full if the render fails.
# What can I make, and what does it cost?
curl https://api.startupaid.org/v1/video/config \
-H "Authorization: Bearer sa_live_xxx"
# => { "enabled": true, "maxInFlight": 4,
# "pricing": { "resolutionPct": { "480p": 70, "720p": 100, "1080p": 150, "4k": 700 }, "refSurcharge": 5 },
# "models": [ { "id": "seedance", "label": "Seedance", "mode": "text", "perSecond": 14,
# "minSecs": 4, "maxSecs": 15, "resolutions": ["480p","720p"], "credits": 56 },
# { "id": "kling", "label": "Kling 2.5 Turbo", "mode": "text", "perSecond": 13,
# "durations": [5, 10], "resolutions": ["1080p"], "credits": 98 }, … ] }
# Price a clip before making it (nothing is charged):
curl -X POST https://api.startupaid.org/v1/video/quote \
-H "Authorization: Bearer sa_live_xxx" -H "Content-Type: application/json" \
-d '{ "model": "seedance", "durationSecs": 8, "resolution": "720p" }'
# => { "credits": 112, "model": "seedance", "durationSecs": 8, "resolution": "720p" }Video: generate a clip
POST /v1/video/jobs with a prompt and, optionally, a model, durationSecs, aspectRatio (16:9, 9:16, 1:1, 4:3, 21:9) and resolution. Image models take a sourceImageUrl to animate as the first frame (and an optional endImageUrl); element models take up to four elementImageUrls. The call returns 202 with the queued job at once. Send an Idempotency-Key header (or idempotencyKey in the body) so a retried request returns the same job instead of charging twice. An account can have four clips rendering at a time.
curl -X POST https://api.startupaid.org/v1/video/jobs \
-H "Authorization: Bearer sa_live_xxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: launch-teaser-1" \
-d '{
"prompt": "Product on a phone in a sunlit café, slow dolly in, warm light",
"model": "seedance",
"durationSecs": 6,
"aspectRatio": "9:16",
"resolution": "720p"
}'
# => 202 { "id": "vid_01a8…", "status": "queued", "creditsCost": 84, "model": "seedance", … }
# Animate a still (image-to-video):
# "model": "seedance-i2v", "sourceImageUrl": "https://…/hero.png"
# Put a subject in a new scene (element-to-video):
# "model": "seedance-e2v", "elementImageUrls": ["https://…/founder-1.png", "https://…/founder-2.png"]Video: poll, download, auto-post
A job moves through queued, submitting, processing and then done or failed, usually within one to three minutes. Poll GET /v1/video/jobs/:id until it is done; the result carries a public videoUrl and thumbnailUrl. To publish the clip the moment it lands, pass an autoPost object when creating the job: the channel ids, an optional caption (the prompt when omitted) and an optional time. The job then reports the post it made under autoPost.postId.
curl https://api.startupaid.org/v1/video/jobs/vid_01a8… \
-H "Authorization: Bearer sa_live_xxx"
# => { "id": "vid_01a8…", "status": "done",
# "videoUrl": "https://cdn.startupaid.org/video/…/take.mp4",
# "thumbnailUrl": "https://cdn.startupaid.org/video/…/take.jpg",
# "creditsCost": 84, "durationSecs": 6, "aspectRatio": "9:16",
# "autoPost": { "channels": ["chan_7a1"], "postId": "post_…" } }
# Post it to YouTube and TikTok the moment it lands:
curl -X POST https://api.startupaid.org/v1/video/jobs \
-H "Authorization: Bearer sa_live_xxx" -H "Content-Type: application/json" \
-d '{
"prompt": "…", "model": "seedance", "durationSecs": 6, "aspectRatio": "9:16",
"autoPost": { "channels": ["chan_7a1", "chan_3c9"], "caption": "Sneak peek 👀" }
}'Images: generate or edit
GET /v1/images/config lists the image models (Flux, Seedream, Nano Banana) with a per-image credit price and a mode: text models draw from a prompt at a size of 1:1, 16:9, 9:16, 4:3 or 3:4; edit models transform or combine the imageUrls you pass with the prompt. POST /v1/images/generate runs to completion and returns the finished images with public URLs in a few seconds, charging credits × count (1–4 images per call, refunded for any that fail). An image URL from here is a valid sourceImageUrl or elementImageUrl for a video job.
curl -X POST https://api.startupaid.org/v1/images/generate \
-H "Authorization: Bearer sa_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Matte black water bottle on a stone table, soft morning light",
"model": "seedream",
"size": "16:9",
"count": 2
}'
# => { "credits": 16, "images": [
# { "id": "img_…", "url": "https://cdn.startupaid.org/image/…/1.png", "width": 1280, "height": 720, "prompt": "…" },
# { "id": "img_…", "url": "https://cdn.startupaid.org/image/…/2.png", … } ] }
# Edit reference images with a prompt:
# "model": "nano-banana-edit", "imageUrls": ["https://…/bottle.png"], "prompt": "Add a lime wedge beside it"AI credits
Every AI feature (video, images, drafted posts, templates) spends AI credits. Each plan includes a monthly allowance, and purchased credits never expire; spending draws on the allowance first. GET /v1/account/ai-credits returns what is used, allowed and remaining, so you can check before a batch. A request that would exceed the balance is refused with 402 and charges nothing. Buy credits, see the full ledger, or invite a founder to earn credits in the dashboard.
curl https://api.startupaid.org/v1/account/ai-credits \
-H "Authorization: Bearer sa_live_xxx"
# => { "used": 612, "allowance": 1000, "topup": 2000, "remaining": 2388 }
# allowance -1 means unlimited. A request beyond the balance:
# => 402 { "error": "you've used all your AI credits for this month — buy more credits or upgrade your plan" }AI agents
MCP: Claude & agents
Everything above is also available to AI agents over the Model Context Protocol as tools: send email and push, verify OTPs, schedule posts, generate video and images, check credits, convert currency. Two ways in. As a connector, add https://api.startupaid.org/mcp under Settings → Connectors in Claude (web, mobile or desktop) and sign in when asked: approving creates an API key named after the connector, revocable from API Keys. As a local server, run the npm package with an API key in Claude Desktop, Claude Code, Cursor or any MCP client. The remote endpoint speaks Streamable HTTP with OAuth 2.1 (dynamic registration, PKCE) and also accepts a plain API key as the bearer token.
Settings → Connectors → Add custom connector Name: startupaid URL: https://api.startupaid.org/mcp Claude sends you to startupaid to sign in and approve, then the tools appear in every chat. Works on claude.ai, the mobile apps and Claude Desktop.
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 with iOS Universal Links and Android App Links, plus 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.
// Mint a link from the API (or the dashboard, or the MCP create_deep_link tool):
POST /v1/deeplinks
{ "title": "Order 8921", "fallback": "https://acme.com/orders/8921",
"appPath": "/orders/8921", "data": "{"orderId": "8921"}" }
// → { "url": "https://links.startupaid.org/l/acme/aX9k2", "clicks": 0, "opens": 0, … }
// Embed the url in the email or push you send. Installed apps open straight
// to appPath; everyone else hits the web fallback, and the click is counted.
// Your app reads the link after opening it (public, no key; counts an open):
GET /l/acme/aX9k2/resolve
// → { "appPath": "/orders/8921", "fallback": "https://acme.com/orders/8921",
// "data": { "orderId": "8921" } }
// App setup: the Deep Links page gives you the exact Associated Domains
// entitlement (iOS) and intent-filter with your path prefix (Android).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. |
| 402 | payment_required | Out of AI credits for the month. Buy credits or wait for the allowance to reset; nothing was charged. |
| 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. For example, a contact with that email already exists in the audience. |
| 429 | rate_limited | You've exceeded your plan's rate limit, or have too many clips rendering at once (four), so 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 (local)
npx -y @startupaid/mcpMCP connector (remote)
https://api.startupaid.org/mcpNeed a key?
Create an account and generate one in seconds.
Social: channels
Connect X, LinkedIn, Threads, Facebook, Instagram, YouTube, TikTok or a custom signed webhook once in the dashboard; each connection becomes a channel with an id.
GET /v1/social/channelslists them. YouTube and TikTok have no text post, so they take clips (TikTok also images) and are the natural destination for a generated video’s auto-post.curl https://api.startupaid.org/v1/social/channels \ -H "Authorization: Bearer sa_live_xxx" # => { "channels": [ # { "id": "chan_9f2", "platform": "twitter", "name": "@acme", "active": true }, # { "id": "chan_7a1", "platform": "youtube", "name": "@acmehq", "active": true } ] }