External integrations
Choose the integration method that fits your task: call an API such as OpenAI, receive notifications from another service, or connect to an external database.
| What you want to do | Setup |
|---|---|
| Call an external API | Set connection details and API keys |
| Send email to the app’s users | Send with the Email SDK |
| Receive notifications from an external service | Create a webhook app token and configure a receiving endpoint |
| Call the app’s API from another internal system | Create an app token and declare allowed paths |
| Use an external database or storage service | Set connection details |
Call an external API
Section titled “Call an external API”Outbound connections from apps to external services are unrestricted, with one exception: the SMTP ports (TCP 25, 465, 587, and 2525) are always blocked. Set required API keys as secrets and call the service from your app. The external service’s contract and rate limits still apply.
To send email, use Keelson’s Email SDK or the HTTPS API of an email service you contract with, such as Resend, SendGrid, or Mailgun. Contracted services cannot be used through their SMTP relay hosts either, so configure the API client rather than an SMTP host and port.
Calls in the other direction—from an external system to your app—require Keelson login by default. For systems that cannot sign in through a browser, use the setup below.
Receive webhooks
Section titled “Receive webhooks”Webhooks notify your app about events in another service. For example, a payment notification can trigger an update to an order’s status.
Receiving a webhook requires an app token with the webhook scope, even if the sender also signs its requests. Declaring auth.endpoints alone does not allow requests authenticated only by the sender’s signature.
First, issue a token for the target app. Replace my-app with its slug.
keelson apps tokens create --app my-app --name webhook-client --scope webhookImplement a receiving handler such as POST /api/webhooks/incoming. Append one of the following paths to your app URL, for example https://acme--my-app.keelson.run, and register it with the sender.
| What the sender can configure | Setup |
|---|---|
| Custom HTTP headers | Use /api/webhooks/incoming as the receiving path and add X-Webhook-Secret: <issued-token> |
| Receiving URL only | Use /api/webhooks/<issued-token>/incoming as the receiving path |
When the token is in the URL, Keelson removes that segment and forwards the request to the app’s /api/webhooks/incoming. Treat URLs containing tokens as secrets. These requests do not include signed-in user information.
Keelson checks token validity, the target app, the webhook scope, and source IP restrictions. Currently, auth.endpoints cannot restrict webhook paths or methods. Check allowed paths and methods in your receiving handler.
If the sender signs requests, receive its signing secret through secrets and verify the signature in your app. The Keelson app token and the sender’s signing secret are separate credentials.
/api/webhooks/email and /api/webhooks/email-events are reserved by the platform and cannot be used for your own webhooks. Do not add them to auth.endpoints.
Call your app’s API from another system
Section titled “Call your app’s API from another system”Issue an app token to call the app’s API from another internal system or a script.
keelson apps tokens create --app my-app --name batch-client --scope api --allowed-ip 203.0.113.0/24Replace my-app with the target app’s slug and 203.0.113.0/24 with the actual source IP range.
- Tokens have the form
keelson_...and are displayed only once, at creation. --scope apiapplies under/api/external/;--scope webhookapplies under/api/webhooks/.--allowed-iprestricts source IPs using CIDR ranges; multiple ranges are allowed. These restrictions apply per token, separately from workspace or app IP controls.- You can configure the same settings on the app’s console page. Under
keelson apps tokens, uselistto list tokens,rotateto replace one, ordeleteto revoke one.
The caller adds Authorization: Bearer keelson_... and calls a path declared in auth.endpoints.
curl -H "Authorization: Bearer keelson_xxx" https://acme--my-app.keelson.run/api/external/statusauth: endpoints: - path: /api/external/status methods: [GET] - /api/external/import # No methods specified means all methodsBefore forwarding the request, Keelson checks that the token is valid for the app and purpose, the source IP is allowed, and the path and method match the declaration.
Paths in auth.endpoints use prefix matching, not exact matching. For example, /api/external/status also matches /api/external/status/detail and /api/external/status-old. Check allowed paths and methods in the app too. An api-scoped token cannot call undeclared APIs.
Send email
Section titled “Send email”With Keelson’s Email SDK, your app can send email to its users without contracting an email service. Use it for business messages such as password resets, request confirmations, and approval requests.
Email may only be sent as business communication to that app’s users (Acceptable Use Policy 2.3). Marketing campaigns, sending to purchased address lists, and sending to people who do not use the app are not allowed. Recurring notifications must give recipients a way to stop receiving them.
import * as email from "@keelsonhq/email";
const result = await email.send({ subject: "Request received", text: "We received request 1234.",});// Store result.send_id to match it with delivery results laterIn Python, import it with from keelson import email and call email.send(...). In Go, create a client with email.New("", "") from github.com/keelsonhq/go-sdk/email and call Send. For installation, see Keelson SDK.
- Keelson sets the endpoint and token (such as
KEELSON_EMAIL_TOKEN) at deploy time, and the SDK reads them. Nokeelson.yamlsetting is needed. Static sites and SPAs have no server to run the SDK, so they cannot send email - The sender is
<app slug>@mail.keelson.runby default. You can set the display name (from_name) and reply-to address (reply_to). On the Team plan, you can also send from an allowed address on your own domain (see below) - A message can have up to 50 recipients across To, CC, and BCC. The body needs at least one of
textorhtml - Sends are limited to 30 per 60 seconds, per app and per workspace
- The number of recipients per month depends on the plan (Plans and limits). Check this month’s usage under Email in the console’s workspace settings
Send from your own domain
Section titled “Send from your own domain”On the Team plan, apps can send from an address on a domain your workspace owns, such as [email protected]. Each workspace can register up to 10 domains, and each domain can have up to 20 allowed sender addresses. Owners and Admins set this up under Email > Domains in the console’s workspace settings. You need permission to change the domain’s DNS settings.
- Under Add Domain, enter the domain. To limit the domain to one app, choose the app in Scope; otherwise, every app in the workspace can use it.
- Add the TXT record shown under Ownership check to your DNS, then select Check status. Only one workspace can verify a given domain.
- After ownership is confirmed, three CNAME records for email signing (DKIM) appear. Add them to your DNS. Verification usually completes within minutes. Select Check status to see the current status. If the records are not found within 72 hours, verification fails; remove the domain and add it again.
- Under Allowed sender addresses, add each address your apps send from.
- In your app, pass the address as
from_address(Node and Python) orFrom(Go).
await email.send({ from_name: "Example Support", subject: "Request received", text: "We received request 1234.",});- Keelson checks the From address on every send. A send is refused unless the address exactly matches an allowed address (letters are not case-sensitive) on a verified domain of the sending app’s workspace. Removing an allowed address takes effect from the next send
- Email is signed with DKIM for your domain, so the signature is aligned with the From address for DMARC. Keelson does not use your domain for the return path (SPF), so SPF is not aligned
- The default
<app slug>@mail.keelson.runsender needs no allowed addresses and keeps working - If the workspace leaves the Team plan, sends from your own domain are refused. Registered domains and allowed addresses are kept and work again after you return to Team
- Bounces, complaints, and the suppression list work the same as for the default sender
When a send fails
Section titled “When a send fails”If a send is rejected, the SDK throws an error. The error message includes the error code.
| HTTP | Error code | Meaning |
|---|---|---|
| 400 | RECIPIENT_SUPPRESSED | A recipient is on the workspace suppression list (see below) |
| 403 | EMAIL_SENDING_SUSPENDED | Email sending is suspended for the workspace. Too many permanent bounces or complaints automatically suspend sending for the whole workspace. Contact us to lift the suspension |
| 403 | SENDER_DOMAIN_NOT_AVAILABLE | The From address is on your own domain, but the workspace is not on the Team plan |
| 403 | DOMAIN_NOT_VERIFIED | The From domain is not verified for the sending app’s workspace, or the domain is limited to another app |
| 403 | SENDER_ADDRESS_NOT_ALLOWED | The From address is not an allowed sender address for its domain |
| 422 | RECIPIENT_LIMIT_EXCEEDED | More than 50 recipients in total |
| 429 | RATE_LIMIT_EXCEEDED | The 60-second send limit was reached. Retry later |
| 429 | MONTHLY_QUOTA_EXCEEDED | This month’s recipient limit was reached. It resets next month |
| 429 | GLOBAL_RATE_LIMIT_EXCEEDED / GLOBAL_DAILY_QUOTA_EXCEEDED | Keelson’s overall sending volume temporarily reached its limit. Retry later |
| 502 | SEND_OUTCOME_UNKNOWN | Keelson could not confirm whether the message was sent. It may have been delivered, so do not retry automatically |
Receive email delivery results
Section titled “Receive email delivery results”Keelson notifies the app’s POST /api/webhooks/email-events when a message is delivered, returned because the address does not exist or for another reason (bounce), or reported as spam by the recipient (complaint). To use these results, implement a handler at this path. No app token or auth.endpoints setting is needed.
Recipients with a permanent bounce or a complaint are added to the workspace suppression list automatically, whether or not the app handles the notification. After that, no app in the same workspace can send to that address (RECIPIENT_SUPPRESSED). Owners and Admins can review and remove addresses under Email in the console’s workspace settings.
Notifications are signed. Verify the signature with the SDK before using the content. Keelson sets the signing key, KEELSON_EMAIL_WEBHOOK_SECRET, automatically.
import * as email from "@keelsonhq/email";
// Handler for POST /api/webhooks/email-events. rawBody is the request body as receivedconst event = email.verifyEventWebhookBytes( rawBody, req.headers, process.env.KEELSON_EMAIL_WEBHOOK_SECRET!,);if (event.event_type === "bounce" && event.bounce_type === "hard") { // Look up the send record by event.send_id and mark the address invalid}In Python, use email.verify_event_webhook(body, headers, secret). In Go, use email.VerifyEventWebhook(r, secret). The signature covers the raw body bytes, so pass the body before parsing it as JSON.
| Field | Content |
|---|---|
event_id | Notification ID. The same notification can arrive more than once, so use it to detect duplicates |
event_type | delivered, bounce, or complaint |
email_address | The recipient address |
send_id | The send_id returned when the message was sent. Use it to match the notification with your send record |
bounce_type | For bounces, hard (permanent) or soft (temporary) |
detail | Additional information |
provider | Name of the sending infrastructure used for delivery |
timestamp | When the event occurred |
A 2xx response completes the notification. If the handler returns 5xx or the notification cannot reach the app, Keelson retries later, up to 4 attempts in total. Notifications that receive a 4xx response are not retried.
External databases
Section titled “External databases”To connect to PostgreSQL, MySQL, or your own libSQL instance, set db.mode: none and pass connection details through secrets. See Use an external database.
External object storage
Section titled “External object storage”For files that exceed the Files / Media SDK limits, write directly to S3-compatible storage from your app. Pass credentials through secrets.