Integración con Salesforce
Conecta Salesforce a Certyo usando Platform Events, callouts de Apex y objetos personalizados. Ancla automáticamente datos de cumplimiento de pedidos en la blockchain y rastrea el estado del certificado directamente en Salesforce.
Requisitos previos
- Edición Salesforce Enterprise o Unlimited (Platform Events y Named Credentials requieren estas ediciones)
- Una clave API de Certyo — consulta la Guía de autenticación
- Administrador del sistema o un conjunto de permisos personalizado con acceso a clases Apex, Platform Events y Named Credentials
- My Domain habilitado (requerido para Named Credentials)
Arquitectura
┌──────────────────────────────────┐
│ Salesforce Org │
│ │
│ Order status -> "Fulfilled" │
│ │ │
│ v │
│ Apex Trigger (OrderTrigger) │
│ │ │
│ v │
│ Platform Event │
│ (Certyo_Record_Ingest__e) │
│ │ │
│ v │
│ Apex Trigger (Platform Event) │
│ -> @future(callout=true) │
│ │ │
│ v │
│ CertyoService.ingestRecord() │──────────┐
│ (Named Credential callout) │ │
│ │ │ │ HTTPS POST
│ v │ │ X-API-Key
│ Certificate__c record │ │
│ (AnchorStatus = Pending) │ v
│ │ ┌──────────────────┐
│ Scheduled Apex (every 15 min) │ │ Certyo API │
│ -> CertyoService.verifyRecord() │ │ POST /api/v1/ │
│ -> Certificate__c updated │ │ records │
│ (AnchorStatus = Anchored) │ │ │
└──────────────────────────────────┘ │ 202 Accepted │
└────────┬─────────┘
│
v
┌──────────────────┐
│ Pipeline │
│ Kafka -> │
│ Accumulate -> │
│ Merkle Tree -> │
│ IPFS -> Polygon │
└──────────────────┘Objeto personalizado: Certificate__c
Crea un objeto personalizado para rastrear el ciclo de vida del certificado blockchain para cada registro. Navega a Setup > Object Manager > Create > Custom Object.
| Nombre API del campo | Tipo | Descripción |
|---|---|---|
RecordId__c | Text(255) | El recordId de Certyo (por ejemplo, el número de pedido) |
RecordHash__c | Text(255) | Hash SHA-256 retornado por Certyo en la ingesta |
SnapshotId__c | Text(255) | ID de snapshot de Certyo que contiene este registro |
MerkleRoot__c | Text(255) | Hash raíz del Merkle tree para el lote de anclaje |
AnchorStatus__c | Picklist | Pending | Batched | Anchored | Failed |
PolygonTxHash__c | Text(255) | Hash de transacción en Polygon una vez anclado on-chain |
IpfsCid__c | Text(255) | Identificador de contenido IPFS para el manifiesto |
VerifiedAt__c | DateTime | Marca de tiempo de la última verificación exitosa |
Order__c | Lookup(Order) | Registro de pedido relacionado para navegación y reportes |
Named Credential
Almacena el endpoint del API de Certyo y el encabezado de autenticación en una Named Credential para que el código Apex nunca maneje secretos directamente. Navega a Setup > Named Credentials > New Named Credential.
Label: Certyo API
Name: Certyo_API
URL: https://www.certyos.com
Identity Type: Named Principal
Authentication: Custom Header
Header Name: X-API-Key
Header Value: certyo_sk_live_your_key_here
Generate Auth Header: unchecked
Allow Merge Fields: checkedImplementación
Clase de servicio principal que maneja tanto la ingesta de registros como la verificación. Usa la Named Credential para autenticación, así que no aparecen claves API en el código.
/**
* CertyoService — Apex class for Certyo API integration.
* Uses Named Credential 'Certyo_API' for authentication.
*
* Methods:
* ingestRecord() — POST /api/v1/records (async via @future)
* verifyRecord() — POST /api/v1/verify/record
* queryRecord() — GET /api/v1/records?tenantId=X&recordId=Y
*/
public with sharing class CertyoService {
private static final String NAMED_CREDENTIAL = 'callout:Certyo_API';
private static final String INGEST_PATH = '/api/v1/records';
private static final String VERIFY_PATH = '/api/v1/verify/record';
private static final String QUERY_PATH = '/api/v1/records';
// Tenant ID — configure via Custom Metadata Type or Custom Setting
private static String getTenantId() {
Certyo_Settings__c settings = Certyo_Settings__c.getOrgDefaults();
return settings.Tenant_Id__c != null
? settings.Tenant_Id__c
: 'default';
}
/**
* Ingest a record asynchronously.
* Called via @future so it runs outside the trigger transaction.
*
* @param orderId The Order record ID (Salesforce ID)
* @param orderNumber The Order number (used as Certyo recordId)
* @param payloadJson Serialized JSON of the order data
*/
@future(callout=true)
public static void ingestRecord(
Id orderId,
String orderNumber,
String payloadJson
) {
String tenantId = getTenantId();
String today = String.valueOf(Date.today());
Map<String, Object> body = new Map<String, Object>{
'tenantId' => tenantId,
'database' => 'salesforce',
'collection' => 'orders',
'recordId' => orderNumber,
'recordVersion' => '1',
'operationType' => 'insert',
'recordPayload' => (Map<String, Object>) JSON.deserializeUntyped(payloadJson),
'sourceTimestamp' => Datetime.now().formatGmt('yyyy-MM-dd\'T\'HH:mm:ss\'Z\''),
'idempotencyKey' => orderNumber + '-v1-' + today
};
HttpRequest req = new HttpRequest();
req.setEndpoint(NAMED_CREDENTIAL + INGEST_PATH);
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody(JSON.serialize(body));
req.setTimeout(30000);
Http http = new Http();
HttpResponse res = http.send(req);
Certificate__c cert = new Certificate__c(
Order__c = orderId,
RecordId__c = orderNumber,
AnchorStatus__c = 'Pending'
);
if (res.getStatusCode() == 202) {
Map<String, Object> result =
(Map<String, Object>) JSON.deserializeUntyped(res.getBody());
cert.RecordHash__c = (String) result.get('recordHash');
System.debug('Certyo ingestion accepted: ' + orderNumber +
' hash=' + cert.RecordHash__c);
} else {
cert.AnchorStatus__c = 'Failed';
System.debug('Certyo ingestion failed: HTTP ' +
res.getStatusCode() + ' ' + res.getBody());
}
upsert cert RecordId__c;
}
/**
* Verify a record against the on-chain Merkle root.
*
* @param recordId The Certyo recordId (e.g. Order number)
* @param payloadJson The original record payload as JSON
* @return Map with verification result fields
*/
public static Map<String, Object> verifyRecord(
String recordId,
String payloadJson
) {
String tenantId = getTenantId();
Map<String, Object> body = new Map<String, Object>{
'tenantId' => tenantId,
'database' => 'salesforce',
'collection' => 'orders',
'recordId' => recordId,
'payload' => (Map<String, Object>) JSON.deserializeUntyped(payloadJson)
};
HttpRequest req = new HttpRequest();
req.setEndpoint(NAMED_CREDENTIAL + VERIFY_PATH);
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody(JSON.serialize(body));
req.setTimeout(30000);
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200) {
return (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
}
return new Map<String, Object>{
'verified' => false,
'error' => 'HTTP ' + res.getStatusCode() + ': ' + res.getBody()
};
}
/**
* Query Certyo for the anchoring status of a record.
*
* @param recordId The Certyo recordId
* @return The first matching snapshot or null
*/
public static Map<String, Object> queryRecordStatus(String recordId) {
String tenantId = getTenantId();
HttpRequest req = new HttpRequest();
req.setEndpoint(NAMED_CREDENTIAL + QUERY_PATH +
'?tenantId=' + EncodingUtil.urlEncode(tenantId, 'UTF-8') +
'&recordId=' + EncodingUtil.urlEncode(recordId, 'UTF-8'));
req.setMethod('GET');
req.setTimeout(15000);
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200) {
Map<String, Object> data =
(Map<String, Object>) JSON.deserializeUntyped(res.getBody());
List<Object> items = (List<Object>) data.get('items');
if (items != null && !items.isEmpty()) {
return (Map<String, Object>) items[0];
}
}
return null;
}
}Este trigger se ejecuta cuando un pedido cambia al estado Activated (estado de Salesforce Order que indica cumplimiento). Publica un Platform Event para desacoplar el callout de la transacción DML.
/**
* OrderCertyoTrigger
* Fires after update on Order. When status changes to 'Activated',
* publishes a Platform Event for async Certyo ingestion.
*/
trigger OrderCertyoTrigger on Order (after update) {
List<Certyo_Record_Ingest__e> events = new List<Certyo_Record_Ingest__e>();
for (Order newOrder : Trigger.new) {
Order oldOrder = Trigger.oldMap.get(newOrder.Id);
// Only fire when status transitions to Activated
if (newOrder.Status == 'Activated' && oldOrder.Status != 'Activated') {
// Build the payload from Order fields
Map<String, Object> payload = new Map<String, Object>{
'orderNumber' => newOrder.OrderNumber,
'accountId' => newOrder.AccountId,
'effectiveDate' => String.valueOf(newOrder.EffectiveDate),
'totalAmount' => newOrder.TotalAmount,
'status' => newOrder.Status,
'description' => newOrder.Description,
'contractId' => newOrder.ContractId,
'poNumber' => newOrder.PoNumber,
'billingCity' => newOrder.BillingCity,
'billingCountry' => newOrder.BillingCountry,
'shippingCity' => newOrder.ShippingCity,
'shippingCountry'=> newOrder.ShippingCountry
};
events.add(new Certyo_Record_Ingest__e(
Order_Id__c = newOrder.Id,
Order_Number__c = newOrder.OrderNumber,
Payload_JSON__c = JSON.serialize(payload)
));
}
}
if (!events.isEmpty()) {
List<Database.SaveResult> results = EventBus.publish(events);
for (Database.SaveResult sr : results) {
if (!sr.isSuccess()) {
for (Database.Error err : sr.getErrors()) {
System.debug('Platform Event publish failed: ' +
err.getStatusCode() + ' - ' + err.getMessage());
}
}
}
}
}Define el Platform Event vía Setup > Platform Events > New Platform Event, o despliega el XML de metadatos a continuación usando Salesforce CLI.Certyo_Record_Ingest__e
<?xml version="1.0" encoding="UTF-8"?>
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
<deploymentStatus>Deployed</deploymentStatus>
<eventType>HighVolume</eventType>
<label>Certyo Record Ingest</label>
<pluralLabel>Certyo Record Ingests</pluralLabel>
<publishBehavior>PublishAfterCommit</publishBehavior>
<fields>
<fullName>Order_Id__c</fullName>
<label>Order ID</label>
<type>Text</type>
<length>18</length>
<isFilteringDisabled>false</isFilteringDisabled>
<isNameField>false</isNameField>
<isSortingDisabled>true</isSortingDisabled>
</fields>
<fields>
<fullName>Order_Number__c</fullName>
<label>Order Number</label>
<type>Text</type>
<length>80</length>
<isFilteringDisabled>false</isFilteringDisabled>
<isNameField>false</isNameField>
<isSortingDisabled>true</isSortingDisabled>
</fields>
<fields>
<fullName>Payload_JSON__c</fullName>
<label>Payload JSON</label>
<type>LongTextArea</type>
<length>131072</length>
<visibleLines>5</visibleLines>
<isFilteringDisabled>true</isFilteringDisabled>
<isNameField>false</isNameField>
<isSortingDisabled>true</isSortingDisabled>
</fields>
</CustomObject>Luego crea un trigger en este Platform Event que llame al : CertyoService:
/**
* CertyoRecordIngestTrigger
* Subscribes to Certyo_Record_Ingest__e Platform Events.
* Calls CertyoService.ingestRecord() via @future for each event.
*/
trigger CertyoRecordIngestTrigger on Certyo_Record_Ingest__e (after insert) {
for (Certyo_Record_Ingest__e event : Trigger.new) {
// @future(callout=true) runs asynchronously outside this transaction
CertyoService.ingestRecord(
event.Order_Id__c,
event.Order_Number__c,
event.Payload_JSON__c
);
}
}Un trabajo de Apex programado que se ejecuta cada 15 minutos para consultar el estado de anclaje en Certyo. Consulta todos los registros con estado y los actualiza cuando se confirma el anclaje. Certificate__c Pending status and updates them when anchoring is confirmed.
/**
* CertyoVerificationScheduler
* Schedulable Apex that polls Certyo for anchoring status and updates
* Certificate__c records.
*
* Schedule via Anonymous Apex:
* System.schedule(
* 'Certyo Verification Poller',
* '0 0/15 * * * ?',
* new CertyoVerificationScheduler()
* );
*/
public with sharing class CertyoVerificationScheduler implements Schedulable {
public void execute(SchedulableContext ctx) {
// Delegate to Queueable so we can make callouts
System.enqueueJob(new CertyoVerificationJob());
}
}
/**
* CertyoVerificationJob
* Queueable with callout support. Processes pending certificates
* in batches to respect Salesforce governor limits.
*/
public with sharing class CertyoVerificationJob implements Queueable, Database.AllowsCallouts {
private static final Integer BATCH_SIZE = 50;
public void execute(QueueableContext ctx) {
List<Certificate__c> pending = [
SELECT Id, RecordId__c, RecordHash__c, Order__c, Order__r.OrderNumber
FROM Certificate__c
WHERE AnchorStatus__c = 'Pending'
ORDER BY CreatedDate ASC
LIMIT :BATCH_SIZE
];
if (pending.isEmpty()) {
return;
}
List<Certificate__c> toUpdate = new List<Certificate__c>();
for (Certificate__c cert : pending) {
try {
Map<String, Object> snapshot =
CertyoService.queryRecordStatus(cert.RecordId__c);
if (snapshot == null) {
continue; // Not yet processed
}
String status = (String) snapshot.get('status');
if (status == 'Anchored') {
cert.AnchorStatus__c = 'Anchored';
cert.PolygonTxHash__c = (String) snapshot.get('polygonTxHash');
cert.MerkleRoot__c = (String) snapshot.get('merkleRoot');
cert.IpfsCid__c = (String) snapshot.get('ipfsCid');
cert.SnapshotId__c = (String) snapshot.get('id');
cert.VerifiedAt__c = Datetime.now();
toUpdate.add(cert);
System.debug('Certyo: ' + cert.RecordId__c + ' anchored — ' +
cert.PolygonTxHash__c);
} else if (status == 'Failed') {
cert.AnchorStatus__c = 'Failed';
toUpdate.add(cert);
System.debug('Certyo: ' + cert.RecordId__c + ' failed anchoring');
}
// If Batched or Pending, skip — will be picked up next run
} catch (Exception e) {
System.debug('Certyo verification error for ' +
cert.RecordId__c + ': ' + e.getMessage());
}
}
if (!toUpdate.isEmpty()) {
update toUpdate;
}
// If we processed a full batch, chain another job for remaining records
if (pending.size() == BATCH_SIZE) {
Integer remainingCount = [
SELECT COUNT()
FROM Certificate__c
WHERE AnchorStatus__c = 'Pending'
];
if (remainingCount > 0) {
System.enqueueJob(new CertyoVerificationJob());
}
}
}
}Autenticación: credenciales de cliente OAuth 2.0
Para integraciones servidor a servidor (por ejemplo, middleware externo que llama tanto a Salesforce como a Certyo), usa el flujo de credenciales de cliente OAuth 2.0 para Salesforce y una clave API para Certyo:
# 1. Get a Salesforce access token
curl -X POST https://login.salesforce.com/services/oauth2/token \
-d "grant_type=client_credentials" \
-d "client_id=YOUR_CONNECTED_APP_CLIENT_ID" \
-d "client_secret=YOUR_CONNECTED_APP_CLIENT_SECRET"
# Response:
# {
# "access_token": "00D...",
# "instance_url": "https://yourorg.my.salesforce.com",
# "token_type": "Bearer"
# }
# 2. Query Salesforce Orders
curl https://yourorg.my.salesforce.com/services/data/v60.0/query/ \
-H "Authorization: Bearer 00D..." \
--data-urlencode "q=SELECT Id, OrderNumber, TotalAmount, Status FROM Order WHERE Status = 'Activated'"
# 3. Ingest into Certyo
curl -X POST https://www.certyos.com/api/v1/records \
-H "X-API-Key: certyo_sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "tenantId": "acme-corp", "database": "salesforce", ... }'Consideraciones de límites de tasa
Salesforce impone varios límites de gobernanza que afectan el diseño de la integración:
| Límite | Valor | Mitigación |
|---|---|---|
| Callouts por transacción | 100 | Usa @future o Queueable — un callout por ejecución asíncrona |
| Timeout de callout | 120s (max) | Certyo retorna 202 en <500ms — bien dentro de los límites |
| Máximo de llamadas @future por transacción | 50 | Agrupa eventos y usa encadenamiento de Queueable para >50 registros |
| Límite de publicación de Platform Event | 10,000/hour (High Volume) | Suficiente para la mayoría de casos; usa Change Data Capture para mayor volumen |
| Límite diario de Apex asíncrono | 250,000 or # licenses x 200 | Monitorea vía Setup > Apex Jobs; usa patrones de procesamiento masivo |
Custom Setting: Certyo_Settings__c
Crea un Hierarchy Custom Setting vía Setup > Custom Settings > New para almacenar el ID de tenant y otros valores de configuración que el lee en tiempo de ejecución: CertyoService reads at runtime:
Tenant_Id__c— Text(255). Tu identificador de tenant de Certyo.Database_Name__c— Text(255). Por defecto .salesforce.Verification_Enabled__c— Checkbox. Activa o desactiva el trabajo de verificación programado.
Configura los valores predeterminados de la organización vía Setup > Custom Settings > Certyo_Settings__c > Manage > New (Org Default).
Lista de verificación de despliegue
- Crea el objeto personalizado con todos los campos listados arriba
Certificate__ccustom object with all fields listed above - Crea el Hierarchy Custom Setting y configura los valores predeterminados de la organización
Certyo_Settings__cHierarchy Custom Setting and set org defaults - Crea la Named Credential con tu clave API de producción
Certyo_APIwith your production API key - Define el Platform Event
Certyo_Record_Ingest__e - Deploy
CertyoService.cls,OrderCertyoTrigger.trigger, andCertyoRecordIngestTrigger.trigger - Deploy
CertyoVerificationScheduler.clsandCertyoVerificationJob.cls - Programa el trabajo de verificación:
System.schedule('Certyo Verification', '0 0/15 * * * ?', new CertyoVerificationScheduler()) - Agrega la lista relacionada al diseño de página del pedido
Certificate__crelated list to the Order page layout - Prueba de extremo a extremo: activa un pedido y verifica que el registro Certificate__c transicione de Pending a Anchored en 2 minutos
AI Integration Skill
Download a skill file that enables AI agents to generate working Salesforce + Certyo integration code for any language or framework.
What's inside
- Autenticación — Named Credentials para almacenamiento de clave API y credenciales de cliente OAuth 2.0
- Arquitectura — Trigger de pedido → Platform Event → callout @future → actualización de Certificate__c
- Mapeo de campos — Campos de Order, Product y Fulfillment al esquema de registro Certyo
- Ejemplos de código — Clase Apex CertyoService, trigger Apex, Platform Event, verificación programada
- Límites de gobernanza — 100 callouts/transacción, patrones @future y Queueable
- Objetos personalizados — Definición de Certificate__c con campos RecordHash, AnchorStatus, PolygonTxHash
How to use
Claude Code
Place the file in your project's .claude/commands/ directory, then use it as a slash command:
# Download the skill file
mkdir -p .claude/commands
curl -o .claude/commands/certyo-salesforce.md \
https://www.certyos.com/developers/skills/certyo-salesforce-skill.md
# Use it in Claude Code
/certyo-salesforce "Genera un servicio Apex que ingeste registros de pedidos en Certyo"Cursor / Copilot / Any AI Agent
Add the file to your project root or attach it to a conversation. The AI agent will use the Salesforce-specific patterns, field mappings, and code examples to generate correct integration code.
# Add to your project
curl -o CERTYO_SALESFORCE.md \
https://www.certyos.com/developers/skills/certyo-salesforce-skill.md
# Then in your AI agent:
"Using the Certyo Salesforce spec in CERTYO_SALESFORCE.md,
genera un servicio apex que ingeste registros de pedidos en certyo"CLAUDE.md Context File
Append the skill file to your project's CLAUDE.md so every Claude conversation has Salesforce + Certyo context automatically.
# Append to your project's CLAUDE.md
echo "" >> CLAUDE.md
echo "## Certyo Salesforce Integration" >> CLAUDE.md
cat CERTYO_SALESFORCE.md >> CLAUDE.md