Skip to main contentCertyo Developer Portal

Salesforce Integration

Connect Salesforce to Certyo using Platform Events, Apex callouts, and custom objects. Automatically anchor order fulfillment data to the blockchain and track certificate status directly in Salesforce.

Prerequisites

  • Salesforce Enterprise or Unlimited edition (Platform Events and Named Credentials require these editions)
  • A Certyo API key — see the Authentication Guide
  • System Administrator or a custom permission set with access to Apex classes, Platform Events, and Named Credentials
  • My Domain enabled (required for Named Credentials)

Architecture

Integration flowbash
┌──────────────────────────────────┐
│        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  │
                                      └──────────────────┘

Custom Object: Certificate__c

Create a custom object to track the blockchain certificate lifecycle for each record. Navigate to Setup > Object Manager > Create > Custom Object.

Field API NameTypeDescription
RecordId__cText(255)The Certyo recordId (e.g. the Order number)
RecordHash__cText(255)SHA-256 hash returned by Certyo on ingestion
SnapshotId__cText(255)Certyo snapshot ID containing this record
MerkleRoot__cText(255)Merkle tree root hash for the anchoring batch
AnchorStatus__cPicklistPending | Batched | Anchored | Failed
PolygonTxHash__cText(255)Polygon transaction hash once anchored on-chain
IpfsCid__cText(255)IPFS content identifier for the manifest
VerifiedAt__cDateTimeTimestamp of last successful verification
Order__cLookup(Order)Related Order record for navigation and reporting

Named Credential

Store the Certyo API endpoint and authentication header in a Named Credential so Apex code never handles raw secrets. Navigate to Setup > Named Credentials > New Named Credential.

Named Credential configurationbash
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:   checked
Callout limits
Salesforce enforces a limit of 100 callouts per Apex transaction. Use Platform Events with @future(callout=true) or Queueable Apex to make callouts outside the trigger transaction context. Never make callouts directly inside a trigger body.

Implementation

Core service class that handles both record ingestion and verification. Uses the Named Credential for authentication so no API keys appear in code.

CertyoService.clscsharp
/**
 * 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;
    }
}

Authentication: OAuth 2.0 client credentials

For server-to-server integrations (e.g., external middleware calling both Salesforce and Certyo), use OAuth 2.0 Client Credentials flow for Salesforce and an API key for Certyo:

Salesforce OAuth 2.0 token requestbash
# 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", ... }'

Rate limit considerations

Salesforce imposes several governor limits that affect the integration design:

LimitValueMitigation
Callouts per transaction100Use @future or Queueable — one callout per async execution
Callout timeout120s (max)Certyo returns 202 in <500ms — well within limits
Max @future calls per transaction50Batch events and use Queueable chaining for >50 records
Platform Event publish limit10,000/hour (High Volume)Sufficient for most use cases; use Change Data Capture for higher volume
Async Apex daily limit250,000 or # licenses x 200Monitor via Setup > Apex Jobs; use bulk patterns

Custom Setting: Certyo_Settings__c

Create a Hierarchy Custom Setting via Setup > Custom Settings > New to store the tenant ID and other configuration values that the reads at runtime: CertyoService

  • Tenant_Id__cText(255). Your Certyo tenant identifier.
  • Database_Name__cText(255). Defaults to . salesforce.
  • Verification_Enabled__cCheckbox. Toggle the scheduled verification job.

Set org-wide defaults via Setup > Custom Settings > Certyo_Settings__c > Manage > New (Org Default).

Test coverage
Salesforce requires 75% Apex test coverage for deployment. Write test classes that mock the Certyo HTTP callout using HttpCalloutMock. The CertyoService methods are designed to be testable — inject mock responses via Test.setMock(HttpCalloutMock.class, ...) in your test setup.

Deployment checklist

  1. Create the custom object with all fields listed above Certificate__c
  2. Create the Hierarchy Custom Setting and set org defaults Certyo_Settings__c
  3. Create the Named Credential with your production API key Certyo_API
  4. Define the Platform Event Certyo_Record_Ingest__e
  5. Deploy CertyoService.cls, OrderCertyoTrigger.trigger, and CertyoRecordIngestTrigger.trigger
  6. Deploy CertyoVerificationScheduler.cls and CertyoVerificationJob.cls
  7. Schedule the verification job: System.schedule('Certyo Verification', '0 0/15 * * * ?', new CertyoVerificationScheduler())
  8. Add the related list to the Order page layout Certificate__c
  9. Test end-to-end: activate an Order and verify the Certificate__c record transitions from Pending to Anchored within 2 minutes

AI Integration · v1.0.0

AI Integration Skill

Download a skill file that enables AI agents to generate working Salesforce + Certyo integration code for any language or framework.

v1.0.0
What is this?
A markdown file containing Salesforce-specific field mappings, authentication setup, code examples, and integration patterns for Certyo. Drop it into your AI agent's context and ask it to generate integration code.

What's inside

  • AuthenticationNamed Credentials for API key storage and OAuth 2.0 client credentials
  • ArchitectureOrder trigger → Platform Event → @future callout → Certificate__c update
  • Field mappingOrder, Product, and Fulfillment fields to Certyo record schema
  • Code examplesCertyoService Apex class, Apex trigger, Platform Event, Scheduled verification
  • Governor limits100 callouts/transaction, @future and Queueable patterns
  • Custom objectsCertificate__c definition with RecordHash, AnchorStatus, PolygonTxHash fields

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 "Generate an Apex service that ingests Order records into 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,
 generate an apex service that ingests order records into 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