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

GroupMethodPathAuthDescription
AppsGET/v1/appsAPI keyList your apps (shared across Push & OTP)
AppsPOST/v1/appsAPI keyCreate an app; returns its unique appId
AppsPATCH/v1/apps/:appIdAPI keyRename an app
AppsDELETE/v1/apps/:appIdAPI keyDelete an app and its module resources
EmailPOST/v1/sendAPI keyQueue and send a transactional email
EmailGET/v1/messages/:idAPI keyFetch a message's delivery status
Mailing listsGET/v1/audiencesAPI keyList your audiences (mailing lists)
Mailing listsPOST/v1/audiencesAPI keyCreate an audience
Mailing listsGET/v1/audiences/:idAPI keyFetch a single audience
Mailing listsPATCH/v1/audiences/:idAPI keyRename / edit an audience
Mailing listsDELETE/v1/audiences/:idAPI keyDelete an audience and its contacts
ContactsGET/v1/audiences/:id/contactsAPI keyList contacts (search + paging)
ContactsPOST/v1/audiences/:id/contactsAPI keyAdd a contact
ContactsPOST/v1/audiences/:id/contacts/importAPI keyBulk import contacts
ContactsPATCH/v1/contacts/:idAPI keyUpdate a contact / subscription
ContactsDELETE/v1/contacts/:idAPI keyRemove a contact
CurrencyGET/v1/convertAPI keyConvert an amount between any two currencies
CurrencyGET/v1/rates/:baseAPI keyCross-rates for a base currency
CurrencyGET/v1/currenciesAPI keyList supported fiat & crypto currencies
CurrencyGET/v1/currency/statusAPI keyRate freshness by asset class
PushPOST/v1/push/apps/:appId/devicesAPI keyRegister an end-user's device token
PushGET/v1/push/apps/:appId/devicesAPI keyList a user's registered devices
PushDELETE/v1/push/apps/:appId/devicesAPI keyRemove a device token
PushPOST/v1/push/apps/:appId/sendAPI keySend a notification (iOS, Android & web)
PushGET/v1/push/apps/:appId/messages/:idAPI keyPer-device delivery breakdown
WhatsApp OTPPOST/v1/otp/sendAPI keySend a one-time passcode over WhatsApp
WhatsApp OTPPOST/v1/otp/verifyAPI keyVerify a code the recipient submitted
SocialGET/v1/social/channelsAPI keyList connected channels and their ids
SocialPOST/v1/social/scheduleAPI keySchedule or publish a post to connected channels
SocialGET/v1/social/postsAPI keyList your scheduled & published posts
LoyaltyGET/v1/loyalty/programsAPI keyList your rewards programs
LoyaltyPOST/v1/loyalty/programsAPI keyCreate a program (tiers, referral bonuses, expiry, points per spend)
LoyaltyPOST/v1/loyalty/programs/:id/awardAPI keyCredit points to a member by email or phone, by count or by activity key
LoyaltyGET/v1/loyalty/programs/:id/contactsAPI keyLook up a member's balance, tier and expiry by ?email= or ?phone=
LoyaltyPOST/v1/loyalty/programs/:id/redeemAPI keySpend a member's points on a catalog reward (till or app)
LoyaltyPOST/v1/loyalty/programs/:id/referralsAPI keyRecord that one member referred another; both sides earn
LoyaltyPOST/v1/loyalty/programs/:id/codesAPI keyMint a code: once per person or recurring, with start/end and a QR
LoyaltyGET/v1/loyalty/programs/:id/leaderboardAPI keyTop earners in a program
LoyaltyPOST/hooks/loyalty/:programId/shopifyPublicShopify order-paid webhook: paid orders earn points
LoyaltyPOST/hooks/loyalty/:programId/stripePublicStripe checkout / payment webhook: payments earn points
OutcomesPOST/v1/eventsAPI keyRecord a conversion (signup, purchase…) and attribute it to the email that drove it
AI VideoGET/v1/video/configAPI keyModels, rates, lengths, resolutions and the pricing schedule
AI VideoPOST/v1/video/quoteAPI keyPrice a clip in credits without making it
AI VideoPOST/v1/video/jobsAPI keyGenerate a clip (charges credits, returns the queued job)
AI VideoGET/v1/video/jobsAPI keyList recent jobs
AI VideoGET/v1/video/jobs/:idAPI keyJob status, video and thumbnail URLs
AI ImagesGET/v1/images/configAPI keyImage models, per-image prices and sizes
AI ImagesPOST/v1/images/generateAPI keyGenerate 1–4 images or edit reference images
AI ImagesGET/v1/imagesAPI keyList generated images
AI creditsGET/v1/account/ai-creditsAPI keyCredits used, allowed and remaining this month
Deep linksPOST/v1/deeplinksAPI keyCreate a link that opens your app (web fallback, click and open counts)
Deep linksGET/v1/deeplinksAPI keyList your deep links
Deep linksGET/l/:key/:slug/resolvePublicThe link's in-app path and data, read by your app after it opens
AI agentsPOST/mcpOAuth or API keyModel Context Protocol endpoint (Streamable HTTP)
SystemGET/healthPublicLiveness 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.

infoThe Free plan includes one app. Paid plans raise the limit (and Scale is unlimited), so upgrade or contact us to add more.
# 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"

Email

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 code and 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

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 } ] }

Social: schedule a post

Schedule a post to any connected channels with one call. Pass the channels ids and your content; add an optional imageUrl (required for Instagram), a videoUrl (an MP4, for example the videoUrl of a finished video job) and a scheduledFor time, or omit it to publish now. A clip is attached natively on X, Instagram, Facebook, Threads, YouTube and TikTok and travels as a link on LinkedIn. YouTube needs a clip and TikTok a clip or an image; a text-only post to either is rejected with a 400 before anything is queued. Fetch GET /v1/social/posts for 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": "…" }
# Attach a clip instead: "videoUrl": "https://…/take.mp4" (posted natively on
# X, Instagram, Facebook, Threads, YouTube and TikTok; a link elsewhere).
# List posts + per-channel results:  GET /v1/social/posts

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

Reference

Errors

Errors return a JSON body with an error field and a conventional HTTP status code.

StatusCodeMeaning
400bad_requestThe payload failed validation (missing or malformed fields, bad email).
401unauthorizedMissing or invalid API key.
402payment_requiredOut of AI credits for the month. Buy credits or wait for the allowance to reset; nothing was charged.
403forbiddenThe from-address isn't a domain you've verified for this account.
404not_foundThe requested resource does not exist.
409conflictDuplicate. For example, a contact with that email already exists in the audience.
429rate_limitedYou've exceeded your plan's rate limit, or have too many clips rendering at once (four), so back off and retry.
502provider_errorAn upstream provider (SMTP or FX) failed; safe to retry.

Official SDKs

javascript

Node.js / TypeScript

npm i @startupaid/sdk
code

Python

pip install startupaid
terminal

Go

go get github.com/StartupAid-Org/startupaid-go
smart_toy

MCP server (local)

npx -y @startupaid/mcp
hub

MCP connector (remote)

https://api.startupaid.org/mcp

Need a key?

Create an account and generate one in seconds.

Get your API key