Adding payments, email and SMS to a generated app
Three integrations, no SDKs, no webhook receivers — and the UI that calls them is already there.
You have a generated app. Now it needs to take money and tell people things. Each provider is a catalog entry you configure with keys — your code names a service and an operation, and never holds a secret.
The usual version of this task
Three vendors normally means three SDKs to install and keep current, three auth models, three sets of webhook receivers with three signature schemes, and a decision about where the keys live that someone will get wrong. It is a fortnight, and none of it is your product.
On Supero each provider is a catalog entry. You import it by service_id, give it keys, and call operations on it. Your application code holds no credential — it names a service and an operation.
One pattern, thirty-odd providers
The three you almost certainly want first:
| Need | service_id | Provider | Config fields |
|---|---|---|---|
| Take a card payment | stripe_checkout | Stripe | stripe_api_key, stripe_publishable_key |
| Send transactional email | email | SendGrid | sendgrid_api_key, default_from_email, `default_from_name`, sandbox_mode |
| Send an SMS | sms | Twilio | twilio_account_sid, twilio_auth_token, `default_from_number` (E.164) |
And the neighbours, once you need them: billing_razorpay and billing_paypal for regional gateways, whatsapp for WhatsApp, slack for internal alerts, push_notification for mobile, google_calendar for invites, amazon_s3 for your own bucket, auth_otp for passcode sign-in. Browse the live list with GET /api/v1/services/catalog — it is the authoritative answer to "is X supported".
Wiring the three
Add them to your app config
# config.py
services: list = field(default_factory=lambda: [
"stripe_checkout",
"email",
"sms",
"workflows", # must be LAST if the list is non-empty
])⏱ ~5 minutes
Give each one its keys
From the Services page in the dashboard, or over the API. Start with test credentials — you swap them for live keys later without a redeploy or a code change.
curl -X POST https://api.supero.dev/api/v1/services/import \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"service_id": "email",
"domain": "acme-corp",
"project_uuid": "8f2a1c40-...",
"config_data": {
"sendgrid_api_key": "SG.xxxx",
"default_from_email": "[email protected]",
"default_from_name": "Acme",
"sandbox_mode": true
}
}'⏱ ~40 minutes, mostly in vendor dashboards
Call one
Every service in the catalog is called the same way — one endpoint, a service_id, an operation, and an input object.
curl -X POST https://api.supero.dev/api/v1/services/execute \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"service_id": "email",
"operation": "send_email",
"input": {
"to_email": "[email protected]",
"subject": "Welcome to Acme",
"body_html": "<p>Thanks for signing up.</p>"
},
"domain": "acme-corp",
"project_uuid": "8f2a1c40-..."
}'From server-side Python the SDK wraps the same call with typed accessors, so you are not hand-building payloads:
from supero.service_lib import ServiceLib
sl = ServiceLib(api_key='ak_...', domain='acme-corp', project_uuid='8f2a...')
sl.email.send('[email protected]', 'Welcome to Acme',
body_html='<p>Thanks for signing up.</p>')
# raw escape hatch for anything without a typed accessor
sl.exec('sms', 'send_sms', {'to_number': '+15551234567',
'body': 'Your order shipped'})⏱ ~10 minutes
Do not call it. Bind it.
Calling a service from your controller works. Binding it to a data change works better, because then the automation runs no matter where the change came from — your UI, the API, a script, or the in-app agent.
A workflow is a declarative chain of steps registered with your app. Here is the whole "welcome email on signup" case:
{
"workflow_id": "welcome_on_signup",
"display_name": "Welcome Email",
"status": "Active", "enabled": True, "on_error": "continue",
"input_schema": {
"email": {"type": "string", "required": True},
"name": {"type": "string", "required": False},
},
"steps": [
{"id": "mail", "type": "service_call",
"service": "email", "operation": "send_email",
"on_error": "continue",
"input_map": {
"to_email": "{{input.email}}",
"subject": "Welcome to Acme",
"body_html": "<p>Hi {{input.name}}, your account is ready.</p>"}},
],
}And the binding that makes it fire on its own. @signup nests the account under user. The others must be schema-qualified — @create:myapp:order, not a bare @create — and they hand you the record itself, so a bare uuid is the record's own.
[{"event": "@signup",
"workflow_id": "welcome_on_signup",
"input_map": {"email": "user.email", "name": "user.full_name"}}]- Register both by passing
workflow_definitions=[...]andevent_bindings=[...]toAppSetup(...).run(), so they live in source control next to the schema. - A workflow is blocked when
statusisDraft,DisabledorArchived, orenabledisfalse. The comparison is exact and case-sensitive, so a typo likedraftslips past the guard and the workflow runs anyway. - Every run is a
wf:workflow_instancerecord and every step awf:workflow_step_log— query them like any other data when you need to know why something did not fire.
The UI is already wired
The generated app is not a headless backend you now have to build a front end for. It already has the admin console and the customer-facing surface, and the platform decides which one a person sees from their role — it derives a ui_view at login, so there are no role-string checks in your UI code.
For a payment, the flow that needs no custom screen at all is the hosted one: create a checkout session, redirect, and let Stripe render the payment page it already renders well.
const result = await services.stripe.checkout({
amount: 42.00, // MAJOR units — dollars, not cents
product: 'Pro plan — annual',
successUrl: window.location.origin + '/thanks',
cancelUrl: window.location.origin + '/pricing',
});
// service calls come back in an execute envelope
const output = ('output' in result) ? result.output : result;
window.location = output.checkout_url;When you do want your own screen, you have both options: a declarative ui_schemas.py entry, or hand-written ui/app.js. Pick one per app — never both in the same app.
What to plan for
- Credentials are per project. Every tenant in a project shares one Stripe account. A customer who needs their own gateway needs their own project, or their own endpoint via the plugin registry.
- Going live is a config change, not a code change — but it is a change someone has to remember to make. Test keys silently keep working.
- Some providers need approval before production. Instagram, Facebook, TikTok, LinkedIn and Google Ads all require app review or partner programmes. Check before you promise a launch date.
- Not everything is in the catalog. For your own internal HTTP service, register it in the plugin registry — it gets stored auth, retries and a circuit breaker.
Where to go from here
- The full commerce version — cart, order, inventory, refunds: Order-to-cash: six vendors, no vendor SDKs.
- A provider that is not in the catalog: Extending Supero: your own endpoints.
- Put it somewhere permanent: Deploying: Supero Cloud, your AWS, your GCP.
Build this on Supero
Describe it and get schemas, APIs, SDKs and a governed multi-tenant backend — or point your own AI editor at Supero over MCP and build it there.