Integración con Microsoft Azure
Arquitectura de referencia para integrar Certyo con servicios gestionados de Azure — API Management, Service Bus, Functions, Logic Apps y Key Vault.
Requisitos previos
- Suscripción de Azure con rol de Colaborador
- Azure CLI v2.60 o posterior
- Clave API de Certyo (almacenada en Azure Key Vault — ver sección Autenticación más abajo)
- .NET 8 SDK (para desarrollo de Azure Functions)
Arquitectura de referencia
Esta arquitectura utiliza servicios nativos de Azure para una ingesta durable, observable y de grado empresarial hacia 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 captura eventos de dominio de tus sistemas fuente (SQL, Cosmos DB, Storage, aplicaciones personalizadas). Service Bus proporciona colas durables con manejo de dead-letter. Azure Functions transforman y envían registros a Certyo. Logic Apps orquestan el sondeo de verificación y notificaciones posteriores.
API Management (APIM)
Despliega el API de Certyo detrás de Azure API Management para gobernanza empresarial — limitación de tasa, validación JWT, transformación de solicitudes y un portal de desarrollador para equipos internos.
Importar la especificación OpenAPI
Registra Certyo como un backend en APIM, luego importa la definición OpenAPI para auto-generar políticas y documentación:
# 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"Políticas de APIM
Agrega políticas de entrada para limitación de tasa, inyección de clave API desde Key Vault y validación de solicitudes:
<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>Este patrón significa que los consumidores internos se autentican con tokens de Azure AD y nunca ven la clave API de Certyo. APIM maneja el intercambio de credenciales de forma transparente.
Service Bus
Usa Azure Service Bus como un buffer de mensajes durable entre tus sistemas empresariales y Certyo. La cola garantiza entrega al menos una vez con reintento configurable y manejo de dead-letter:
- Cola — punto a punto, un consumidor (la Azure Function que llama a Certyo)
- Topic + Suscripciones — distribución para patrones multi-consumidor (por ejemplo, ingesta a Certyo Y espejo a un data lake)
- Cola de dead-letter — los mensajes fallidos llegan aquí después del máximo de intentos de entrega, permitiendo investigación sin pérdida de datos
Azure Functions
Las funciones serverless actúan como el pegamento entre Service Bus y Certyo. Dos funciones clave:
1. Función de ingesta (trigger de Service Bus)
Activada por cada mensaje en la cola de Service Bus, lo transforma en un registro Certyo y llama a POST /api/v1/records:
2. Función de sondeo de verificación (trigger de Timer)
Se ejecuta según un horario para verificar si los registros ingestados recientemente han sido anclados on-chain, luego publica los resultados en 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
Para usuarios de negocio que necesitan orquestación sin código, Logic Apps proporciona un diseñador visual para conectar triggers empresariales a Certyo. Patrón común:
- Trigger — "Cuando se agrega una fila a la tabla SQL" o "Cuando se crea un registro de Dynamics 365"
- Transformar — Mapea los campos fuente al esquema de registro de Certyo usando el mapeador de datos integrado
- Acción HTTP — POST a https://www.certyos.com/api/v1/records con el encabezado X-API-Key
- Condición — Verificar 202 Accepted
- Notificar — Publicar en canal de Teams o enviar correo en caso de éxito/fallo
{
"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"] }
}
}
}
}Autenticación
El patrón de autenticación recomendado superpone la identidad nativa de Azure sobre la clave API de Certyo:
- Key Vault — Almacena el
X-API-Keyde Certyo como un secreto. Otorga acceso vía RBAC (rol Key Vault Secrets User) a los service principals que lo necesiten. - Managed Identity — Azure Functions y Logic Apps usan identidad administrada asignada por el sistema para recuperar la clave de Key Vault en tiempo de ejecución. Sin credenciales en código o configuración.
- Claves de suscripción APIM — Los consumidores internos se autentican con APIM usando claves de suscripción o tokens de Azure AD. APIM inyecta la clave API de Certyo en su nombre (ver la política anterior).
# 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"Monitoreo
Integra Application Insights para observabilidad de extremo a extremo en todo el pipeline de ingesta:
- Rastreo distribuido — Azure Functions correlaciona automáticamente trazas a través de mensajes de Service Bus vía encabezados
Diagnostic-Id - Métricas personalizadas — Rastrea la latencia de ingesta (mensaje en cola hasta respuesta 202 de Certyo) y la latencia de anclaje (ingesta hasta confirmación on-chain)
- Alertas — Configura alertas sobre la profundidad de la cola de dead-letter, fallos de ejecución de funciones y tasas de error del API de Certyo
- Workbooks — Construye Azure Monitor Workbooks para visualizar el throughput de registros, cumplimiento del SLA de anclaje y tasas de éxito de verificación
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 timechartPróximos pasos
- Despliega la plantilla Bicep anterior para crear toda la infraestructura en minutos
- Configura las políticas de APIM para tu tenant de Azure AD y la estructura de equipos internos
- Configura alertas de Application Insights para la profundidad de la cola de dead-letter y fallos de ingesta
- Revisa la Guía de ingesta para detalles sobre el esquema de registros e idempotencia
- Ve la Guía de verificación para detalles sobre pruebas criptográficas
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
- Autenticación — Managed Identity, secretos de Key Vault y claves de suscripción APIM
- Arquitectura — Event Grid → Service Bus → Azure Function → arquitectura de referencia Certyo
- Ejemplos de código — C# Azure Functions (ServiceBusTrigger, TimerTrigger), plantilla Bicep IaC
- API Management — Configuración de APIM con limitación de tasa, validación JWT y portal de desarrollador
- Logic Apps — Plantilla de orquestación sin código para SQL → Certyo → notificación de Teams
- Monitoreo — Métricas personalizadas de Application Insights y consultas KQL para observabilidad
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 "Genera un pipeline de Service Bus a Certyo con Azure Functions y 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,
genera un pipeline de service bus a certyo con azure functions y 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