---
name: Certyo + Microsoft Azure Integration
version: 1.0.0
description: Generate Azure → Certyo integration code with Managed Identity, Service Bus, Functions, APIM, and working examples
api_base: https://www.certyos.com
auth: X-API-Key header
last_updated: 2026-04-14
---

# Certyo + Microsoft Azure Integration Skill

This skill generates production-ready Microsoft Azure integrations that ingest records into Certyo's blockchain-anchored authenticity platform. It uses Managed Identity, Key Vault for secrets, Service Bus as a durable buffer, Azure Functions for processing, APIM for external exposure, and Bicep for infrastructure as code.

## Certyo API Reference

### Endpoints

| Method | Path | Description | Response |
|--------|------|-------------|----------|
| `POST` | `/api/v1/records` | Ingest a single record | `202 Accepted` |
| `POST` | `/api/v1/records/bulk` | Ingest up to 1000 records | `202 Accepted` |
| `POST` | `/api/v1/verify/record` | Verify blockchain anchoring | `200 OK` |
| `GET` | `/api/v1/records` | Query records | `200 OK` |

### Authentication

All requests require the `X-API-Key` header with a valid Certyo API key.

### Record Payload

```json
{
  "tenantId": "string (required)",
  "database": "string (required)",
  "collection": "string (required)",
  "recordId": "string (required)",
  "recordPayload": { "...any JSON (required)" },
  "clientId": "string (optional)",
  "recordVersion": "string (optional, default '1')",
  "operationType": "upsert|insert|update|delete (optional)",
  "sourceTimestamp": "ISO 8601 (optional)",
  "idempotencyKey": "string (optional)"
}
```

### Response (202 Accepted)

```json
{
  "recordId": "string",
  "recordHash": "string (SHA-256)",
  "tenantId": "string",
  "acceptedAt": "ISO 8601",
  "idempotencyReplayed": false
}
```

### Pipeline

Record ingestion flows through: Kafka accumulation (1000 records or 60s flush) then Merkle tree computation then IPFS pin then Polygon anchor (~60-90s end to end).

## Integration Pattern

```
Source Event (Event Grid / Service Bus / HTTP)
        |
   Service Bus Queue (durable buffer)
        |
   Azure Function (ServiceBusTrigger) — transform + POST to Certyo
        |
   Certyo API (/api/v1/records)
        |
   Timer Function — poll verification + write back
```

For external consumers, expose the Certyo API through Azure API Management (APIM) with subscription keys, rate limiting, and request/response transformation.

### Azure Services Used

| Service | Role |
|---------|------|
| **API Management** | Gateway for external consumers, rate limiting, caching |
| **Service Bus** | Durable message buffer between event sources and Functions |
| **Event Grid** | Event routing from Azure resources (Blob, Cosmos DB, etc.) |
| **Azure Functions** | Serverless compute for transformation and API calls |
| **Logic Apps** | Low-code orchestration for non-developer teams |
| **Key Vault** | Secure storage for Certyo API key |
| **Application Insights** | Monitoring, custom metrics, distributed tracing |

## Authentication

### Managed Identity for Azure Services

Use system-assigned Managed Identity on Azure Functions to access Key Vault, Service Bus, and other Azure resources. Never use connection strings for Azure service-to-service communication.

### Certyo API Key in Key Vault

Store the Certyo API key as a Key Vault secret. The Function App's Managed Identity gets `Secret Get` permission via RBAC.

### APIM Subscription Keys

For external consumers accessing Certyo through APIM, issue subscription keys per consumer. APIM injects the Certyo API key on the backend, so consumers never see it.

## Code Examples

### C# Azure Function: Service Bus Trigger for Record Ingestion

**`CertyoIngestionFunction.cs`** (isolated worker model, .NET 8):

