Skip to main contentCertyo Developer Portal

Microsoft Azure Integration

Reference architecture for integrating Certyo with Azure managed services — API Management, Service Bus, Functions, Logic Apps, and Key Vault.

Native .NET fit
Certyo's .NET 8 backend runs natively on Azure App Service or AKS — the tightest integration is with Azure.

Prerequisites

  • Azure subscription with Contributor role
  • Azure CLI v2.60 or later
  • Certyo API key (stored in Azure Key Vault — see Authentication section below)
  • .NET 8 SDK (for Azure Functions development)

Reference Architecture

This architecture uses Azure-native services for durable, observable, enterprise-grade ingestion into Certyo:

Azure Reference Architecture
Source System ──► Event Grid ──► Service Bus Queue ──► Azure Function ──► Certyo API
                                         │                                      │
                                    Dead-letter                          Logic App polls
                                      queue                              anchoring status
                                                                                │
                                                                        Event Grid topic
                                                                         (record anchored)
                                                                                │
                                                                        Subscriber webhooks
                                                                        / Teams / Email

Event Grid captures domain events from your source systems (SQL, Cosmos DB, Storage, custom apps). Service Bus provides durable queuing with dead-letter handling. Azure Functions transform and forward records to Certyo. Logic Apps orchestrate verification polling and downstream notifications.


API Management (APIM)

Deploy Certyo's API behind Azure API Management for enterprise governance — rate limiting, JWT validation, request transformation, and a developer portal for internal teams.

Import the OpenAPI spec

Register Certyo as a backend in APIM, then import the OpenAPI definition to auto-generate policies and documentation:

Import Certyo API into APIMbash
# Create the APIM instance (skip if you already have one)
az apim create \
  --name certyo-apim \
  --resource-group certyo-rg \
  --publisher-name "Your Organization" \
  --publisher-email ops@yourorg.com \
  --sku-name Developer

# Import Certyo API from OpenAPI spec
az apim api import \
  --resource-group certyo-rg \
  --service-name certyo-apim \
  --path certyo \
  --api-id certyo-records \
  --specification-format OpenApi \
  --specification-url "https://www.certyos.com/api/v1/swagger.json" \
  --display-name "Certyo Records API"

# Set the backend URL
az apim api update \
  --resource-group certyo-rg \
  --service-name certyo-apim \
  --api-id certyo-records \
  --service-url "https://www.certyos.com"

APIM policies

Add inbound policies for rate limiting, API key injection from Key Vault, and request validation:

APIM inbound policy (XML in JSON config)json
<policies>
  <inbound>
    <!-- Rate limit per subscription: 100 calls/minute -->
    <rate-limit calls="100" renewal-period="60" />

    <!-- Validate JWT from Azure AD -->
    <validate-jwt header-name="Authorization" failed-validation-httpcode="401">
      <openid-config url="https://login.microsoftonline.com/{tenant-id}/v2.0/.well-known/openid-configuration" />
      <required-claims>
        <claim name="aud" match="all">
          <value>{your-app-client-id}</value>
        </claim>
      </required-claims>
    </validate-jwt>

    <!-- Inject Certyo API key from Named Value (backed by Key Vault) -->
    <set-header name="X-API-Key" exists-action="override">
      <value>{{certyo-api-key}}</value>
    </set-header>

    <!-- Remove the Authorization header before forwarding -->
    <set-header name="Authorization" exists-action="delete" />
  </inbound>
  <backend>
    <forward-request />
  </backend>
  <outbound />
</policies>

This pattern means internal consumers authenticate with Azure AD tokens and never see the Certyo API key. APIM handles the credential swap transparently.


Service Bus

Use Azure Service Bus as a durable message buffer between your enterprise systems and Certyo. The queue guarantees at-least-once delivery with configurable retry and dead-letter handling:

  • Queuepoint-to-point, one consumer (the Azure Function that calls Certyo)
  • Topic + Subscriptionsfan-out for multi-consumer patterns (e.g., ingest to Certyo AND mirror to a data lake)
  • Dead-letter queuefailed messages land here after max delivery attempts, enabling investigation without data loss

Azure Functions

Serverless functions act as the glue between Service Bus and Certyo. Two key functions:

1. Ingestion function (Service Bus trigger)

Triggered by each message on the Service Bus queue, transforms it into a Certyo record, and calls POST /api/v1/records:

2. Verification polling function (Timer trigger)

Runs on a schedule to check whether recently ingested records have been anchored on-chain, then publishes results to Event Grid:

CertyoIngestionFunction.cscsharp
using System.Net.Http.Json;
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;

namespace Certyo.Functions;

public class CertyoIngestionFunction
{
    private readonly HttpClient _http;
    private readonly ILogger<CertyoIngestionFunction> _logger;
    private string? _apiKey;

    public CertyoIngestionFunction(
        IHttpClientFactory httpClientFactory,
        ILogger<CertyoIngestionFunction> logger)
    {
        _http = httpClientFactory.CreateClient("Certyo");
        _logger = logger;
    }

