Salesforce Open Badges:由 CRM 触发器颁发可验证凭证
通过 Apex 调用 Badges Ninja API,让任意 Salesforce 对象事件——Lead 赢单、Account 升级、培训里程碑——自动触发 Open Badge 颁发。
大多数培训团队早已在 Salesforce 里记录了谁完成了什么。课程报名状态变为 “Completed”,合作伙伴账户达到某个认证等级,客户完成了入门引导。记录变了,然后一周后有人导出名单,手动用邮件发送证书。
本指南教你补上这个缺口:当记录达到你关心的状态时,Salesforce 调用 https://badges.ninja 的 API,颁发一份 Open Badge v2.0 凭证,并把验证链接回写到该记录上。之后你可以把这个链接放进邮件模板、报表或 Experience Cloud 页面。
你需要一位 Salesforce 管理员(或一个开发者沙盒)、一枚已在 https://badges.ninja 上设计好的数字徽章,以及一个 API 密钥。如果你从未调用过这个 API,请先阅读 API 快速入门,只需十分钟。
什么时候适合用 CRM 触发器
在写 Apex 之前,先确认你是否真的需要它。如果你每学期或每季度批量颁发一次凭证,仪表板的 CSV 上传更快,而且完全不需要代码。Salesforce 方案在以下情况下才物有所值:
- 记录一变更就应立即颁发,无需任何人工步骤。
- “谁获得了凭证”的数据本来就在 Salesforce 里,你不想再维护第二个数据源。
- 你希望把凭证链接存放在 Contact、Lead 或自定义培训记录上,以便做报表。
如果你的工作流工具是 Zapier 而不是 Apex,同样的 API 调用在那里也能用;参见 Zapier Open Badges 自动化。
你要构建什么
整个流程由四部分组成:
- 一枚徽章,在可视化设计器中设计。复制它的 id(即
GET /badges返回的id)。 - 一个 API 密钥,在仪表板的 API Keys 页面创建。密钥只显示一次,所以请立刻保存。
- 一个 Named Credential,位于 Salesforce 中,用来存放密钥,使其永远不会出现在代码里。
- 一个 Apex 触发器加一个 Queueable 类,负责颁发凭证,并把颁发链接写回记录。