```csharp
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;

namespace Certyo.Azure.Functions;

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

    public CertyoIngestionFunction(
        IHttpClientFactory httpClientFactory,
        ILogger<CertyoIngestionFunction> logger)
    {
        _httpClient = httpClientFactory.CreateClient("Certyo");
        _logger = logger;
        _certyoBaseUrl = Environment.GetEnvironmentVariable("CertyoBaseUrl")
            ?? "https://www.certyos.com";
    }

    [Function("IngestRecord")]
    public async Task Run(
        [ServiceBusTrigger("certyo-ingest", Connection = "ServiceBusConnection")]
        string messageBody,
        FunctionContext context)
    {
        var correlationId = context.InvocationId;
        _logger.LogInformation("Processing record ingestion. CorrelationId: {CorrelationId}",
            correlationId);

        try
        {
            var sourceRecord = JsonSerializer.Deserialize<JsonElement>(messageBody);

            var certyoPayload = new
            {
                tenantId = Environment.GetEnvironmentVariable("CertyoTenantId"),
                database = sourceRecord.GetProperty("source").GetString(),
                collection = sourceRecord.GetProperty("entityType").GetString(),
                recordId = sourceRecord.GetProperty("id").GetString(),
                recordPayload = sourceRecord,
                operationType = "upsert",
                sourceTimestamp = sourceRecord.TryGetProperty("modifiedAt", out var ts)
                    ? ts.GetString()
                    : DateTime.UtcNow.ToString("O"),
                idempotencyKey = $"{sourceRecord.GetProperty("source")}:" +
                    $"{sourceRecord.GetProperty("id")}:" +
                    $"{sourceRecord.GetProperty("version").GetString() ?? "1"}"
            };

            var apiKey = await GetApiKeyAsync();

            var request = new HttpRequestMessage(HttpMethod.Post,
                $"{_certyoBaseUrl}/api/v1/records");
            request.Headers.Add("X-API-Key", apiKey);
            request.Content = new StringContent(
                JsonSerializer.Serialize(certyoPayload),
                Encoding.UTF8,
                "application/json");

            var response = await _httpClient.SendAsync(request);

            if (response.StatusCode == System.Net.HttpStatusCode.Accepted)
            {
                var result = await response.Content.ReadAsStringAsync();
                var resultJson = JsonSerializer.Deserialize<JsonElement>(result);

                _logger.LogInformation(
                    "Record ingested. RecordId: {RecordId}, Hash: {RecordHash}, CorrelationId: {CorrelationId}",
                    resultJson.GetProperty("recordId").GetString(),
                    resultJson.GetProperty("recordHash").GetString(),
                    correlationId);
            }
            else
            {
                var errorBody = await response.Content.ReadAsStringAsync();
                _logger.LogError(
                    "Certyo API error. Status: {StatusCode}, Body: {ErrorBody}, CorrelationId: {CorrelationId}",
                    response.StatusCode, errorBody, correlationId);

                throw new HttpRequestException(
                    $"Certyo API returned {response.StatusCode}: {errorBody}");
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex,
                "Failed to ingest record. CorrelationId: {CorrelationId}", correlationId);
            throw; // Let Service Bus handle retry via delivery count
        }
    }

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

        var kvUri = Environment.GetEnvironmentVariable("KeyVaultUri")
            ?? throw new InvalidOperationException("KeyVaultUri not configured");

        var client = new SecretClient(new Uri(kvUri), new DefaultAzureCredential());
        var secret = await client.GetSecretAsync("CertyoApiKey");
        _apiKey = secret.Value.Value;
        return _apiKey;
    }
}
```

### C# Azure Function: Timer Trigger for Verification Polling

**`CertyoVerificationFunction.cs`**:

```csharp
using System.Text;
using System.Text.Json;
using Azure.Security.KeyVault.Secrets;
using Azure.Identity;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;

namespace Certyo.Azure.Functions;

public class CertyoVerificationFunction
{
    private readonly HttpClient _httpClient;
    private readonly ILogger<CertyoVerificationFunction> _logger;
    private readonly string _certyoBaseUrl;

    public CertyoVerificationFunction(
        IHttpClientFactory httpClientFactory,
        ILogger<CertyoVerificationFunction> logger)
    {
        _httpClient = httpClientFactory.CreateClient("Certyo");
        _logger = logger;
        _certyoBaseUrl = Environment.GetEnvironmentVariable("CertyoBaseUrl")
            ?? "https://www.certyos.com";
    }

    [Function("VerifyRecords")]
    public async Task Run(
        [TimerTrigger("0 */2 * * * *")] TimerInfo timer,
        FunctionContext context)
    {
        _logger.LogInformation("Verification polling started at {Time}", DateTime.UtcNow);

        var apiKey = await GetApiKeyAsync();
        var tenantId = Environment.GetEnvironmentVariable("CertyoTenantId");

        // Query recent unverified records
        var queryRequest = new HttpRequestMessage(HttpMethod.Get,
            $"{_certyoBaseUrl}/api/v1/records?tenantId={tenantId}");
        queryRequest.Headers.Add("X-API-Key", apiKey);

        var queryResponse = await _httpClient.SendAsync(queryRequest);
        if (!queryResponse.IsSuccessStatusCode)
        {
            _logger.LogWarning("Failed to query records: {Status}", queryResponse.StatusCode);
            return;
        }

        var records = JsonSerializer.Deserialize<JsonElement>(
            await queryResponse.Content.ReadAsStringAsync());

        if (records.ValueKind != JsonValueKind.Array) return;

        var verifiedCount = 0;
        var pendingCount = 0;

        foreach (var record in records.EnumerateArray())
        {
            var verifyPayload = new
            {
                tenantId,
                recordId = record.GetProperty("recordId").GetString(),
                recordHash = record.GetProperty("recordHash").GetString()
            };

            var verifyRequest = new HttpRequestMessage(HttpMethod.Post,
                $"{_certyoBaseUrl}/api/v1/verify/record");
            verifyRequest.Headers.Add("X-API-Key", apiKey);
            verifyRequest.Content = new StringContent(
                JsonSerializer.Serialize(verifyPayload),
                Encoding.UTF8,
                "application/json");

            var verifyResponse = await _httpClient.SendAsync(verifyRequest);
            if (verifyResponse.IsSuccessStatusCode)
            {
                var result = JsonSerializer.Deserialize<JsonElement>(
                    await verifyResponse.Content.ReadAsStringAsync());

                if (result.TryGetProperty("verified", out var verified) &&
                    verified.GetBoolean())
                {
                    verifiedCount++;
                    _logger.LogInformation(
                        "Record verified on-chain. RecordId: {RecordId}",
                        verifyPayload.recordId);
                }
                else
                {
                    pendingCount++;
                }
            }
        }

        _logger.LogInformation(
            "Verification complete. Verified: {Verified}, Pending: {Pending}",
            verifiedCount, pendingCount);
    }

    private async Task<string> GetApiKeyAsync()
    {
        var kvUri = Environment.GetEnvironmentVariable("KeyVaultUri")
            ?? throw new InvalidOperationException("KeyVaultUri not configured");

        var client = new SecretClient(new Uri(kvUri), new DefaultAzureCredential());
        var secret = await client.GetSecretAsync("CertyoApiKey");
        return secret.Value.Value;
    }
}
```

### Function App Program.cs (Isolated Worker)

**`Program.cs`**:

```csharp
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .ConfigureServices(services =>
    {
        services.AddHttpClient("Certyo", client =>
        {
            client.Timeout = TimeSpan.FromSeconds(30);
            client.DefaultRequestHeaders.Accept.Add(
                new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
        });
        services.AddApplicationInsightsTelemetryWorkerService();
        services.ConfigureFunctionsApplicationInsights();
    })
    .Build();

host.Run();
```

### Bicep Template: Service Bus + Function App + Key Vault

**`main.bicep`**:

```bicep
@description('Certyo integration resource prefix')
param prefix string = 'certyo'

@description('Location for all resources')
param location string = resourceGroup().location

@description('Certyo API key to store in Key Vault')
@secure()
param certyoApiKey string

@description('Certyo tenant ID')
param certyoTenantId string

var functionAppName = '${prefix}-func-${uniqueString(resourceGroup().id)}'
var keyVaultName = '${prefix}-kv-${uniqueString(resourceGroup().id)}'
var serviceBusName = '${prefix}-sb-${uniqueString(resourceGroup().id)}'
var storageName = '${prefix}st${uniqueString(resourceGroup().id)}'
var appInsightsName = '${prefix}-ai-${uniqueString(resourceGroup().id)}'
var hostingPlanName = '${prefix}-plan-${uniqueString(resourceGroup().id)}'

// Storage Account for Function App
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: storageName
  location: location
  sku: { name: 'Standard_LRS' }
  kind: 'StorageV2'
}

// Application Insights
resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
  name: appInsightsName
  location: location
  kind: 'web'
  properties: {
    Application_Type: 'web'
    RetentionInDays: 90
  }
}

// Service Bus Namespace + Queue
resource serviceBusNamespace 'Microsoft.ServiceBus/namespaces@2022-10-01-preview' = {
  name: serviceBusName
  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
    deadLetteringOnMessageExpiration: true
    defaultMessageTimeToLive: 'P7D'
    lockDuration: 'PT5M'
    maxSizeInMegabytes: 1024
  }
}

resource ingestDlq 'Microsoft.ServiceBus/namespaces/queues@2022-10-01-preview' = {
  parent: serviceBusNamespace
  name: 'certyo-ingest-dlq'
  properties: {
    maxSizeInMegabytes: 1024
    defaultMessageTimeToLive: 'P30D'
  }
}

// Key Vault
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = {
  name: keyVaultName
  location: location
  properties: {
    sku: { family: 'A', name: 'standard' }
    tenantId: subscription().tenantId
    enableRbacAuthorization: true
    enableSoftDelete: true
    softDeleteRetentionInDays: 90
  }
}

resource certyoApiKeySecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = {
  parent: keyVault
  name: 'CertyoApiKey'
  properties: {
    value: certyoApiKey
    contentType: 'text/plain'
  }
}

// Hosting Plan (Consumption)
resource hostingPlan 'Microsoft.Web/serverfarms@2023-01-01' = {
  name: hostingPlanName
  location: location
  sku: {
    name: 'Y1'
    tier: 'Dynamic'
  }
  properties: {
    reserved: false
  }
}

// Function App with Managed Identity
resource functionApp 'Microsoft.Web/sites@2023-01-01' = {
  name: functionAppName
  location: location
  kind: 'functionapp'
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    serverFarmId: hostingPlan.id
    siteConfig: {
      netFrameworkVersion: 'v8.0'
      appSettings: [
        { name: 'AzureWebJobsStorage', value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=core.windows.net;AccountKey=${storageAccount.listKeys().keys[0].value}' }
        { name: 'FUNCTIONS_EXTENSION_VERSION', value: '~4' }
        { name: 'FUNCTIONS_WORKER_RUNTIME', value: 'dotnet-isolated' }
        { name: 'APPLICATIONINSIGHTS_CONNECTION_STRING', value: appInsights.properties.ConnectionString }
        { name: 'ServiceBusConnection__fullyQualifiedNamespace', value: '${serviceBusNamespace.name}.servicebus.windows.net' }
        { name: 'KeyVaultUri', value: keyVault.properties.vaultUri }
        { name: 'CertyoBaseUrl', value: 'https://www.certyos.com' }
        { name: 'CertyoTenantId', value: certyoTenantId }
      ]
    }
  }
}

// RBAC: Function App -> Key Vault Secrets User
resource kvRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(keyVault.id, functionApp.id, '4633458b-17de-408a-b874-0445c86b69e6')
  scope: keyVault
  properties: {
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6') // Key Vault Secrets User
    principalId: functionApp.identity.principalId
    principalType: 'ServicePrincipal'
  }
}

// RBAC: Function App -> Service Bus Data Receiver
resource sbRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(serviceBusNamespace.id, functionApp.id, '4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0')
  scope: serviceBusNamespace
  properties: {
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0') // Azure Service Bus Data Receiver
    principalId: functionApp.identity.principalId
    principalType: 'ServicePrincipal'
  }
}

output functionAppName string = functionApp.name
output functionAppUrl string = 'https://${functionApp.properties.defaultHostName}'
output keyVaultUri string = keyVault.properties.vaultUri
output serviceBusNamespace string = '${serviceBusNamespace.name}.servicebus.windows.net'
```

### Azure CLI: APIM Setup for External Consumers

