Microsoft Azure Integration
Reference architecture for integrating Certyo with Azure managed services — API Management, Service Bus, Functions, Logic Apps, and Key Vault.
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:
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 / EmailEvent 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:
# 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:
<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:
- Queue — point-to-point, one consumer (the Azure Function that calls Certyo)
- Topic + Subscriptions — fan-out for multi-consumer patterns (e.g., ingest to Certyo AND mirror to a data lake)
- Dead-letter queue — failed 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:
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);@description('Azure region for all resources')
param location string = resourceGroup().location
@description('Certyo API base URL')
param certyoApiUrl string = 'https://www.certyos.com'
// --- Service Bus Namespace + Queue ---
resource serviceBusNamespace 'Microsoft.ServiceBus/namespaces@2022-10-01-preview' = {
name: 'certyo-sb-${uniqueString(resourceGroup().id)}'
location: location
sku: {
name: 'Standard'
tier: 'Standard'
}
}
resource ingestQueue 'Microsoft.ServiceBus/namespaces/queues@2022-10-01-preview' = {
parent: serviceBusNamespace
name: 'certyo-ingest'
properties: {
maxDeliveryCount: 10
lockDuration: 'PT5M'
defaultMessageTimeToLive: 'P14D'
deadLetteringOnMessageExpiration: true
enablePartitioning: false
}
}
// --- Key Vault for API Key ---
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = {
name: 'certyo-kv-${uniqueString(resourceGroup().id)}'
location: location
properties: {
sku: { family: 'A', name: 'standard' }
tenantId: subscription().tenantId
enableRbacAuthorization: true
enableSoftDelete: true
softDeleteRetentionInDays: 90
}
}
// --- Storage Account for Function App ---
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: 'certyofn${uniqueString(resourceGroup().id)}'
location: location
sku: { name: 'Standard_LRS' }
kind: 'StorageV2'
}
// --- App Service Plan (Consumption) ---
resource hostingPlan 'Microsoft.Web/serverfarms@2023-01-01' = {
name: 'certyo-plan'
location: location
sku: {
name: 'Y1'
tier: 'Dynamic'
}
properties: {
reserved: true
}
}
// --- Function App ---
resource functionApp 'Microsoft.Web/sites@2023-01-01' = {
name: 'certyo-fn-${uniqueString(resourceGroup().id)}'
location: location
kind: 'functionapp,linux'
identity: {
type: 'SystemAssigned'
}
properties: {
serverFarmId: hostingPlan.id
siteConfig: {
linuxFxVersion: 'DOTNET-ISOLATED|8.0'
appSettings: [
{ name: 'AzureWebJobsStorage', value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccount.listKeys().keys[0].value}' }
{ name: 'FUNCTIONS_EXTENSION_VERSION', value: '~4' }
{ name: 'FUNCTIONS_WORKER_RUNTIME', value: 'dotnet-isolated' }
{ name: 'ServiceBusConnection', value: serviceBusNamespace.listKeys(serviceBusNamespace::authRule.id, '2022-10-01-preview').primaryConnectionString }
{ name: 'KEY_VAULT_URI', value: keyVault.properties.vaultUri }
{ name: 'CERTYO_API_URL', value: certyoApiUrl }
]
}
}
}
resource authRule 'Microsoft.ServiceBus/namespaces/AuthorizationRules@2022-10-01-preview' existing = {
parent: serviceBusNamespace
name: 'RootManageSharedAccessKey'
}
// --- Key Vault access for Function App's Managed Identity ---
resource kvRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(keyVault.id, functionApp.id, '4633458b-17de-408a-b874-0445c86b69e6')
scope: keyVault
properties: {
principalId: functionApp.identity.principalId
roleDefinitionId: subscriptionResourceId(
'Microsoft.Authorization/roleDefinitions',
'4633458b-17de-408a-b874-0445c86b69e6' // Key Vault Secrets User
)
principalType: 'ServicePrincipal'
}
}
output functionAppName string = functionApp.name
output serviceBusNamespace string = serviceBusNamespace.name
output keyVaultUri string = keyVault.properties.vaultUri# Variables
RG="certyo-rg"
LOCATION="eastus"
APIM_NAME="certyo-apim"
KV_NAME="certyo-kv"
# Create resource group
az group create --name $RG --location $LOCATION
# Store Certyo API key in Key Vault
az keyvault secret set \
--vault-name $KV_NAME \
--name certyo-api-key \
--value "certyo_sk_live_your_key_here"
# Create APIM Named Value backed by Key Vault
az apim nv create \
--resource-group $RG \
--service-name $APIM_NAME \
--named-value-id certyo-api-key \
--display-name "Certyo API Key" \
--secret true \
--value "{{certyo-api-key}}"
# Create a Product for internal consumers
az apim product create \
--resource-group $RG \
--service-name $APIM_NAME \
--product-id certyo-internal \
--title "Certyo Internal" \
--description "Internal access to Certyo Records API" \
--subscription-required true \
--approval-required false \
--state published
# Add the Certyo API to the product
az apim product api add \
--resource-group $RG \
--service-name $APIM_NAME \
--product-id certyo-internal \
--api-id certyo-records
# Create a subscription for a team
az apim subscription create \
--resource-group $RG \
--service-name $APIM_NAME \
--product-id certyo-internal \
--subscription-id "team-orders" \
--display-name "Orders Team"using System.Net.Http.Json;
using Azure.Identity;
using Azure.Messaging.EventGrid;
using Azure.Security.KeyVault.Secrets;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
namespace Certyo.Functions;
public class CertyoVerificationPoller
{
private readonly HttpClient _http;
private readonly ILogger<CertyoVerificationPoller> _logger;
private string? _apiKey;
public CertyoVerificationPoller(
IHttpClientFactory httpClientFactory,
ILogger<CertyoVerificationPoller> logger)
{
_http = httpClientFactory.CreateClient("Certyo");
_logger = logger;
}
/// <summary>
/// Runs every 2 minutes. Queries Certyo for recently ingested records
/// that have not yet been verified, then publishes anchoring events
/// to Event Grid for downstream subscribers.
/// </summary>
[Function("VerificationPoller")]
public async Task Run(
[TimerTrigger("0 */2 * * * *")] TimerInfo timer,
FunctionContext context)
{
var apiKey = await GetApiKeyAsync();
var tenantId = Environment.GetEnvironmentVariable("CERTYO_TENANT_ID")!;
// Query pending snapshots from the last 10 minutes
var since = DateTimeOffset.UtcNow.AddMinutes(-10).ToString("o");
var request = new HttpRequestMessage(HttpMethod.Get,
$"/api/v1/snapshots?tenantId={tenantId}&status=PendingAnchor&since={since}");
request.Headers.Add("X-API-Key", apiKey);
var response = await _http.SendAsync(request);
response.EnsureSuccessStatusCode();
var snapshots = await response.Content
.ReadFromJsonAsync<SnapshotQueryResult>();
if (snapshots?.Items is null || snapshots.Items.Length == 0)
{
_logger.LogInformation("No pending snapshots found.");
return;
}
var eventGridEndpoint = Environment.GetEnvironmentVariable("EVENT_GRID_ENDPOINT")!;
var eventGridClient = new EventGridPublisherClient(
new Uri(eventGridEndpoint), new DefaultAzureCredential());
foreach (var snapshot in snapshots.Items)
{
if (snapshot.Status == "Anchored" && snapshot.OnChainConfirmed)
{
_logger.LogInformation(
"Snapshot {SnapshotId} anchored on Polygon. Publishing event.",
snapshot.SnapshotId);
await eventGridClient.SendEventAsync(new EventGridEvent(
subject: $"/certyo/records/{tenantId}/{snapshot.SnapshotId}",
eventType: "Certyo.Record.Anchored",
dataVersion: "1.0",
data: new
{
snapshot.SnapshotId,
snapshot.Status,
snapshot.OnChainConfirmed,
snapshot.MerkleRoot,
snapshot.AnchoredAt,
TenantId = tenantId
}));
}
}
_logger.LogInformation(
"Processed {Count} snapshots.", snapshots.Items.Length);
}
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 SnapshotQueryResult(SnapshotItem[] Items);
public record SnapshotItem(
string SnapshotId,
string Status,
bool OnChainConfirmed,
string? MerkleRoot,
DateTimeOffset? AnchoredAt);Logic Apps
For business users who need no-code orchestration, Logic Apps provide a visual designer to connect enterprise triggers to Certyo. Common pattern:
- Trigger — "When a row is added to SQL table" or "When a Dynamics 365 record is created"
- Transform — Map source fields to the Certyo record schema using the built-in data mapper
- HTTP action — POST to https://www.certyos.com/api/v1/records with the X-API-Key header
- Condition — Check for 202 Accepted
- Notify — Post to Teams channel or send email on success/failure
{
"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 Vault — Store the Certyo
X-API-Keyas a secret. Grant access via RBAC (Key Vault Secrets User role) to service principals that need it. - Managed Identity — Azure 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 keys — Internal consumers authenticate to APIM with subscription keys or Azure AD tokens. APIM injects the Certyo API key on their behalf (see the policy above).
# 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 tracing — Azure Functions automatically correlate traces through Service Bus messages via
Diagnostic-Idheaders - Custom metrics — Track ingestion latency (message enqueued to Certyo 202 response) and anchoring latency (ingestion to on-chain confirmation)
- Alerts — Configure alerts on dead-letter queue depth, function execution failures, and Certyo API error rates
- Workbooks — Build Azure Monitor Workbooks to visualize record throughput, anchoring SLA compliance, and verification success rates
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 timechartNext 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 Skill
Download a skill file that enables AI agents to generate working Microsoft Azure + Certyo integration code for any language or framework.
What's inside
- Authentication — Managed Identity, Key Vault secrets, and APIM subscription keys
- Architecture — Event Grid → Service Bus → Azure Function → Certyo reference architecture
- Code examples — C# Azure Functions (ServiceBusTrigger, TimerTrigger), Bicep IaC template
- API Management — APIM setup with rate limiting, JWT validation, and developer portal
- Logic Apps — No-code orchestration template for SQL → Certyo → Teams notification
- Monitoring — Application 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