Implement email on a client site¶
Purpose¶
Explain how a portfolio (client) site uses the MKP Works email system — what you configure on the platform, what you install on the client, which APIs to call, and how to hand the work to another developer or LLM with a self-contained brief.
What this system is (and is not)¶
MKP Works owns outbound, template-based transactional email. You do not run SMTP, SES, SendGrid, Resend, Nodemailer, or any mail SDK on the client site for this flow. The platform renders Django email templates, delivers mail, and writes an audit row (EmailDelivery).
| Supported | Not supported |
|---|---|
| Confirmation / notification mail after a form submit | Inbound mail / reading a mailbox |
| Explicit templated sends from your trusted backend | Arbitrary “send any email to anyone” from the browser |
| Templates and recipients configured in platform admin | Client sites defining templates in their own code |
There are two client integration paths. Pick one (or both):
- Forms path (browser) — User submits a form; MKP sends confirmation + staff notification automatically.
- Server notifications path (backend) — Your server calls MKP to send a named template.
Browser clients cannot send arbitrary mail. Never put server secrets in frontend code.
Live prototype¶
End-to-end form → email on this platform (docs ClientSite mkpworks-docs):
Working email-sending form
Submit with your email. Confirmation + staff notification are sent by the email gateway. Check admin Email deliveries.
Code to paste on your client site¶
<!-- Loads when the live demo config is available -->
Server-notification live control (docs proxy; production clients use server secrets on the backend only):
Server notification demo
Prerequisites (platform admin)¶
Do this once in Django Admin (/admin/) before coding the client site.
- ClientSite exists, status active, with your product domain(s) on AllowedDomain.
- Credentials
- Public site key (
pk_...) — browser / forms only. - Server credential —
X-MKP-Client-Id(skpub_...) +X-MKP-Client-Secret(sk_...) for trusted backends. The raw secret is shown once when issued. support_emailon the ClientSite — required for form staff notifications and for templates that use recipient modesite_support_address.- EmailTemplate rows for your site (or global templates), each with a unique
code(for exampleforms.submission_received,forms.staff_notification,orders.shipped). - For the forms path: a FormDefinition with fields, linked to confirmation and/or notification templates.
Recipient modes (important for server sends)¶
When your backend calls the notifications API, the template’s allowed_recipient_mode can override the recipient you pass:
| Mode | Effective recipient |
|---|---|
submitted_email |
The recipient you provide |
site_support_address |
Forced to ClientSite.support_email |
configured_address |
Forced to the template’s configured_recipient |
authenticated_user |
Forced to the authenticated user’s email (user required) |
staff_only |
Requires a staff user; otherwise denied |
The forms path uses an internal force-recipient flag, so confirmation goes to the submitter and staff mail goes to support_email even when modes would otherwise rewrite recipients.
Template variables¶
Templates use Django template language. Forms pass context like:
{{ form.name }},{{ form.slug }}{{ payload.email }},{{ payload.name }}, … (submission fields){{ submission }}(submission object)
For server notifications, you supply context yourself; reference those keys in the subject/body templates (for example {{ order_id }}, {{ payload.name }}).
Path A — Forms email (browser / JavaScript)¶
Use this when contact, waitlist, or lead forms should trigger confirmation and/or staff notification without your backend sending mail.
What to install on the client site¶
| Item | Required? | Notes |
|---|---|---|
Hosted script mkp.js |
Yes (easiest) | No npm package. Zero JS dependencies. |
| npm / Node mail libraries | No | Do not install Nodemailer, Resend, etc. for this |
| Server secret | No | Use site key only in the browser |
Hosted script URL (production example):
Local / custom base URL: {PLATFORM_BASE_URL}/sdk/v1/mkp.js.
Embed example¶
<script src="https://mkpworks.com/sdk/v1/mkp.js"></script>
<script>
MKP.init({
baseUrl: "https://mkpworks.com",
siteKey: "pk_YOUR_SITE_KEY"
});
</script>
<div data-mkp="form" data-form="contact"></div>
Replace contact with your form slug. The page origin must match an allowed domain for the site.
API endpoints (forms)¶
Base: {PLATFORM}/api/v1/
Get form schema¶
Submit (triggers emails)¶
POST /api/v1/forms/{form_slug}/submissions/
X-MKP-Site-Key: pk_...
Origin: https://your-client-domain.example
Content-Type: application/json
{
"data": {
"name": "Ada",
"email": "ada@example.com",
"message": "Hello",
"_hp": "",
"_t": "1710000000.0"
},
"source_url": "https://your-client-domain.example/contact"
}
Flat body is also accepted (email, message, source_url at the top level). Honeypot _hp should stay empty; _t is a timing token used by spam controls.
Success: 201 with submission fields (id, status, email, created_at, spam_score, …).
Email side effects (only when status is received and an email is present; skipped for spam/quarantine):
- If the form has a notification template and the site has
support_email→ staff notification. - If the form has a confirmation template → confirmation to the submitter.
There is no browser API to send mail directly.
Path B — Server notifications (trusted backend)¶
Use this when your product backend should send a named template (order updates, account events, custom product mail) after some server-side event.
What to install on the client site¶
| Stack | Install |
|---|---|
| Python | Prefer the platform SDK at sdk/python/mkp_client (add to PYTHONPATH or install as a local package). Runtime dependency: httpx. |
| Any language | Nothing required beyond an HTTP client. Call the REST endpoint with the headers below. |
| Browser / frontend | Do not call this endpoint. Do not ship sk_... secrets to the client. |
| Mail SDKs (SendGrid, etc.) | Not used for MKP transactional mail |
API endpoint¶
POST /api/v1/server/notifications/
X-MKP-Client-Id: skpub_...
X-MKP-Client-Secret: sk_...
Content-Type: application/json
Accept: application/json
{
"template": "forms.submission_received",
"recipient": "user@example.com",
"context": {
"payload": { "name": "Ada" },
"order_id": "ord_123"
}
}
Response 200:
status may be sent or failed. The recipient in the response is masked for privacy. Failed deliveries are audited on the platform; treat non-sent as a soft failure in your product logic.
Python SDK example¶
from mkp_client import MKPClient
client = MKPClient(
base_url="https://mkpworks.com",
client_id="skpub_...",
client_secret="sk_...",
)
result = client.notifications.send(
"forms.submission_received",
"user@example.com",
context={"payload": {"name": "Ada"}},
)
# result -> {"status": "sent"|"failed", "recipient": "u***@..."}
Raw HTTP example (any language)¶
curl -sS -X POST "https://mkpworks.com/api/v1/server/notifications/" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "X-MKP-Client-Id: skpub_..." \
-H "X-MKP-Client-Secret: sk_..." \
-d '{
"template": "forms.submission_received",
"recipient": "user@example.com",
"context": {"payload": {"name": "Ada"}}
}'
Replit development mode (saved Repl IDs)¶
Use this when the client site runs on Replit and you want browser forms to call this platform without adding every preview URL as an Allowed Domain, and without opening all of Replit.
Production ignores saved Repl IDs. Only the development settings module sets MKP_ALLOW_REPLIT_DEV_IDS=True.
What you do (step by step)¶
- Run this platform in development on Replit (
DJANGO_SETTINGS_MODULE=mkpworks.settings.development). Confirm the platform preview URL loads (that URL is your{PLATFORM_BASE_URL}). - On the client Repl, get its ID in the Shell:
Copy the value (example:
abc123def456). - In platform admin → Client sites → your site → Allowed domains → add the current Replit preview hostname (from the browser address bar).
- One ID per line, or comma-separated.
- Add every client Repl you will test from (only your IDs).
- Issue a public site key for that ClientSite if you do not already have one (
pk_...). - On the client site, point
mkp.jsat the platform Repl root (not a subdomain):Replace<script src="{PLATFORM_BASE_URL}/sdk/v1/mkp.js"></script> <script> MKP.init({ baseUrl: "{PLATFORM_BASE_URL}", siteKey: "pk_YOUR_SITE_KEY" }); </script> <div data-mkp="form" data-form="contact"></div>{PLATFORM_BASE_URL}with this platform’s Replit HTTPS origin (for examplehttps://your-platform-host.replit.dev). - Open the client Repl preview and submit the form. A matching saved Repl ID on a
*.replit.dev/*.replit.app/*.repl.coOrigin is enough for Origin checks in development. - Confirm mail side effects in platform admin (Email deliveries). In development, message bodies usually print to the platform console (
EMAIL_BACKENDconsole).
What this does / does not do¶
| Layer | Behavior in Replit dev mode |
|---|---|
| CORS (development) | Allows browser calls from Replit preview host patterns so preflight works |
| Form Origin check | Allows only Origins whose hostname is a Replit preview host and contains one of this site’s saved Repl IDs |
| Other people’s Repls | Rejected (their REPL_ID is not on your ClientSite) |
| Production | MKP_ALLOW_REPLIT_DEV_IDS=False; use exact Allowed Domains only |
| Server notifications | Unaffected — use server secrets from your backend; no browser Origin gate |
Optional: exact domain instead of Repl ID¶
You can still add the client preview hostname under Allowed Domains (exact host). Saved Repl IDs are the less brittle option when preview URLs change but REPL_ID stays stable.
Checklist for a new client site¶
- Register ClientSite + allowed domain(s) +
support_email. - For Replit client testing: add saved Replit ids on the ClientSite (see above).
- Issue site key and/or server secret.
- Create EmailTemplate(s) with stable
codevalues and correct recipient modes. - Forms path: create FormDefinition, wire confirmation/notification templates, embed
mkp.js(or call form APIs with site key). - Server path: store server credentials only on the backend; call
POST /api/v1/server/notifications/orMKPClient.notifications.send. - Send a test (admin test-send and/or real submit/API call); confirm
EmailDeliveryrows in admin. - In production, the platform must have a working Django
EMAIL_BACKEND/ SMTP (or equivalent). Client sites do not configure SMTP for this feature. Use Allowed Domains (not Repl IDs) for real product hosts.
Related Pages¶
LLM implementation brief¶
Copy everything in this section into another chat when you want an LLM to implement MKP email on a different client codebase. Fill in the bracketed values first.
# Task: Implement MKP Works Platform transactional email on this client site
## Product facts (do not invent alternatives)
- MKP Works Platform owns outbound template-based transactional email.
- Client sites do NOT install SMTP/SendGrid/Resend/Nodemailer/Mailgun/SES SDKs for this feature.
- There is NO inbound mailbox API.
- Browser code MUST NOT call the server notifications API and MUST NEVER contain server secrets.
- Platform base URL: {{PLATFORM_BASE_URL}} (example: https://mkpworks.com)
- Client site slug: {{SITE_SLUG}}
- Public site key (browser only): {{SITE_KEY}} # pk_...
- Server client id: {{CLIENT_ID}} # skpub_...
- Server client secret: {{CLIENT_SECRET}} # sk_... (backend env only)
- Allowed browser origin / domain: {{CLIENT_ORIGIN}} # AllowedDomain in admin, OR (Replit dev) saved Repl ID
- Optional client Replit REPL_ID for platform ClientSite.replit_ids: {{CLIENT_REPL_ID}}
- Site support email (platform admin field): {{SUPPORT_EMAIL}}
- If testing from Replit: platform must use development settings; admin ClientSite.replit_ids must include {{CLIENT_REPL_ID}} from `echo $REPL_ID` on the client Repl; mkp.js baseUrl must be the platform Repl HTTPS origin.
## Choose integration mode for this site
Mode: {{FORMS | SERVER_NOTIFICATIONS | BOTH}}
### Mode FORMS — browser form submit triggers email
Platform admin must already have:
- Active FormDefinition slug: {{FORM_SLUG}} (example: contact)
- Optional confirmation EmailTemplate code linked on the form
- Optional notification EmailTemplate code linked on the form
- ClientSite.support_email set if staff notification is required
Client install:
1. Load hosted script (no npm package, no JS dependencies):
<script src="{{PLATFORM_BASE_URL}}/sdk/v1/mkp.js"></script>
2. Init and mount:
MKP.init({ baseUrl: "{{PLATFORM_BASE_URL}}", siteKey: "{{SITE_KEY}}" });
<div data-mkp="form" data-form="{{FORM_SLUG}}"></div>
3. Alternatively, without mkp.js, call the REST APIs below with fetch/XHR from the browser using only X-MKP-Site-Key.
APIs:
- GET {{PLATFORM_BASE_URL}}/api/v1/forms/{{FORM_SLUG}}/
Headers: X-MKP-Site-Key: {{SITE_KEY}}
- POST {{PLATFORM_BASE_URL}}/api/v1/forms/{{FORM_SLUG}}/submissions/
Headers:
X-MKP-Site-Key: {{SITE_KEY}}
Origin: {{CLIENT_ORIGIN}}
Content-Type: application/json
Body (JSON):
{
"data": {
"name": "<string>",
"email": "<string>",
"message": "<string>",
"_hp": "",
"_t": "<unix_timestamp_float_as_string>"
},
"source_url": "{{CLIENT_ORIGIN}}/..."
}
Expected: HTTP 201 JSON with id, status, email, created_at, spam_score.
Side effect: if status is received and email present, platform sends linked confirmation/notification templates.
Do not implement a separate "send email" call in the browser.
### Mode SERVER_NOTIFICATIONS — trusted backend sends named templates
Platform admin must already have EmailTemplate rows with codes you will call, e.g.:
- {{TEMPLATE_CODE}} (example: forms.submission_received)
Recipient mode on each template matters:
- submitted_email → uses request recipient
- site_support_address → forced to ClientSite.support_email
- configured_address → forced to template.configured_recipient
- authenticated_user / staff_only → special cases; prefer submitted_email or configured_address for product mail
Client install (Python preferred):
- Copy/install platform package from MKP repo path sdk/python/mkp_client
- Dependency: httpx
- Env vars on the CLIENT BACKEND only:
MKP_BASE_URL={{PLATFORM_BASE_URL}}
MKP_CLIENT_ID={{CLIENT_ID}}
MKP_CLIENT_SECRET={{CLIENT_SECRET}}
Python usage:
from mkp_client import MKPClient
client = MKPClient(base_url=MKP_BASE_URL, client_id=MKP_CLIENT_ID, client_secret=MKP_CLIENT_SECRET)
client.notifications.send("{{TEMPLATE_CODE}}", "user@example.com", context={...})
REST (any language):
POST {{PLATFORM_BASE_URL}}/api/v1/server/notifications/
Headers:
X-MKP-Client-Id: {{CLIENT_ID}}
X-MKP-Client-Secret: {{CLIENT_SECRET}}
Content-Type: application/json
Accept: application/json
Body:
{
"template": "{{TEMPLATE_CODE}}",
"recipient": "user@example.com",
"context": { "any": "keys referenced by the Django email templates" }
}
Response 200:
{ "status": "sent" | "failed", "recipient": "u***@example.com" }
Treat status != "sent" as delivery failure in app logic. Recipient is masked.
## Hard constraints for the implementing agent
1. Do not add email provider packages to the client site for MKP mail.
2. Do not expose sk_ / Client-Secret to frontend, mobile apps, or public repos.
3. Do not create client-side endpoints that proxy arbitrary free-form email; only call MKP with template codes.
4. Match Origin / domain allowlist for form submits.
5. Template HTML/subject/body are authored in MKP Django Admin, not in the client repo (unless this task explicitly includes asking an operator to create them).
6. Platform login has no email verification step — do not confuse transactional mail with people auth.
## Acceptance criteria
- [ ] Chosen mode(s) work against {{PLATFORM_BASE_URL}}
- [ ] Secrets only in backend env for SERVER_NOTIFICATIONS
- [ ] Forms path uses site key + mkp.js or documented form APIs only
- [ ] Successful send visible as EmailDelivery in platform admin (masked recipient)
- [ ] No client SMTP configuration required