```bash
#!/bin/bash
# Setup Azure API Management with Certyo backend
# Prerequisites: az cli logged in, resource group exists

RESOURCE_GROUP="certyo-integration-rg"
APIM_NAME="certyo-apim"
LOCATION="eastus2"
CERTYO_API_KEY="your-certyo-api-key"

# Create APIM instance (Developer tier for non-production)
az apim create \
  --name "$APIM_NAME" \
  --resource-group "$RESOURCE_GROUP" \
  --location "$LOCATION" \
  --publisher-name "Your Organization" \
  --publisher-email "admin@yourorg.com" \
  --sku-name Developer

# Import Certyo API from OpenAPI spec
az apim api import \
  --resource-group "$RESOURCE_GROUP" \
  --service-name "$APIM_NAME" \
  --api-id "certyo-records" \
  --path "certyo" \
  --display-name "Certyo Blockchain Records" \
  --specification-format OpenApiJson \
  --specification-url "https://www.certyos.com/api/v1/openapi.json" \
  --service-url "https://www.certyos.com" \
  --protocols https \
  --subscription-required true

# Create named value for API key (stored as secret)
az apim nv create \
  --resource-group "$RESOURCE_GROUP" \
  --service-name "$APIM_NAME" \
  --named-value-id "certyo-api-key" \
  --display-name "Certyo API Key" \
  --value "$CERTYO_API_KEY" \
  --secret true

# Set inbound policy to inject X-API-Key header
az apim api policy create \
  --resource-group "$RESOURCE_GROUP" \
  --service-name "$APIM_NAME" \
  --api-id "certyo-records" \
  --xml-policy '<policies>
    <inbound>
      <base />
      <set-header name="X-API-Key" exists-action="override">
        <value>{{certyo-api-key}}</value>
      </set-header>
      <rate-limit calls="100" renewal-period="60" />
    </inbound>
    <backend><base /></backend>
    <outbound><base /></outbound>
    <on-error><base /></on-error>
  </policies>'

# Create a product for consumers
az apim product create \
  --resource-group "$RESOURCE_GROUP" \
  --service-name "$APIM_NAME" \
  --product-id "certyo-standard" \
  --display-name "Certyo Standard" \
  --description "Standard access to Certyo blockchain records API" \
  --subscription-required true \
  --approval-required true \
  --subscriptions-limit 5 \
  --state published

# Add API to product
az apim product api add \
  --resource-group "$RESOURCE_GROUP" \
  --service-name "$APIM_NAME" \
  --product-id "certyo-standard" \
  --api-id "certyo-records"

echo "APIM Gateway URL: https://${APIM_NAME}.azure-api.net/certyo"
```

### Logic App Template: Low-Code Ingestion

**`certyo-logic-app.json`**:

```json
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "logicAppName": {
      "type": "string",
      "defaultValue": "certyo-ingest-la"
    },
    "certyoApiKey": {
      "type": "securestring"
    },
    "certyoTenantId": {
      "type": "string"
    }
  },
  "resources": [
    {
      "type": "Microsoft.Logic/workflows",
      "apiVersion": "2019-05-01",
      "name": "[parameters('logicAppName')]",
      "location": "[resourceGroup().location]",
      "properties": {
        "state": "Enabled",
        "definition": {
          "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
          "contentVersion": "1.0.0.0",
          "triggers": {
            "When_messages_are_available_in_queue": {
              "type": "ApiConnection",
              "inputs": {
                "host": {
                  "connection": { "name": "@parameters('$connections')['servicebus']['connectionId']" }
                },
                "method": "get",
                "path": "/@{encodeURIComponent('certyo-ingest')}/messages/head",
                "queries": { "queueType": "Main" }
              },
              "recurrence": { "frequency": "Second", "interval": 30 }
            }
          },
          "actions": {
            "Parse_Message": {
              "type": "ParseJson",
              "inputs": {
                "content": "@base64ToString(triggerBody()?['ContentData'])",
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "string" },
                    "source": { "type": "string" },
                    "entityType": { "type": "string" },
                    "data": { "type": "object" }
                  }
                }
              },
              "runAfter": {}
            },
            "POST_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": "@body('Parse_Message')?['source']",
                  "collection": "@body('Parse_Message')?['entityType']",
                  "recordId": "@body('Parse_Message')?['id']",
                  "recordPayload": "@body('Parse_Message')?['data']",
                  "operationType": "upsert"
                },
                "retryPolicy": {
                  "type": "exponential",
                  "count": 4,
                  "interval": "PT10S",
                  "minimumInterval": "PT5S",
                  "maximumInterval": "PT1H"
                }
              },
              "runAfter": { "Parse_Message": ["Succeeded"] }
            }
          }
        }
      }
    }
  ]
}
```