    [Function("IngestRecord")]
    public async Task Run(
        [ServiceBusTrigger("certyo-ingest", Connection = "ServiceBusConnection")]
        ServiceBusReceivedMessage message,
        FunctionContext context)
    {
        var apiKey = await GetApiKeyAsync();
        var record = message.Body.ToObjectFromJson<CertyoRecord>();

        _logger.LogInformation(
            "Ingesting record {RecordId} for tenant {TenantId}",
            record.RecordId, record.TenantId);

        var request = new HttpRequestMessage(HttpMethod.Post, "/api/v1/records")
        {
            Content = JsonContent.Create(new
            {
                record.TenantId,
                record.Database,
                record.Collection,
                record.RecordId,
                record.RecordVersion,
                operationType = "upsert",
                record.RecordPayload,
                sourceTimestamp = record.SourceTimestamp ?? DateTimeOffset.UtcNow,
                idempotencyKey = $"{record.RecordId}-{record.RecordVersion}-{DateTime.UtcNow:yyyy-MM-dd}"
            })
        };
        request.Headers.Add("X-API-Key", apiKey);

        var response = await _http.SendAsync(request);

        if (!response.IsSuccessStatusCode)
        {
            var body = await response.Content.ReadAsStringAsync();
            _logger.LogError(
                "Certyo ingestion failed for {RecordId}: {Status} {Body}",
                record.RecordId, response.StatusCode, body);
            throw new InvalidOperationException(
                $"Certyo returned {response.StatusCode}");
        }

        var result = await response.Content
            .ReadFromJsonAsync<CertyoIngestionResult>();
        _logger.LogInformation(
            "Record {RecordId} accepted. Hash: {Hash}",
            record.RecordId, result?.RecordHash);
    }

    private async Task<string> GetApiKeyAsync()
    {
        if (_apiKey is not null) return _apiKey;

        var vaultUri = Environment.GetEnvironmentVariable("KEY_VAULT_URI")!;
        var client = new SecretClient(new Uri(vaultUri), new DefaultAzureCredential());
        var secret = await client.GetSecretAsync("certyo-api-key");
        _apiKey = secret.Value.Value;
        return _apiKey;
    }
}

public record CertyoRecord(
    string TenantId,
    string Database,
    string Collection,
    string RecordId,
    string RecordVersion,
    object RecordPayload,
    DateTimeOffset? SourceTimestamp);

public record CertyoIngestionResult(
    string RecordId,
    string RecordHash,
    string TenantId,
    DateTimeOffset AcceptedAt,
    bool IdempotencyReplayed);

Logic Apps

For business users who need no-code orchestration, Logic Apps provide a visual designer to connect enterprise triggers to Certyo. Common pattern:

  1. Trigger"When a row is added to SQL table" or "When a Dynamics 365 record is created"
  2. TransformMap source fields to the Certyo record schema using the built-in data mapper
  3. HTTP actionPOST to https://www.certyos.com/api/v1/records with the X-API-Key header
  4. ConditionCheck for 202 Accepted
  5. NotifyPost to Teams channel or send email on success/failure
Logic App template: SQL row to Certyojson
{
  "definition": {
    "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
    "triggers": {
      "When_a_row_is_added": {
        "type": "ApiConnection",
        "inputs": {
          "host": { "connection": { "name": "@parameters('$connections')['sql']['connectionId']" } },
          "method": "get",
          "path": "/datasets/default/tables/@{encodeURIComponent('dbo.AuditRecords')}/onnewitems"
        },
        "recurrence": { "frequency": "Minute", "interval": 1 }
      }
    },
    "actions": {
      "Send_to_Certyo": {
        "type": "Http",
        "inputs": {
          "method": "POST",
          "uri": "https://www.certyos.com/api/v1/records",
          "headers": {
            "X-API-Key": "@parameters('certyoApiKey')",
            "Content-Type": "application/json"
          },
          "body": {
            "tenantId": "@parameters('certyoTenantId')",
            "database": "azure-sql",
            "collection": "AuditRecords",
            "recordId": "@{triggerBody()?['Id']}",
            "recordVersion": "1",
            "operationType": "insert",
            "recordPayload": "@triggerBody()",
            "sourceTimestamp": "@{triggerBody()?['CreatedAt']}",
            "idempotencyKey": "@{triggerBody()?['Id']}-1-@{formatDateTime(utcNow(), 'yyyy-MM-dd')}"
          }
        },
        "runAfter": {}
      },
      "Post_to_Teams": {
        "type": "ApiConnection",
        "inputs": {
          "host": { "connection": { "name": "@parameters('$connections')['teams']['connectionId']" } },
          "method": "post",
          "path": "/v3/beta/teams/@{parameters('teamsTeamId')}/channels/@{parameters('teamsChannelId')}/messages",
          "body": {
            "body": {
              "content": "Record @{triggerBody()?['Id']} sent to Certyo. Hash: @{body('Send_to_Certyo')?['recordHash']}"
            }
          }
        },
        "runAfter": { "Send_to_Certyo": ["Succeeded"] }
      }
    }
  }
}