第 1 步:把密钥存进 Named Credential
在 Apex 中硬编码 API 密钥是必须避免的错误。在 Setup 中,先创建一个带自定义请求头的 External Credential,再创建一个指向 API 主机的 Named Credential:
- URL:
https://api.badges.ninja - 自定义请求头名称:
X-Api-Key - 自定义请求头值: 你的密钥,格式为
bws_YOUR_KEY_HERE - Named Credential 名称:
BadgesNinja
请让运行自动化的权限集(permission set)拥有访问该 External Credential 主体(principal)的权限。有了 Named Credential,你就不需要单独的 Remote Site Setting;Salesforce 会通过该凭证授权调用。如果你的组织仍在使用旧版 Named Credential 或普通端点,请把 https://api.badges.ninja 添加为 Remote Site Setting,并在代码中发送请求头。
第 2 步:为记录添加字段
在代表成就的对象上(我们使用一个名为 Training_Enrollment__c 的自定义对象),添加:
Badge_Id__c(Text):要颁发的徽章。按课程存放它,可以让一个触发器服务多枚徽章。Badge_Award_Id__c(Text,unique):API 返回的颁发 id。Badge_Url__c(URL):公开的验证页面。Badge_Error__c(Long Text):最近一次错误,这样管理员无需打开调试日志就能看到失败原因。
Badge_Award_Id__c 字段一物两用。它是你的防重复保护:只有当它为空时,触发器才会颁发。
第 3 步:Apex 调用
触发器不能直接发起调用,因为在存在未提交 DML 的同一事务中不允许进行 callout。请把工作交给实现了 Database.AllowsCallouts 的 Queueable。
POST /awards 的 API 约定很简短。请求体包含 badgeId、一个嵌套的 recipient 对象(带 name 和 email),以及必填的 issuedOn。调用成功时返回 HTTP 200 和 { "awardId": "..." }。接收者姓名至少要有五个字符,因此名为 “Al Li” 的 Contact 会被拒绝;调用前请先校验。
public class BadgeIssueJob implements Queueable, Database.AllowsCallouts {
private Id enrollmentId;
public BadgeIssueJob(Id enrollmentId) { this.enrollmentId = enrollmentId; }
public void execute(QueueableContext ctx) {
Training_Enrollment__c e = [
SELECT Id, Badge_Id__c, Badge_Award_Id__c, Contact__r.Name, Contact__r.Email
FROM Training_Enrollment__c WHERE Id = :enrollmentId LIMIT 1
];
if (e.Badge_Award_Id__c != null) { return; } // already issued
Map<String, Object> recipient = new Map<String, Object>{
'name' => e.Contact__r.Name,
'email' => e.Contact__r.Email
};
Map<String, Object> body = new Map<String, Object>{
'badgeId' => e.Badge_Id__c,
'recipient' => recipient,
'issuedOn' => Datetime.now().getTime()
};
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:BadgesNinja/awards');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody(JSON.serialize(body));
HttpResponse res = new Http().send(req);
if (res.getStatusCode() == 200) {
Map<String, Object> out = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
String awardId = (String) out.get('awardId');
e.Badge_Award_Id__c = awardId;
e.Badge_Url__c = 'https://badges.ninja/awards/' + awardId;
e.Badge_Error__c = null;
} else {
e.Badge_Error__c = res.getStatusCode() + ': ' + res.getBody();
}
update e;
}
}
有两个细节值得注意。第一,issuedOn 是纪元毫秒数,这正是 Datetime.getTime() 返回的值。第二,响应中不包含验证 URL,所以代码用颁发 id 自行拼出:https://badges.ninja/awards/<awardId>。
第 4 步:触发器
让触发器保持精简。它只负责判断哪些记录符合条件,并为每条记录入队一个任务。
trigger TrainingEnrollmentBadge on Training_Enrollment__c (after insert, after update) {
for (Training_Enrollment__c e : Trigger.new) {
Boolean completed = e.Status__c == 'Completed';
Boolean wasCompleted = Trigger.isUpdate && Trigger.oldMap.get(e.Id).Status__c == 'Completed';
if (completed && !wasCompleted && e.Badge_Award_Id__c == null && e.Badge_Id__c != null) {
System.enqueueJob(new BadgeIssueJob(e.Id));
}
}
}
wasCompleted 这个判断很关键。没有它,对已完成记录的任何后续编辑都会再次触发逻辑。再加上任务内部”颁发 id 为空”的保护,一条记录最多只会产生一份凭证。
还有一个需要遵守的限制:Salesforce 只允许在单个事务中入队少量 Queueable 任务(同步上下文中为 50 个)。如果你批量导入完成记录,比如一次导入 2000 行数据,上面的触发器就会撞上这个上限。对于批量加载,要么改用仪表板的 CSV 批量颁发来上传名单,要么改用带 Database.AllowsCallouts 的 Batchable 类,每个 scope 只处理少量记录。

