portaly-email
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePortaly Vibe Invitation Email Integration
Portaly Vibe 邀请邮件集成
Use this skill to help a human user wire up the registration link from Portaly Vibe invitation emails to the right landing page.
使用此功能帮助用户将Portaly Vibe邀请邮件中的注册链接配置到正确的着陆页。
Concept
核心概念
When a creator's follower clicks the CTA in a Portaly invitation email, the request always hits Portaly first at — that endpoint is the central click tracker (rate limit, click-event log, attribution). Portaly then 302-redirects the user to a waitlist landing page.
https://portaly.ai/r/{referralCode}Two modes decide where that redirect lands:
| Mode | Where the user lands | Setup |
|---|---|---|
| A. Hosted (default) | | None |
| B. Self-hosted | | Set |
Mode is per-merchant, decided by whether is set. Toggling mode takes effect within ~60 seconds (Portaly's edge cache TTL) and applies to every email already in flight.
creatorSubscriptionConfig.appBaseUrl当创作者的粉丝点击Portaly邀请邮件中的CTA按钮时,请求会先到达Portaly的端点——这是中央点击追踪器(负责速率限制、点击事件日志、归因分析)。随后Portaly会通过302重定向将用户引导至等待列表着陆页。
https://portaly.ai/r/{referralCode}两种模式决定了跳转的目标页面:
| 模式 | 用户跳转至 | 设置方式 |
|---|---|---|
| A. 托管模式(默认) | | 无需设置 |
| B. 自托管模式 | | 设置 |
模式按商家单独设置,由是否存在决定。切换模式后约60秒生效(Portaly边缘缓存的TTL),且对所有已发出的邮件立即生效。
creatorSubscriptionConfig.appBaseUrlEmail Types Reference
邮件类型参考
Portaly Vibe sends five email types on the merchant's behalf. Only the bottom two contain a registration link and use the Mode A/B redirect logic above — the rest are pure transactional notifications.
| Template type | Triggered by | Contains a link? | Common reason to disable |
|---|---|---|---|
| | No | The vibe coder's app already sends its own welcome email |
| Payment callback (status | No | The vibe coder customizes the upgrade email in their own product |
| | No | The vibe coder wants control over cancellation timing/copy |
| | Yes (Mode A/B) | Rarely disabled — this is the campaign feature itself |
| | Yes (Mode A/B) | Rarely disabled — confirms the signup |
Portaly Vibe代表商家发送五种类型的邮件。只有最后两种包含注册链接,并采用上述A/B模式的跳转逻辑——其余均为纯事务性通知。
| 模板类型 | 触发条件 | 是否包含链接? | 常见禁用原因 |
|---|---|---|---|
| | 否 | Vibe开发者的应用已自行发送欢迎邮件 |
| 支付回调(状态为 | 否 | Vibe开发者在自有产品中自定义了升级邮件 |
| | 否 | Vibe开发者希望控制取消订阅的时机/文案 |
| | 是(采用A/B模式) | 极少禁用——这是核心的活动功能 |
| | 是(采用A/B模式) | 极少禁用——用于确认注册 |
Disabling a template
禁用模板
Per merchant, per type:
bash
curl -X PUT https://portaly.ai/api/creator-email/templates/welcome_free \
-H "Authorization: Bearer ${PORTALY_API_KEY}" \
-H "Content-Type: application/json" \
-d '{ "enabled": false }'Re-enable by sending . Disabling takes effect immediately for new triggers — already-enqueued outbox rows still send.
{ "enabled": true }Avoiding double emails. If the vibe coder has their own welcome / upgrade / cancellation flow, disable the matching template before wiring(seesyncToPortaly) or the payment callback handler (seeportaly-user). Otherwise every existing user the first bulk sync touches gets one Portalyportaly-payment, and every successful checkout gets one Portalywelcome_freeon top of the vibe coder's own message.welcome_paid
可按商家、按类型禁用:
bash
curl -X PUT https://portaly.ai/api/creator-email/templates/welcome_free \\
-H "Authorization: Bearer ${PORTALY_API_KEY}" \\
-H "Content-Type: application/json" \\
-d '{ "enabled": false }'发送即可重新启用。禁用后对新触发的邮件立即生效——已进入发件队列的邮件仍会发送。
{ "enabled": true }避免重复邮件。如果Vibe开发者自有欢迎/升级/取消订阅流程,请在配置(详见syncToPortaly)或支付回调处理器(详见portaly-user)之前禁用对应的模板。否则首次批量同步时,每个现有用户都会收到一封Portaly的portaly-payment邮件,每笔成功结账都会在Vibe开发者自有邮件之外再收到一封Portaly的welcome_free邮件。welcome_paid
API Host
API主机
https://portaly.aihttps://portaly.aiAuthentication
身份验证
Same Creator Subscription API Key ( / ) used by .
pcs_live_*pcs_test_*portaly-payment使用与相同的创作者订阅API密钥( / )。
portaly-paymentpcs_live_*pcs_test_*Workflow
工作流程
Step 1 — Choose Mode
步骤1 —— 选择模式
Before writing any code, ask the human user which mode they want and wait for an explicit answer:
Portaly Vibe sends invitation emails on behalf of creators. The CTA in those emails goes through Portaly for click tracking, then redirects to a waitlist landing page. You have two options:
- A. Hosted (recommended for fastest launch) — Use Portaly's hosted waitlist page. No server-side work. The page is generic but functional. Best when you don't have a brand reason to host it yourself.
- B. Self-hosted (recommended for brand consistency) — Host
on your own domain. Full control over UI, copy, and post-signup flow. Requires implementing the page and registering your/waitlist/[creatorSlug]with Portaly.appBaseUrlWhich would you like? You can switch later.
If the user picks A, jump to Mode A — Hosted CTA. If B, jump to Mode B — Self-hosted Waitlist.
在编写任何代码之前,询问用户想要哪种模式并等待明确答复:
Portaly Vibe代表创作者发送邀请邮件。邮件中的CTA按钮会先经过Portaly进行点击追踪,再重定向至等待列表着陆页。你有两个选项:
- A. 托管模式(推荐快速上线) —— 使用Portaly托管的等待列表页面。无需服务器端开发。页面为通用样式但功能完整。适合无品牌化需求的场景。
- B. 自托管模式(推荐保持品牌一致性) —— 在自有域名下托管
页面。可完全控制UI、文案和注册后流程。需要实现该页面并向Portaly注册/waitlist/[creatorSlug]。appBaseUrl你想要选择哪种?后续可随时切换。
如果用户选择A,跳转至模式A —— 托管CTA;如果选择B,跳转至模式B —— 自托管等待列表。
Mode A — Hosted CTA
模式A —— 托管CTA
See for full snippets.
references/hosted-cta.mdWhat to do:
- Confirm is empty (it is by default). If the merchant previously enabled Mode B, clear it:
appBaseUrl- Vibe MCP (preferred): call with
vibe_update_brand— no API key needed.{ "appBaseUrl": "" } - REST fallback: with
PUT /api/creator-subscription/config.{ "appBaseUrl": "" }
- Vibe MCP (preferred): call
- Find the creator's slug — returns the merchant config. The slug also appears in the Portaly Vibe Dashboard.
GET /api/creator-subscription/config - Embed the CTA URL in the vibe coder's app, email signature, social bio, etc.:
https://portaly.ai/waitlist/{creatorSlug} - No server-side implementation needed. Portaly serves the page, accepts the signup form, and stores the waitlist row.
That's it for Mode A. The creator can start sending invitation emails immediately — every click lands on Portaly's hosted page.
完整代码片段详见。
references/hosted-cta.md操作步骤:
- 确认为空(默认状态)。如果商家之前启用过模式B,请清空该值:
appBaseUrl- 优先使用Vibe MCP:调用接口并传入
vibe_update_brand——无需API密钥。{ "appBaseUrl": "" } - REST备选方案:调用接口并传入
PUT /api/creator-subscription/config。{ "appBaseUrl": "" }
- 优先使用Vibe MCP:调用
- 查找创作者的slug——调用接口返回商家配置。slug也可在Portaly Vibe控制台中查看。
GET /api/creator-subscription/config - 将CTA URL嵌入Vibe开发者的应用、邮件签名、社交账号简介等位置:
https://portaly.ai/waitlist/{creatorSlug} - 无需服务器端实现。Portaly会提供页面、处理注册表单并存储等待列表数据。
模式A的操作到此结束。创作者可立即开始发送邀请邮件——所有点击都会跳转到Portaly的托管页面。
Mode B — Self-hosted Waitlist
模式B —— 自托管等待列表
See for complete code templates (Next.js, React SPA, plain HTML).
references/self-hosted-waitlist.md完整代码模板(Next.js、React SPA、纯HTML)详见。
references/self-hosted-waitlist.mdStep B1 — Register appBaseUrl
appBaseUrl步骤B1 —— 注册appBaseUrl
appBaseUrlVibe MCP (preferred): call with — no needed.
vibe_update_brand{ "appBaseUrl": "https://your-app.example.com" }PORTALY_API_KEYREST fallback:
bash
curl -X PUT https://portaly.ai/api/creator-subscription/config \
-H "Authorization: Bearer ${PORTALY_API_KEY}" \
-H "Content-Type: application/json" \
-d '{ "appBaseUrl": "https://your-app.example.com" }'Constraints (enforced by Portaly):
- Must be HTTPS
- Max 255 characters
- Trailing slashes are stripped automatically
- Empty string clears the field (= switches back to Mode A)
优先使用Vibe MCP:调用接口并传入——无需。
vibe_update_brand{ "appBaseUrl": "https://your-app.example.com" }PORTALY_API_KEYREST备选方案:
bash
curl -X PUT https://portaly.ai/api/creator-subscription/config \\
-H "Authorization: Bearer ${PORTALY_API_KEY}" \\
-H "Content-Type: application/json" \\
-d '{ "appBaseUrl": "https://your-app.example.com" }'约束条件(由Portaly强制执行):
- 必须为HTTPS协议
- 最大长度255字符
- 自动去除末尾斜杠
- 空字符串会清空该字段(即切换回模式A)
Step B2 — Implement /waitlist/[creatorSlug]
/waitlist/[creatorSlug]步骤B2 —— 实现/waitlist/[creatorSlug]
页面
/waitlist/[creatorSlug]The path must be — Portaly's redirect target is hard-coded. Anything else and the user hits a 404.
/waitlist/{creatorSlug}The page receives query params from Portaly's redirect — preserve them when posting back:
| Param | Purpose |
|---|---|
| Referral code, must be passed back to attribute the signup |
| Always |
| Campaign id (optional) |
| Outbox id, identifies the specific recipient (optional) |
The page must call two Portaly endpoints:
- — returns
GET https://portaly.ai/api/waitlist/{creatorSlug}. Use it to render the headline ({ data: { creator: { slug, merchantName }, count } }) and signup count.Join {merchantName}'s waitlist - — body
POST https://portaly.ai/api/waitlist/{creatorSlug}. Returns{ email, name?, source?, ref? }.{ data: { joined, alreadyOnList, creator } }
Both endpoints are public (no API key needed). The POST is rate-limited per IP (5/hour per creator) and per creator (200/hour total) — show the user a "try again shortly" message on .
429路径必须为——Portaly的重定向目标是硬编码的。任何其他路径都会导致用户访问404页面。
/waitlist/{creatorSlug}页面会从Portaly的重定向中接收查询参数——提交表单时需保留这些参数:
| 参数 | 用途 |
|---|---|
| 推荐码,必须传回以实现注册归因 |
| 固定为 |
| 活动ID(可选) |
| 发件箱ID,用于识别具体收件人(可选) |
页面必须调用两个Portaly接口:
- ——返回
GET https://portaly.ai/api/waitlist/{creatorSlug}。用于渲染标题({ data: { creator: { slug, merchantName }, count } })和注册人数。加入{merchantName}的等待列表 - ——请求体为
POST https://portaly.ai/api/waitlist/{creatorSlug}。返回{ email, name?, source?, ref? }。{ data: { joined, alreadyOnList, creator } }
两个接口均为公开接口(无需API密钥)。POST接口按IP(每个创作者每小时5次)和按创作者(每小时总共200次)进行速率限制——收到响应时需向用户显示"请稍后重试"的提示。
429Step B3 — Wire to user sync (optional but recommended)
步骤B3 —— 对接用户同步(可选但推荐)
The signup is a new user from your perspective. After the POST succeeds, fire-and-forget a call so the creator can see the new follower in the Portaly Dashboard. See portaly-user/SKILL.md Step 5 for the helper.
syncToPortaly([{ email, name, status: 'active' }])ts
// after POST /api/waitlist succeeds
syncToPortaly([{ email, name }]).catch((err) =>
console.error('[Portaly Sync]', err)
)从你的角度来看,注册用户是新用户。POST接口调用成功后,可发起一个的异步调用,以便创作者在Portaly控制台中查看新粉丝。详见portaly-user/SKILL.md 步骤5中的辅助工具。
syncToPortaly([{ email, name, status: 'active' }])ts
// 在POST /api/waitlist调用成功后
syncToPortaly([{ email, name }]).catch((err) =>
console.error('[Portaly Sync]', err)
)Step B4 — Verify
步骤B4 —— 验证
- From the creator's dashboard, send a test invitation email to your own inbox.
- Click the CTA link in the email.
- The browser should redirect through and land on
portaly.ai/r/....https://your-app.example.com/waitlist/{slug}?ref=...&utm_source=invitation&... - Submit the form; check the Portaly Dashboard's waitlist tab to confirm the row.
- If is wired, the user should also appear in the Dashboard's user list.
syncToPortaly
- 从创作者控制台发送测试邀请邮件到你自己的邮箱。
- 点击邮件中的CTA链接。
- 浏览器应通过进行重定向,最终跳转到
portaly.ai/r/...。https://your-app.example.com/waitlist/{slug}?ref=...&utm_source=invitation&... - 提交表单;检查Portaly控制台的等待列表标签页确认数据已添加。
- 如果已对接,该用户也应出现在控制台的用户列表中。
syncToPortaly
Sending a Campaign (Vibe MCP)
发送活动(Vibe MCP)
Independent of Mode A/B above. This workflow drives outgoing follower-email campaigns — drafting an invitation, queueing it to a recipient list, and reading back analytics. Available only when the agent is connected to the creator's Vibe MCP server (the install instructions for that are in the Vibe dashboard's onboarding flow). Authentication is the MCP Bearer token; no involved.
PORTALY_API_KEYSee for a copy-pastable end-to-end run.
references/sending-campaigns.md与上述A/B模式无关。此工作流用于发起粉丝邮件营销活动——起草邀请邮件、将其加入收件人队列、查看分析数据。仅当Agent连接到创作者的Vibe MCP服务器时可用(安装说明在Vibe控制台的引导流程中)。身份验证使用MCP Bearer令牌;无需。
PORTALY_API_KEY端到端运行示例详见。
references/sending-campaigns.mdTools
工具
| Tool | Purpose |
|---|---|
| Inventory: drafts to act on, in-flight sends, completed history. Filter by |
| Create a new campaign in |
| Persist |
| Funnel + event totals + 30-day timeseries for one campaign. |
| 工具 | 用途 |
|---|---|
| 活动清单:待处理草稿、进行中的发送、已完成历史记录。可按 |
| 创建一个 |
| 将 |
| 单个活动的转化漏斗 + 事件总数 + 30天时间序列数据。 |
Workflow
工作流程
- Confirm intent with the creator — what's the campaign for? Is there an existing draft, or starting fresh? Call to check.
vibe_list_campaigns - Create the draft with . Pass any context the creator gives (campaign angle, tone, must-mention deadline) into
vibe_create_campaign. The campaign starts empty — no subject, no body, no recipients.aiContext - Hand recipient import to the dashboard. Tell the creator:
Recipient import is in the Vibe dashboard's Email → Outreach tab. Open your campaign there, upload a CSV / Google Sheet / paste addresses, then come back and tell me when you're done. Recipient management is intentionally not exposed via MCP — column-mapping a CSV in chat is fragile and the dashboard already has a preview UI.
- Draft +
subjectwith the creator. Constraints:bodyHtml- Subject ≤ 255 chars.
- Body is HTML, ≤ 100,000 chars.
- Must include somewhere in the body — that's the tracked invitation link the recipient clicks. Without it, you've shipped a CTA-less email.
{inviteUrl} - Always-available placeholders: ,
{customerName},{merchantName}. Any extra column the creator imported is exposed as{inviteUrl}— confirm those slugs with the creator before referencing them.{slug}
- Confirm with the creator before sending. Sending is irreversible and burns from their monthly quota + purchased credits. Show the subject, the rendered body (or a preview link), the recipient count.
- Call with
vibe_send_campaign,campaignId,subject. Switch on thebodyHtml:outcome
| Outcome | Meaning | What to do |
|---|---|---|
| Send is in flight | Tell the creator: enqueued N emails, M quota remaining. Optionally schedule a follow-up to call |
| id is wrong / belongs to another merchant | Recheck |
| Imports are empty | Step 3 wasn't completed. Send the creator back to Email → Outreach to import their list. |
| Recipients > remaining quota | Response includes |
- Read analytics with once delivery has had time to register (a few minutes). The funnel goes
vibe_get_campaign_analytics(campaignId).imported → enqueued → delivered → opened → clicked → bounced → complained → signedUp → convertedonly populates if the recipient hits the waitlist landing page (Mode A or B above);signedUponly fills if they later subscribe viaconverted.portaly-payment
- 与创作者确认意图——活动目的是什么?是否已有草稿,还是从零开始?调用接口检查。
vibe_list_campaigns - 创建草稿使用接口。将创作者提供的任何上下文(活动角度、语气、必须提及的截止日期)传入
vibe_create_campaign。活动初始为空——无主题、无内容、无收件人。aiContext - 引导创作者在控制台导入收件人。告知创作者:
收件人导入在Vibe控制台的邮件 → 触达标签页中。打开你的活动,上传CSV/谷歌表格/粘贴邮箱地址,完成后告知我。 收件人管理未通过MCP开放——在聊天中处理CSV列映射容易出错,而控制台已有预览UI。
- 与创作者一起起草+
subject。约束条件:bodyHtml- 主题≤255字符。
- 内容为HTML格式,≤100,000字符。
- 必须在内容中包含——这是收件人点击的带追踪的邀请链接。如果缺少该占位符,邮件将没有CTA按钮。
{inviteUrl} - 可用的默认占位符:、
{customerName}、{merchantName}。创作者导入的任何额外列都会以{inviteUrl}形式暴露——引用前需与创作者确认这些slug。{slug}
- 发送前与创作者确认。发送操作不可撤销,且会消耗每月配额和购买的 credits。向创作者展示主题、渲染后的内容(或预览链接)、收件人数量。
- **调用**接口,传入
vibe_send_campaign、campaignId、subject。根据bodyHtml处理:outcome
| 结果 | 含义 | 操作 |
|---|---|---|
| 邮件已加入队列 | 告知创作者:已加入N封邮件到队列,剩余M配额。可选安排后续调用 |
| ID错误/属于其他商家 | 重新检查 |
| 未导入收件人 | 步骤3未完成。引导创作者回到邮件 → 触达标签页导入收件人列表。 |
| 收件人数量超过剩余配额 | 响应中包含 |
- 查看分析数据在邮件发送一段时间后(几分钟)调用接口。转化漏斗为
vibe_get_campaign_analytics(campaignId)。导入 → 加入队列 → 送达 → 打开 → 点击 → 退回 → 投诉 → 注册 → 转化仅在收件人访问等待列表着陆页(模式A或B)时填充;signedUp仅在后续通过converted订阅时填充。portaly-payment
Guardrails for sending
发送注意事项
- Always include . The whole point of the campaign is the click — without the placeholder, recipients see a wall of text with no CTA.
{inviteUrl} - Confirm the recipient count before sending. A misplaced 0 in a CSV column or a stale draft can lead to mass-emailing the wrong list. Read it back to the creator: "About to send to N people imported on <date>. Proceed?"
- Do not call repeatedly to "retry" a
vibe_send_campaign. That's a soft fail — the creator must top up first. Retrying without action just churns calls.quota_exceeded - Subject lines for follower outreach matter more than for transactional email. Push back if the creator gives a generic "Update from {merchantName}" — suggest something tied to the campaign's angle.
- 必须包含。活动的核心是获得点击——如果缺少该占位符,收件人只会看到无CTA的纯文本。
{inviteUrl} - 发送前确认收件人数量。CSV列中的误输入或陈旧草稿可能导致向错误列表发送大量邮件。向创作者复述:"即将发送给<日期>导入的N位用户。是否继续?"
- 请勿反复调用接口重试
vibe_send_campaign结果。这是软失败——创作者必须先补充配额。无操作重试只会增加调用次数。quota_exceeded - 粉丝触达活动的主题比事务性邮件更重要。如果创作者给出通用的"来自{merchantName}的更新",请建议使用与活动角度相关的主题。
Switching Modes
切换模式
| From | To | Action |
|---|---|---|
| Mode A → Mode B | Set | |
| Mode B → Mode A | Set |
Switch propagates within ~60 seconds (Portaly's per-process cache TTL). In-flight emails immediately pick up the new mode on the next click — Portaly resolves the redirect target at click time, not at send time.
| 从 | 到 | 操作 |
|---|---|---|
| 模式A → 模式B | 通过 | |
| 模式B → 模式A | 通过 |
切换后约60秒生效(Portaly进程缓存的TTL)。已发出的邮件在下次点击时会立即应用新模式——Portaly在点击时解析跳转目标,而非发送时。
Guardrails
注意事项
- HTTPS only for .
appBaseUrlis rejected by Portaly.http://cannot be used in production — for local dev use ngrok / Cloudflare Tunnel.localhost - Path is fixed: exactly. Do not alias to
/waitlist/{creatorSlug},/signup, etc. — Portaly redirects to the literal/joinpath./waitlist/{slug} - Click tracking always runs through Portaly. Do not try to point the email CTA directly at your own domain to "skip" — you'll lose click analytics and rate limiting.
/r/{code} - Preserve UTM and query params on the POST body in Mode B. Dropping them breaks campaign attribution on Portaly's side.
ref - Do not skip user sync. A signup that's only stored on Portaly's waitlist row but missing from the creator's user list creates support pain when the creator wonders why a known follower doesn't show up in their dashboard.
- 仅支持HTTPS。
appBaseUrl会被Portaly拒绝。生产环境中无法使用http://——本地开发请使用ngrok/Cloudflare Tunnel。localhost - 路径固定:必须为。请勿别名到
/waitlist/{creatorSlug}、/signup等路径——Portaly会重定向到字面意义上的/join路径。/waitlist/{slug} - 点击追踪始终通过Portaly。请勿尝试将邮件CTA直接指向自有域名以"跳过"——这会丢失点击分析数据和速率限制功能。
/r/{code} - 模式B中请保留UTM和查询参数。丢失这些参数会破坏Portaly端的活动归因。
ref - 请勿跳过用户同步。仅存储在Portaly等待列表中但未出现在创作者用户列表中的注册用户会引发支持问题,创作者会疑惑为何已知粉丝未显示在控制台中。
Output Preferences
输出偏好
- Always confirm Mode A vs Mode B with the human user before doing setup work.
- For Mode A, prefer one short paragraph + the CTA URL. No code templates needed.
- For Mode B, lean on instead of inlining all the code.
references/self-hosted-waitlist.md - Keep secrets (API keys) out of chat — write instructions instead.
.env
- 在进行设置工作前,始终与用户确认模式A或模式B。
- 对于模式A,优先使用简短段落+CTA URL。无需提供代码模板。
- 对于模式B,优先引用而非内联所有代码。
references/self-hosted-waitlist.md - 聊天中请勿泄露密钥(API密钥)——请编写配置说明。
.env
Reference Documents
参考文档
- — Mode A snippets and CTA placement examples.
references/hosted-cta.md - — Mode B implementation templates for Next.js, React SPA, and plain HTML.
references/self-hosted-waitlist.md - — End-to-end campaign send via Vibe MCP, with body templates and outcome handling.
references/sending-campaigns.md
- —— 模式A代码片段和CTA放置示例。
references/hosted-cta.md - —— 模式B实现模板(Next.js、React SPA、纯HTML)。
references/self-hosted-waitlist.md - —— 通过Vibe MCP端到端发送活动的示例,包含内容模板和结果处理。",
references/sending-campaigns.md