Authentication

The recommended authentication pattern layers Azure-native identity on top of Certyo's API key:

  • Key VaultStore the Certyo X-API-Key as a secret. Grant access via RBAC (Key Vault Secrets User role) to service principals that need it.
  • Managed IdentityAzure Functions and Logic Apps use system-assigned managed identity to retrieve the key from Key Vault at runtime. No credentials in code or config.
  • APIM subscription keysInternal consumers authenticate to APIM with subscription keys or Azure AD tokens. APIM injects the Certyo API key on their behalf (see the policy above).
Grant Function App access to Key Vaultbash
# Get the Function App's managed identity principal ID
PRINCIPAL_ID=$(az functionapp identity show \
  --name certyo-fn-abc123 \
  --resource-group certyo-rg \
  --query principalId -o tsv)

# Assign Key Vault Secrets User role
az role assignment create \
  --assignee $PRINCIPAL_ID \
  --role "Key Vault Secrets User" \
  --scope "/subscriptions/{sub-id}/resourceGroups/certyo-rg/providers/Microsoft.KeyVault/vaults/certyo-kv"

Monitoring

Integrate Application Insights for end-to-end observability across the ingestion pipeline:

  • Distributed tracingAzure Functions automatically correlate traces through Service Bus messages via Diagnostic-Id headers
  • Custom metricsTrack ingestion latency (message enqueued to Certyo 202 response) and anchoring latency (ingestion to on-chain confirmation)
  • AlertsConfigure alerts on dead-letter queue depth, function execution failures, and Certyo API error rates
  • WorkbooksBuild Azure Monitor Workbooks to visualize record throughput, anchoring SLA compliance, and verification success rates
Custom metrics in Azure Functionscsharp
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;

// In your function class constructor:
private readonly TelemetryClient _telemetry;

// After successful ingestion:
_telemetry.TrackMetric(new MetricTelemetry
{
    Name = "CertyoIngestionLatencyMs",
    Sum = stopwatch.ElapsedMilliseconds,
    Count = 1,
    Properties =
    {
        ["tenantId"] = record.TenantId,
        ["collection"] = record.Collection
    }
});

_telemetry.TrackEvent("CertyoRecordIngested", new Dictionary<string, string>
{
    ["recordId"] = record.RecordId,
    ["recordHash"] = result.RecordHash,
    ["tenantId"] = record.TenantId
});

// KQL query for Application Insights:
// customMetrics
// | where name == "CertyoIngestionLatencyMs"
// | summarize avg(value), percentile(value, 95) by bin(timestamp, 5m)
// | render timechart

Next steps

  • Deploy the Bicep template above to create the full infrastructure stack in minutes
  • Configure APIM policies for your Azure AD tenant and internal team structure
  • Set up Application Insights alerts for dead-letter queue depth and ingestion failures
  • Review the Ingestion Guide for details on record schema and idempotency
  • See the Verification Guide for cryptographic proof details

AI Integration · v1.0.0

AI Integration Skill

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

v1.0.0
What is this?
A markdown file containing Microsoft Azure-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

  • AuthenticationManaged Identity, Key Vault secrets, and APIM subscription keys
  • ArchitectureEvent Grid → Service Bus → Azure Function → Certyo reference architecture
  • Code examplesC# Azure Functions (ServiceBusTrigger, TimerTrigger), Bicep IaC template
  • API ManagementAPIM setup with rate limiting, JWT validation, and developer portal
  • Logic AppsNo-code orchestration template for SQL → Certyo → Teams notification
  • MonitoringApplication Insights custom metrics and KQL queries for observability

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-azure.md \
  https://www.certyos.com/developers/skills/certyo-azure-skill.md

# Use it in Claude Code
/certyo-azure "Generate a Service Bus to Certyo pipeline with Azure Functions and Key Vault"

Cursor / Copilot / Any AI Agent

Add the file to your project root or attach it to a conversation. The AI agent will use the Microsoft Azure-specific patterns, field mappings, and code examples to generate correct integration code.

# Add to your project
curl -o CERTYO_AZURE.md \
  https://www.certyos.com/developers/skills/certyo-azure-skill.md

# Then in your AI agent:
"Using the Certyo Microsoft Azure spec in CERTYO_AZURE.md,
 generate a service bus to certyo pipeline with azure functions and key vault"

CLAUDE.md Context File

Append the skill file to your project's CLAUDE.md so every Claude conversation has Microsoft Azure + Certyo context automatically.

# Append to your project's CLAUDE.md
echo "" >> CLAUDE.md
echo "## Certyo Microsoft Azure Integration" >> CLAUDE.md
cat CERTYO_AZURE.md >> CLAUDE.md