错误处理与重试
API 返回标准状态码,每一种都需要你的代码做出不同的响应:
| 状态码 | 含义 | 处理方式 |
|---|---|---|
| 400 | 字段缺失或无效,包括接收者姓名少于五个字符 | 不要重试。修正数据,并把消息写入 Badge_Error__c。 |
| 401 | API 密钥错误或缺失 | 检查 Named Credential。在修复之前重试没有用。 |
| 402 | 你的套餐凭证数量或配额已用完 | 停止颁发并通知管理员;升级套餐或配额重置后再重试。 |
| 404 | badgeId 不存在 | 修正记录或课程上的 Badge_Id__c。 |
| 5xx 或超时 | 暂时性故障 | 可以稍后安全重试。 |
由于任务会把错误写到记录上,并让 Badge_Award_Id__c 保持为空,重试就像勾选一个复选框或重新保存记录一样简单。再写一个小型的定时 Apex 类,查找已完成但没有颁发 id 且带有暂时性错误的报名记录并重新入队,你就拥有了一个无需额外基础设施的自动重试队列。请用计数字段限制尝试次数,避免永久出错的记录无限循环。
接入生产环境之前,请先确认你的套餐。Free 套餐每年包含 1,200 份凭证;Starter 为 $9/月,Pro 为 $29/月。如果触发器对每个 Lead 都触发,小套餐很快就会被耗尽,所以请把条件限定在真正的成就上。
让凭证在 Salesforce 中可见
Badge_Url__c 一旦有值,你就可以在任何地方使用它:
- 邮件提醒与模板: 把该字段合并进一封”恭喜,这是你的凭证”邮件。
- 报表: 基于
Badge_Award_Id__c != null的报表可以按课程、地区或账户负责人统计颁发数量。 - Experience Cloud: 在合作伙伴的认证页面显示该链接,让买家自行验证。
- 页面布局: 添加该 URL 字段,让支持人员一眼就能回答”她拿到数字徽章了吗?”
验证页面是公开的,无需注册,这正是它的意义所在。接收者、招聘经理或客户的采购团队都可以打开链接,确认是谁颁发了这份凭证、它证明了什么以及颁发时间。如需机器可读的验证,任一颁发记录的 Open Badge v2.0 JSON-LD 可通过 https://api.badges.ninja/certify-badge/award/<awardId> 获取。
接收者还能获得常见的附加功能:唯一的验证 URL、二维码、A4 PDF 证书,以及接收者门户中的”添加到 LinkedIn”选项,该门户通过魔法链接而非密码登录。
为什么这比 AppExchange 徽章插件更划算
AppExchange 上的徽章插件通常按 Salesforce 席位或托管包许可证计费。这笔成本随你的 Salesforce 团队规模增长,而不是随你颁发的凭证数量增长,因此十人的培训团队和一万名学员的项目,为同样的产出付出的费用差别很大。
API 集成则反转了这一模式。你按凭证数量以固定套餐付费,触发器代码可以自由阅读和修改,输出是标准的 Open Badge v2.0,接收者不会被锁定在专有的查看器里。你也保留了设计自由:可视化设计器提供 80 多种模板、形状、图标和自定义上传,而不是软件包内固定的版式。
坦率地说,这里有个取舍:插件点击即可安装,不需要 Apex。如果团队里没有人能维护几十行代码,插件或无代码连接器可能更合适。如果你比较的是整体平台而不是集成方式,Credly 替代方案一文并排对比了价格和功能差异。
上线前的测试
先在沙盒中完整跑一遍流程。
- 创建一枚测试徽章,以及一个使用你可控邮箱地址的测试接收者。
- 在 Apex 测试中,实现
HttpCalloutMock,返回状态码 200 和{"awardId":"test-award-id"};Salesforce 不允许在测试上下文中发起真实 callout,而你需要这部分覆盖率才能部署。 - 在沙盒中,把一条报名记录设为 Completed,并检查
Badge_Award_Id__c和Badge_Url__c是否在一分钟内被填充。 - 在无痕浏览器窗口中打开该 URL,确认公开验证页面无需登录即可显示。
- 人为制造一次失败(清空徽章 id,或使用少于五个字符的接收者姓名),并确认错误出现在
Badge_Error__c中。
如果 API 密钥曾泄露到调试日志或截图中,请立即轮换。在 API Keys 页面可以即时吊销,而你只需更新 External Credential,无需改动代码。
总结
这个模式很小巧:精简的触发器决定何时颁发,Queueable 负责调用,Named Credential 保管密钥,记录上的三个字段保存结果。它适用于培训里程碑、合作伙伴等级、活动出席,或任何你能在 Salesforce 中表达的状态变化。
从一个对象和一枚徽章开始,观察一周,等重试行为符合你的数据后再扩展。
准备好颁发你的第一份可验证凭证了吗? 在 badges.ninja 免费开始 — 可视化设计器、公开验证页面、PDF 证书、Open Badge v2.0 输出。无需信用卡。
本文是如何撰写的
本博客的部分文章由 AI 助手协助起草,随后由 Badges Ninja 团队审核、核实并编辑后发布。所有代码示例和价格均已对照产品实际情况核实。欲了解更多关于我们编辑与 AI 流程的信息,请查看我们的 编辑流程页面 .

关于作者
Nacho Coll
Founder & Engineer at Badges Ninja
Nacho founded Badges Ninja to make issuing verifiable digital credentials as simple as a single API call — Open Badge v2.0 badges and certificates, minted, hosted, and verifiable without standing up your own issuer infrastructure. Writes about the Open Badges spec, credential verification, and running a credentialing platform serverless on AWS, from the operator side of the wire.

