External Integrations
Payments, email, SMS, object storage, calendars and LLM keys, invoked through one endpoint — with the credentials held in the platform keystore instead of your application code.
What It Is For
The Shape
| service_id | Apps | config_keys |
|---|---|---|
| email (SendGrid) | 146 | sendgrid_api_key, sendgrid_webhook_signing_key, default_from_email, default_from_name |
| stripe_checkout | 81 | stripe_api_key, stripe_webhook_secret |
| amazon_s3 | 30 | aws_access_key_id, aws_secret_access_key, default_bucket |
| sms (Twilio) | 20 | twilio_account_sid, twilio_auth_token, default_from_number |
| ai | 13 | openai_api_key, anthropic_api_key |
| google_calendar | 6 | google_service_account_json |
| email_smtp | 5 | smtp_host, smtp_username, smtp_password |
| billing_paypal | 2 | paypal_client_id, paypal_client_secret |
| push_notification | 2 | fcm_service_account_json |
| billing_razorpay | 1 | razorpay_key_id, razorpay_key_secret, razorpay_webhook_secret |
# config.py — shipped with the application bundle.
# These integrations arrive imported but inert. They need credentials
# per project, supplied after deploy, before any call to them succeeds.
INTEGRATIONS_NEEDING_CONFIG = [
{"service_id": "stripe_checkout",
"config_keys": ["stripe_api_key", "stripe_webhook_secret"]},
{"service_id": "email",
"config_keys": ["sendgrid_api_key", "sendgrid_webhook_signing_key",
"default_from_email", "default_from_name"]},
{"service_id": "amazon_s3",
"config_keys": ["aws_access_key_id", "aws_secret_access_key",
"default_bucket"]},
]POST /api/v1/keystore/platform-secrets
{
"service_id": "stripe_checkout",
"secret_fields": {
"stripe_api_key": "sk_live_...",
"stripe_webhook_secret": "whsec_..."
},
"is_test": false,
"label": "Acme production Stripe"
}How It Works
- 1
Import the service into the project
POST /api/v1/services/import with a service_id, a domain and a project_uuid. Import creates a service_config record and registers the provider plugin in plugin_registry, so the platform knows the integration exists before any credential does.
- 2
Supply the credential
PUT /api/v1/services/{service_id}/config with a config_data object keyed by that integration's config_keys, or seed the keystore and apply it. The response carries config_status: active when every mandatory key is present, incomplete otherwise.
- 3
Invoke an operation
POST /api/v1/services/execute with the service_id, the operation name and an input object — the same endpoint every platform service uses. There is no per-provider API to learn and no per-provider client to ship.
- 4
Receive the callback
Providers report asynchronous outcomes by calling back. That is why the config_keys include sendgrid_webhook_signing_key, stripe_webhook_secret and razorpay_webhook_secret — the signing material that verifies an inbound callback really came from the provider.
POST /api/v1/services/execute
{
"service_id": "stripe_checkout",
"operation": "create_checkout_session",
"domain": "acme",
"project_uuid": "9f3c1b6a-2e5d-4f81-b0a7-3c5e9d1f2a44",
"input": {
"op": "create_checkout_session",
"mode": "payment",
"amount": 29.99,
"currency": "usd",
"product_name": "Annual membership",
"quantity": 1,
"client_reference_id": "order-7741",
"success_url": "https://app.example.com/success?session_id={CHECKOUT_SESSION_ID}",
"cancel_url": "https://app.example.com/cancel"
}
}
// 200
{
"success": true,
"output": {
"provider_session_id": "cs_test_a1b2c3...",
"checkout_url": "https://checkout.stripe.com/c/pay/cs_test_a1b2c3...",
"status": "created"
}
}ℹ️ Stripe is a real integration, not a stub
What You Get
- •One invocation surface. Every integration is reached through POST /api/v1/services/execute with a service_id and an operation — the same endpoint that drives carts, approvals and workflows.
- •Credentials that never enter your code. The vault listing surfaces field names with values masked, and the stored secret is sealed at rest, decrypted only on the authorised read path.
- •Input validated before the provider is called: the execute wrapper checks it against the operation's input_schema, so a malformed call fails at the boundary rather than halfway through a payment.
- •Responses filtered to the operation's output_schema — the shape you code against is the shape you get, and a provider adding a field does not change it.
- •A configuration status you can act on: active or incomplete, per project, so an operator can see which integrations are actually usable.
- •Per-project credentials. The same bundle runs against a sandbox key in one project and a live key in another, with no rebuild.
- •Webhook signing material as a first-class config key on every integration that calls back, so verification is configuration rather than something you invent.
- •Ten providers across email, payments, SMS, storage, calendar, push and AI keys — see Managed Integrations.
What You Decide
⚠️ A payment integration is not a billing system
- •Reconciliation. A webhook tells you a checkout session was paid. What that means for your order record — which fields move, which transition fires, what happens if the callback arrives twice or before your own write lands — is entirely your logic.
- •Idempotency and retry. If an execute call fails or times out, whether to retry, and how to make a retry safe when the first attempt may already have charged a card, is a per-operation decision.
- •Provider-side setup. The account, the webhook URL registered in the provider dashboard, moving out of test mode, and any verification the provider demands are all outside the platform.
- •The inert period. An application ships with its integrations imported but unconfigured, and calls fail with a clear error until credentials are supplied. Whether a user can reach that path before an operator configures the project is a launch-sequencing decision, not a bug.
- •Which integration to use. email and email_smtp both send mail; billing_paypal, billing_razorpay and stripe_checkout all take money. The platform does not choose, and the choice is hard to reverse once records carry provider identifiers.
Where to Go Next
Secrets
Security & Field Encryption
How the keystore seals secrets, and how to encrypt record fields client-side. Open
On this page