Amazon Web Services Integration
Reference architecture for integrating Certyo with AWS using EventBridge, Lambda, Step Functions, and API Gateway.
Prerequisites
- AWS account with IAM permissions for EventBridge, Lambda, Step Functions, and Secrets Manager
- AWS CLI v2 configured with credentials
- Certyo API key (stored in AWS Secrets Manager — see Authentication section below)
- Node.js 20+ or Python 3.12+ (for Lambda development)
Reference Architecture
This architecture leverages AWS-native event routing and serverless compute to integrate with Certyo:
Source System ──► EventBridge ──► API Destination (Certyo) ──► POST /api/v1/records
│ │
DLQ (SQS) Step Functions
for failed (verification workflow)
deliveries │
┌──────┴──────┐
│ Wait 90s │
│ Verify │
│ Branch │
└──────┬──────┘
│
SNS notification
(anchored / failed)EventBridge receives domain events from your source systems (S3, DynamoDB Streams, custom applications). For simple forwarding, API Destinations call Certyo directly without a Lambda intermediary. For complex transformations, Lambda processes events before ingestion. Step Functions orchestrate multi-step verification workflows.
EventBridge API Destinations
The simplest integration pattern: EventBridge calls Certyo's API directly via an API Destination. No Lambda code required for straightforward event forwarding.
How it works
- Connection — Stores the Certyo API key with
API_KEYauth type. EventBridge manages credential rotation if you update the secret. - API Destination — Defines the Certyo endpoint URL, HTTP method, and invocation rate limit.
- Rule — Matches incoming events by source, detail-type, or custom patterns and routes them to the API Destination.
- Input Transformer — Maps your event schema to the Certyo record payload without any Lambda code.
- Retry + DLQ — Built-in retry with exponential backoff (up to 24 hours with 185 retries). Failed events land in an SQS dead-letter queue.
Lambda Functions
For events that require transformation, enrichment, or conditional logic before ingestion, use Lambda as the processing layer:
- S3 trigger — New objects in a bucket trigger a Lambda that reads the file, extracts records, and sends each to Certyo
- DynamoDB Streams — Capture change data from DynamoDB tables and forward modified items as Certyo records
- Webhook handler — Receive webhooks from third-party systems via API Gateway, transform, and ingest
Step Functions
Orchestrate multi-step workflows that ingest a record, wait for anchoring, verify the result, and branch on success or failure:
- Standard Workflow — For long-running verification flows (up to 1 year execution). Uses Wait states to pause for Certyo's ~60-90 second anchoring window.
- Express Workflow — For high-volume, short-duration executions (up to 5 minutes). Ideal for bulk ingestion where verification is handled separately.
API Gateway
Front Certyo for internal consumers with API Gateway for usage plans, throttling, and API key management:
- Create a REST API proxy that forwards requests to
https://www.certyos.com/api/v1/ - Add a usage plan with throttling (e.g., 100 requests/second per consumer) and monthly quotas
- Inject the Certyo
X-API-Keyvia a Lambda authorizer or VTL mapping template - Internal consumers authenticate with their own API Gateway keys — they never see the Certyo key
Code Samples
import json
import os
import urllib.parse
import boto3
import urllib3
secrets_client = boto3.client("secretsmanager")
s3_client = boto3.client("s3")
http = urllib3.PoolManager()
CERTYO_API_URL = os.environ.get("CERTYO_API_URL", "https://www.certyos.com")
CERTYO_TENANT_ID = os.environ["CERTYO_TENANT_ID"]
SECRET_ARN = os.environ["CERTYO_SECRET_ARN"]
_api_key_cache = None
def get_api_key():
global _api_key_cache
if _api_key_cache is None:
secret = secrets_client.get_secret_value(SecretId=SECRET_ARN)
_api_key_cache = json.loads(secret["SecretString"])["apiKey"]
return _api_key_cache
def handler(event, context):
"""
Triggered by S3 PutObject events. Reads the uploaded JSON file,
extracts records, and sends each to Certyo for blockchain anchoring.
"""
api_key = get_api_key()
results = []
for record in event["Records"]:
bucket = record["s3"]["bucket"]["name"]
key = urllib.parse.unquote_plus(record["s3"]["object"]["key"])
print(f"Processing s3://{bucket}/{key}")
# Read the S3 object
obj = s3_client.get_object(Bucket=bucket, Key=key)
body = json.loads(obj["Body"].read().decode("utf-8"))
# Support single record or array of records
items = body if isinstance(body, list) else [body]
for item in items:
record_id = item.get("id", key.split("/")[-1].replace(".json", ""))
payload = {
"tenantId": CERTYO_TENANT_ID,
"database": f"s3-{bucket}",
"collection": key.rsplit("/", 1)[0] if "/" in key else "root",
"recordId": record_id,
"recordVersion": str(item.get("version", "1")),
"operationType": "upsert",
"recordPayload": item,
"sourceTimestamp": record["eventTime"],
"idempotencyKey": f"{record_id}-{record['eventTime'][:10]}",
}
response = http.request(
"POST",
f"{CERTYO_API_URL}/api/v1/records",
body=json.dumps(payload),
headers={
"X-API-Key": api_key,
"Content-Type": "application/json",
},
)
result = json.loads(response.data.decode("utf-8"))
if response.status != 202:
print(f"ERROR: Certyo returned {response.status} for {record_id}: {result}")
raise Exception(f"Ingestion failed for {record_id}: {response.status}")
print(f"Record {record_id} accepted. Hash: {result['recordHash']}")
results.append(result)
return {
"statusCode": 200,
"body": json.dumps({
"message": f"Ingested {len(results)} records",
"records": results,
}),
}import * as cdk from "aws-cdk-lib";
import * as events from "aws-cdk-lib/aws-events";
import * as targets from "aws-cdk-lib/aws-events-targets";
import * as sqs from "aws-cdk-lib/aws-sqs";
import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager";
import { Construct } from "constructs";
export class CertyoEventBridgeStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Store the Certyo API key in Secrets Manager
const certyoSecret = new secretsmanager.Secret(this, "CertyoApiKey", {
secretName: "certyo/api-key",
description: "Certyo API key for record ingestion",
secretObjectValue: {
apiKey: cdk.SecretValue.unsafePlainText("REPLACE_WITH_ACTUAL_KEY"),
},
});
// Dead-letter queue for failed deliveries
const dlq = new sqs.Queue(this, "CertyoDlq", {
queueName: "certyo-eventbridge-dlq",
retentionPeriod: cdk.Duration.days(14),
encryption: sqs.QueueEncryption.SQS_MANAGED,
});
// EventBridge Connection — stores Certyo API key credentials
const connection = new events.Connection(this, "CertyoConnection", {
connectionName: "certyo-api-connection",
description: "Connection to Certyo Records API",
authorization: events.Authorization.apiKey(
"X-API-Key",
cdk.SecretValue.secretsManager(certyoSecret.secretArn, {
jsonField: "apiKey",
})
),
});
// EventBridge API Destination — the Certyo endpoint
const apiDestination = new events.ApiDestination(
this,
"CertyoApiDestination",
{
apiDestinationName: "certyo-records-api",
connection,
endpoint: "https://www.certyos.com/api/v1/records",
httpMethod: events.HttpMethod.POST,
rateLimitPerSecond: 100,
description: "Certyo record ingestion endpoint",
}
);
// Custom event bus for your domain events
const bus = new events.EventBus(this, "DomainEventBus", {
eventBusName: "domain-events",
});
// Rule: route matching events to Certyo API Destination
new events.Rule(this, "IngestToCertyoRule", {
eventBus: bus,
ruleName: "route-to-certyo",
description: "Forward domain record events to Certyo for blockchain anchoring",
eventPattern: {
source: ["com.yourorg.orders", "com.yourorg.inventory"],
detailType: ["RecordCreated", "RecordUpdated"],
},
targets: [
new targets.ApiDestination(apiDestination, {
// Transform the EventBridge event into Certyo record format
event: events.RuleTargetInput.fromObject({
tenantId: events.EventField.fromPath("$.detail.tenantId"),
database: events.EventField.fromPath("$.source"),
collection: events.EventField.fromPath("$.detail-type"),
recordId: events.EventField.fromPath("$.detail.recordId"),
recordVersion: events.EventField.fromPath("$.detail.version"),
operationType: "upsert",
recordPayload: events.EventField.fromPath("$.detail.payload"),
sourceTimestamp: events.EventField.fromPath("$.time"),
idempotencyKey: events.EventField.fromPath("$.detail.idempotencyKey"),
}),
deadLetterQueue: dlq,
retryAttempts: 185,
maxEventAge: cdk.Duration.hours(24),
}),
],
});
// Outputs
new cdk.CfnOutput(this, "EventBusArn", { value: bus.eventBusArn });
new cdk.CfnOutput(this, "DlqUrl", { value: dlq.queueUrl });
new cdk.CfnOutput(this, "SecretArn", { value: certyoSecret.secretArn });
}
}{
"Comment": "Certyo record ingestion and verification workflow",
"StartAt": "IngestRecord",
"States": {
"IngestRecord": {
"Type": "Task",
"Resource": "arn:aws:states:::http:invoke",
"Parameters": {
"ApiEndpoint": "https://www.certyos.com/api/v1/records",
"Method": "POST",
"Authentication": {
"ConnectionArn.$": "$.connectionArn"
},
"RequestBody": {
"tenantId.$": "$.tenantId",
"database.$": "$.database",
"collection.$": "$.collection",
"recordId.$": "$.recordId",
"recordVersion.$": "$.recordVersion",
"operationType": "upsert",
"recordPayload.$": "$.recordPayload",
"sourceTimestamp.$": "$$.State.EnteredTime",
"idempotencyKey.$": "States.Format('{}-{}-{}', $.recordId, $.recordVersion, States.ArrayGetItem(States.StringSplit($$.State.EnteredTime, 'T'), 0))"
},
"Headers": {
"Content-Type": "application/json"
}
},
"ResultPath": "$.ingestionResult",
"Retry": [
{
"ErrorEquals": ["States.Http.StatusCode.429", "States.Http.StatusCode.503"],
"IntervalSeconds": 5,
"MaxAttempts": 3,
"BackoffRate": 2.0
}
],
"Catch": [
{
"ErrorEquals": ["States.ALL"],
"Next": "IngestionFailed",
"ResultPath": "$.error"
}
],
"Next": "WaitForAnchoring"
},
"WaitForAnchoring": {
"Type": "Wait",
"Seconds": 90,
"Comment": "Wait for Certyo's accumulation window (60s) + Polygon confirmation (~30s)",
"Next": "VerifyRecord"
},
"VerifyRecord": {
"Type": "Task",
"Resource": "arn:aws:states:::http:invoke",
"Parameters": {
"ApiEndpoint": "https://www.certyos.com/api/v1/verify/record",
"Method": "POST",
"Authentication": {
"ConnectionArn.$": "$.connectionArn"
},
"RequestBody": {
"tenantId.$": "$.tenantId",
"database.$": "$.database",
"collection.$": "$.collection",
"recordId.$": "$.recordId",
"payload.$": "$.recordPayload"
},
"Headers": {
"Content-Type": "application/json"
}
},
"ResultPath": "$.verificationResult",
"Retry": [
{
"ErrorEquals": ["States.Http.StatusCode.429"],
"IntervalSeconds": 2,
"MaxAttempts": 3,
"BackoffRate": 2.0
}
],
"Catch": [
{
"ErrorEquals": ["States.ALL"],
"Next": "VerificationFailed",
"ResultPath": "$.error"
}
],
"Next": "CheckVerificationResult"
},
"CheckVerificationResult": {
"Type": "Choice",
"Choices": [
{
"And": [
{ "Variable": "$.verificationResult.ResponseBody.verified", "BooleanEquals": true },
{ "Variable": "$.verificationResult.ResponseBody.anchoredOnChain", "BooleanEquals": true }
],
"Next": "RecordAnchored"
},
{
"Variable": "$.verificationResult.ResponseBody.verified",
"BooleanEquals": false,
"Next": "RetryVerification"
}
],
"Default": "RetryVerification"
},
"RetryVerification": {
"Type": "Pass",
"Parameters": {
"retryCount.$": "States.MathAdd($.retryCount, 1)"
},
"ResultPath": "$.retryState",
"Next": "CheckRetryLimit"
},
"CheckRetryLimit": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.retryState.retryCount",
"NumericGreaterThan": 5,
"Next": "VerificationFailed"
}
],
"Default": "WaitBeforeRetry"
},
"WaitBeforeRetry": {
"Type": "Wait",
"Seconds": 30,
"Next": "VerifyRecord"
},
"RecordAnchored": {
"Type": "Task",
"Resource": "arn:aws:states:::sns:publish",
"Parameters": {
"TopicArn.$": "$.snsTopicArn",
"Subject": "Certyo Record Anchored",
"Message": {
"recordId.$": "$.recordId",
"tenantId.$": "$.tenantId",
"recordHash.$": "$.ingestionResult.ResponseBody.recordHash",
"verified": true,
"anchoredOnChain": true,
"polygonScanUrl.$": "$.verificationResult.ResponseBody.onChainProof.polygonScanEventUrl"
}
},
"End": true
},
"IngestionFailed": {
"Type": "Task",
"Resource": "arn:aws:states:::sns:publish",
"Parameters": {
"TopicArn.$": "$.snsTopicArn",
"Subject": "Certyo Ingestion Failed",
"Message": {
"recordId.$": "$.recordId",
"error.$": "$.error"
}
},
"End": true
},
"VerificationFailed": {
"Type": "Task",
"Resource": "arn:aws:states:::sns:publish",
"Parameters": {
"TopicArn.$": "$.snsTopicArn",
"Subject": "Certyo Verification Failed",
"Message": {
"recordId.$": "$.recordId",
"error": "Verification did not succeed after maximum retries"
}
},
"End": true
}
}
}# Store the Certyo API key in Secrets Manager
aws secretsmanager create-secret \
--name certyo/api-key \
--description "Certyo API key for record ingestion" \
--secret-string '{"apiKey":"certyo_sk_live_your_key_here"}'
# Create an EventBridge Connection with API Key auth
# The connection securely stores the Certyo API key
aws events create-connection \
--name certyo-api-connection \
--authorization-type API_KEY \
--auth-parameters '{
"ApiKeyAuthParameters": {
"ApiKeyName": "X-API-Key",
"ApiKeyValue": "certyo_sk_live_your_key_here"
}
}' \
--description "Connection to Certyo Records API"
# Wait for connection to be AUTHORIZED
aws events describe-connection \
--name certyo-api-connection \
--query 'ConnectionState'
# Create the API Destination pointing to Certyo
CONNECTION_ARN=$(aws events describe-connection \
--name certyo-api-connection \
--query 'ConnectionArn' --output text)
aws events create-api-destination \
--name certyo-records-api \
--connection-arn $CONNECTION_ARN \
--invocation-endpoint "https://www.certyos.com/api/v1/records" \
--http-method POST \
--invocation-rate-limit-per-second 100 \
--description "Certyo record ingestion endpoint"
# Create a custom event bus
aws events create-event-bus --name domain-events
# Create a dead-letter queue for failed deliveries
aws sqs create-queue \
--queue-name certyo-eventbridge-dlq \
--attributes '{
"MessageRetentionPeriod": "1209600",
"SqsManagedSseEnabled": "true"
}'
DLQ_ARN=$(aws sqs get-queue-attributes \
--queue-url $(aws sqs get-queue-url --queue-name certyo-eventbridge-dlq --query 'QueueUrl' --output text) \
--attribute-names QueueArn --query 'Attributes.QueueArn' --output text)
# Create a rule that matches domain record events
API_DEST_ARN=$(aws events describe-api-destination \
--name certyo-records-api \
--query 'ApiDestinationArn' --output text)
# Create IAM role for EventBridge to invoke the API Destination
ROLE_ARN=$(aws iam create-role \
--role-name EventBridgeCertyoRole \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "events.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}' --query 'Role.Arn' --output text)
aws iam put-role-policy \
--role-name EventBridgeCertyoRole \
--policy-name InvokeApiDestination \
--policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["events:InvokeApiDestination"],
"Resource": ["'"$API_DEST_ARN"'"]
}]
}'
aws events put-rule \
--name route-to-certyo \
--event-bus-name domain-events \
--event-pattern '{
"source": ["com.yourorg.orders", "com.yourorg.inventory"],
"detail-type": ["RecordCreated", "RecordUpdated"]
}' \
--description "Forward domain record events to Certyo"
# Attach the API Destination as the rule target with input transformation
aws events put-targets \
--rule route-to-certyo \
--event-bus-name domain-events \
--targets '[{
"Id": "certyo-api-destination",
"Arn": "'"$API_DEST_ARN"'",
"RoleArn": "'"$ROLE_ARN"'",
"InputTransformer": {
"InputPathsMap": {
"tenantId": "$.detail.tenantId",
"database": "$.source",
"collection": "$.detail-type",
"recordId": "$.detail.recordId",
"version": "$.detail.version",
"payload": "$.detail.payload",
"timestamp": "$.time",
"idempotencyKey": "$.detail.idempotencyKey"
},
"InputTemplate": "{"tenantId": <tenantId>, "database": <database>, "collection": <collection>, "recordId": <recordId>, "recordVersion": <version>, "operationType": "upsert", "recordPayload": <payload>, "sourceTimestamp": <timestamp>, "idempotencyKey": <idempotencyKey>}"
},
"RetryPolicy": {
"MaximumRetryAttempts": 185,
"MaximumEventAgeInSeconds": 86400
},
"DeadLetterConfig": {
"Arn": "'"$DLQ_ARN"'"
}
}]'
echo "EventBridge pipeline configured. Send events to the 'domain-events' bus."
echo "Events matching the rule will be forwarded to Certyo automatically."Authentication
The recommended authentication pattern uses AWS-native secrets management with IAM for service-to-service authorization:
- Secrets Manager — Store the Certyo
X-API-Keyas a secret. Enable automatic rotation if your organization requires it. - EventBridge Connection — The connection object securely stores the API key and injects it into every API Destination request. You do not handle the key in application code.
- Lambda execution role — Grant Lambda functions
secretsmanager:GetSecretValueon the specific secret ARN. Use the least-privilege principle. - API Gateway API keys — For internal consumers, issue API Gateway keys tied to usage plans. The gateway injects the Certyo key via a Lambda authorizer or mapping template.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadCertyoApiKey",
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue"
],
"Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:certyo/api-key-*"
}
]
}Cost Optimization
Choose the right integration pattern based on your workload to minimize AWS costs:
- EventBridge API Destinations (cheapest) — No Lambda invocation costs. You pay only for EventBridge events ($1.00 per million events) and API Destination invocations. Use this for simple event forwarding where no transformation is needed.
- EventBridge + Input Transformer — If you only need to reshape the event payload (rename fields, extract nested values), use the EventBridge Input Transformer instead of Lambda. Zero compute cost.
- Lambda (for transformations) — Reserve Lambda for cases where you need to read from S3, call external APIs, or apply complex business logic before ingesting. Use ARM64 (Graviton) for 20% cost savings.
- Step Functions Express — For high-volume verification workflows (>100K executions/month), Express Workflows cost up to 80% less than Standard Workflows.
Next steps
- Deploy the CDK stack above to provision the full EventBridge pipeline in minutes
- Publish a test event to the
domain-eventsbus and verify it arrives in Certyo - Deploy the Step Functions verification workflow for end-to-end anchoring confirmation
- 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 AWS + Certyo integration code for any language or framework.
What's inside
- Authentication — EventBridge Connection with API Key, Lambda with Secrets Manager
- Architecture — EventBridge → API Destination or Lambda → Certyo → Step Functions verify
- Code examples — Python Lambda, CDK TypeScript stack, Step Functions ASL workflow
- API Destinations — Zero-Lambda direct forwarding to Certyo with retry policies
- Step Functions — Verification workflow with Wait states, retry loops, and branching
- Cost optimization — API Destinations vs Lambda vs Express Workflows comparison
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-aws.md \
https://www.certyos.com/developers/skills/certyo-aws-skill.md
# Use it in Claude Code
/certyo-aws "Generate an EventBridge API Destination that forwards events to 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 AWS-specific patterns, field mappings, and code examples to generate correct integration code.
# Add to your project
curl -o CERTYO_AWS.md \
https://www.certyos.com/developers/skills/certyo-aws-skill.md
# Then in your AI agent:
"Using the Certyo AWS spec in CERTYO_AWS.md,
generate an eventbridge api destination that forwards events to certyo"CLAUDE.md Context File
Append the skill file to your project's CLAUDE.md so every Claude conversation has AWS + Certyo context automatically.
# Append to your project's CLAUDE.md
echo "" >> CLAUDE.md
echo "## Certyo AWS Integration" >> CLAUDE.md
cat CERTYO_AWS.md >> CLAUDE.md