## Verification & Write-back

The Timer-triggered Function (VerifyRecords) polls Certyo every 2 minutes for recently ingested records and verifies their blockchain anchoring status.

For write-back to Azure source systems:

- **Cosmos DB**: Use the Cosmos DB SDK with Managed Identity to patch the source document with `certyo_verified`, `certyo_recordHash`, and `certyo_anchor_tx` fields.
- **Azure SQL**: Use `SqlConnection` with `DefaultAzureCredential` to execute an UPDATE statement on the source table.
- **Blob Storage**: Write a `.certyo-proof.json` sidecar file next to the original blob with the verification result.
- **Event Grid**: Publish a `Certyo.Record.Verified` custom event so downstream systems can react to verification completion.

### Application Insights Custom Metrics

Track Certyo integration health with custom metrics:

```csharp
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.Extensibility;

// In your Function:
var telemetry = new TelemetryClient(TelemetryConfiguration.Active);

telemetry.TrackMetric("CertyoRecordIngested", 1, new Dictionary<string, string>
{
    ["tenantId"] = tenantId,
    ["database"] = database,
    ["collection"] = collection
});

telemetry.TrackMetric("CertyoVerificationLatencyMs", latencyMs);
telemetry.TrackMetric("CertyoVerificationSuccess", verified ? 1 : 0);
```

## Code Generation Rules

1. **Always use Managed Identity for Azure service-to-service authentication.** Use `DefaultAzureCredential` for Key Vault, Service Bus, Cosmos DB, and SQL. Never use connection strings with embedded keys for Azure services. The only exception is the Certyo API key itself, which must be retrieved from Key Vault.

2. **Store the Certyo API key in Key Vault, never in app settings or environment variables.** Use a Key Vault reference (`@Microsoft.KeyVault(...)`) in app settings or retrieve the secret at runtime using `SecretClient` with Managed Identity. Rotate the key by updating the Key Vault secret without redeploying.

3. **Use Service Bus as a durable buffer between event sources and Certyo.** Never call the Certyo API directly from an Event Grid subscriber or HTTP trigger. Service Bus provides at-least-once delivery, dead-letter queues, and message deferral. Set `maxDeliveryCount` to 10 and enable dead-lettering.

4. **Use Azure API Management (APIM) for external consumers.** Expose the Certyo API through APIM with subscription keys, rate limiting (100 calls/minute), and request validation. APIM injects the `X-API-Key` header via policy so consumers never handle the Certyo API key directly.

5. **Implement Application Insights custom metrics for all Certyo operations.** Track `CertyoRecordIngested`, `CertyoVerificationLatencyMs`, and `CertyoVerificationSuccess` metrics with tenant/database/collection dimensions. Set up alerts for ingestion failures and verification latency spikes.

6. **Use Bicep for all infrastructure as code.** Define all resources (Service Bus, Function App, Key Vault, APIM) in Bicep templates with parameterized values. Use `@secure()` for sensitive parameters. Never create resources manually in the portal for production environments.

7. **Use the isolated worker model for Azure Functions.** Target .NET 8 with `Microsoft.Azure.Functions.Worker` packages. Register `HttpClient` via `IHttpClientFactory` for connection pooling and resilience. Configure `host.json` with appropriate batching and concurrency settings.

8. **Implement exponential backoff retry on all HTTP calls to Certyo.** Configure retry policies on Service Bus triggers (via `maxDeliveryCount`) and on HTTP requests (via Polly or built-in retry). Use 4 retries with exponential backoff starting at 10 seconds.

9. **Use Event Grid for routing Azure-native events to Service Bus.** Subscribe Event Grid topics to Service Bus queues for Blob Storage events, Cosmos DB change feed, and custom application events. Use Event Grid filtering to route only relevant events.

10. **Separate ingestion and verification into distinct Function triggers.** Use a ServiceBusTrigger for record ingestion (event-driven, processes immediately) and a TimerTrigger for verification polling (every 2 minutes). Never combine both in a single function or use a polling approach for ingestion.
