Send email from a Replit app
Replit has a Secrets pane, and that is where an emails.sh key belongs: values there are exposed to your program as environment variables and are not part of the files someone forks. The request below tells the Agent to read the key from there rather than write it into a file.
Paste into the Replit Agent
Copy this. The page it names is served as markdown at that exact URL, so the assistant reads the real integration rather than guessing at an API shape.
Add transactional email to this Repl using emails.sh so signing up sends a verification email.
Read https://emails.sh/docs.md first so you use the real request shape instead of guessing it.
Requirements:
- Install the SDK (npm install @emails.sh/sdk for node, pip install emailssh for python).
- Read the key from the environment variable EMAILSSH_API_KEY. I will add it in the Secrets pane. Do not write the key into any file, do not print it, and if it is missing at runtime raise an error that names the variable.
- Do the send in server-side code only, never in code that runs in the browser.
- Send from onboarding@emails.sh until I verify my own domain.
- Tell me the exact secret name to add when you are done.You add the secret.
Open Secrets in the Repl, add EMAILSSH_API_KEY with the esh_ key from https://emails.sh/dashboard. It arrives in your program as an ordinary environment variable.
The Agent installs the client.
`npm install @emails.sh/sdk` or `pip install emailssh`, depending on what the Repl is written in.
It writes the server-side send.
A route or function that reads the key from the environment and calls POST /v1/emails, returning the id to the caller.
You run it and watch the console.
The first send returns { "id": "...", "status": "queued" }. A missing secret shows up here as a named error rather than a blank failure.
What the Agent writes
main.py, a Flask route that sends the verification email.
import os
import uuid
from emailssh import Emailssh
from flask import Flask, request, jsonify
app = Flask(__name__)
# Reading the secret at startup means a missing value fails immediately, not
# on the first signup at three in the morning.
api_key = os.environ.get("EMAILSSH_API_KEY")
if not api_key:
raise RuntimeError("EMAILSSH_API_KEY is not set. Add it in the Secrets pane.")
emails = Emailssh(api_key)
@app.post("/signup")
def signup():
email = request.json["email"]
token = str(uuid.uuid4())
link = f"https://acme.com/verify?token={token}"
result = emails.send(
from_="Acme <onboarding@emails.sh>",
to=[email],
subject="Confirm your email",
html=f'<p>Confirm your address.</p><p><a href="{link}">Confirm your email</a></p>',
text=f"Confirm your address: {link}",
idempotency_key=f"verify:{token}",
)
return jsonify({"email_id": result["id"]})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)Worth knowing
A key typed into a file is a key you published
Repls can be forked and read. Secrets are not part of what a fork copies, files are. Keep the key in Secrets only, and revoke it if it ever appeared in code.
A restart is sometimes needed after adding a secret
A process that already started does not see a secret added afterwards. Stop and run again, otherwise the code looks broken while the value is fine.
Deployments have their own secrets
The value set on the development Repl is not automatically the value the deployment sees. Check both if the published version stops sending.
The Agent invents API fields without the docs
Naming https://emails.sh/docs.md in the request is what gets `to` as an array and `html` plus `text` instead of a body shape borrowed from another provider.
It can write your lifecycle sequences as a file
An automation here is a YAML document: a trigger, an optional filter, and a list of steps that each send, wait, or branch. GET https://emails.sh/v1/automations/<id>.yaml returns it, PUT the same path replaces it, and a document you push is stored as the exact bytes you sent. So an assistant can write a trial sequence into your repository, you review the diff like any other change, and CI pushes it on merge. Errors from the parser name the wrong thing, say what to write instead, and give a line number, which is what lets an assistant correct itself. Note that this runs over the REST API rather than over MCP: there are no automation tools on the MCP server.
What arrives
One call to POST /v1/emails, and this is the message. The delivery result for it is on GET /v1/emails/:id a second later.
To: student@example.com
Subject: Confirm your email
Confirm your address: https://acme.com/verify?token=e40a72Questions
Where exactly do I put the key?
The Secrets pane of the Repl, named EMAILSSH_API_KEY. Your code reads it with os.environ or process.env.
Can I use Gmail SMTP instead?
It tends to fail on a deployed project: app passwords, rate limits, and blocked ports. An API call over HTTPS has none of those problems.
Does the free tier cover a student project?
3,000 emails a month and 100 a day, no credit card. That is a lot of test signups.
How do I know it was delivered?
GET /v1/emails/:id with the id you got back, or open the same record at https://emails.sh/dashboard.