NEWSupero Studio — generate, control, and deploy production apps.Start free →HIRINGSee the roles →

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.

IntegrationsBuild estimate ~1 hour7 min readAugust 25, 2026

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.

Explorer

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.

3
providers, zero SDKs installed
1
endpoint for every one of them
~1 h
mostly waiting on vendor dashboards
The shape

One pattern, thirty-odd providers

The three you almost certainly want first:

Needservice_idProviderConfig fields
Take a card paymentstripe_checkoutStripestripe_api_key, stripe_publishable_key
Send transactional emailemailSendGridsendgrid_api_key, default_from_email, `default_from_name`, sandbox_mode
Send an SMSsmsTwiliotwilio_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".

The walkthrough

Wiring the three

Add them to your app config

python
# 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.

bash
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.

bash
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:

python
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

The better version

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:

python
{
  "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.

json
[{"event": "@signup",
  "workflow_id": "welcome_on_signup",
  "input_map": {"email": "user.email", "name": "user.full_name"}}]
  • Register both by passing workflow_definitions=[...] and event_bindings=[...] to AppSetup(...).run(), so they live in source control next to the schema.
  • A workflow is blocked when status is Draft, Disabled or Archived, or enabled is false. The comparison is exact and case-sensitive, so a typo like draft slips past the guard and the workflow runs anyway.
  • Every run is a wf:workflow_instance record and every step a wf:workflow_step_log — query them like any other data when you need to know why something did not fire.
The part you do not build

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.

javascript
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.

Know the edges

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.
Next

Where to go from here

Delivery estimates in this guide are for the scope described, built by one experienced developer with any vendor sandbox credentials already in hand. Treat them as a realistic starting point, not a commitment — and check the documentation for the authoritative behaviour of any API named here.

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.