Sorcha Platform - Integration Guide
Version: 1.0.0 Last Updated: 2025-11-17 Sprint: 7
Table of Contents
- Introduction
- Integration Patterns
- Quick Start Integration
- Blueprint-Based Workflows
- Wallet Integration
- Register Integration
- Real-time Notifications
- Best Practices
- Troubleshooting
- Advanced Topics
Introduction
This guide provides step-by-step instructions for integrating applications with the Sorcha Platform. Whether you're building a frontend application, backend service, or integration middleware, this guide covers the essential patterns and practices.
What You'll Learn
- How to create and manage blueprints
- How to integrate wallet services for cryptographic operations
- How to submit and track transactions on the register
- How to handle real-time notifications
- Best practices for production deployments
Prerequisites
- Basic understanding of REST APIs
- Familiarity with JSON and HTTP
- Development environment with HTTP client capabilities
- .NET 10 SDK (for running the platform)
Integration Patterns
Pattern 1: Direct API Integration
Use Case: Simple applications with direct HTTP access
┌─────────────┐
│ Your App │
└──────┬──────┘
│ HTTP/REST
▼
┌─────────────┐
│ API Gateway │
└─────────────┘Advantages:
- Simple and straightforward
- No additional dependencies
- Full control over requests
Example Technologies:
- JavaScript (fetch, axios)
- C# (HttpClient)
- Python (requests)
- Any HTTP client
Pattern 2: SDK Integration (Future)
Use Case: Applications needing type-safe, language-specific integration
┌─────────────┐
│ Your App │
│ │
│ Sorcha SDK │
└──────┬──────┘
│
▼
┌─────────────┐
│ API Gateway │
└─────────────┘Status: Coming in future releases
Pattern 3: Event-Driven Integration
Use Case: Real-time applications requiring instant updates
┌─────────────┐
│ Your App │
│ │
│ SignalR │◄─── Real-time Events
└──────┬──────┘
│ REST API
▼
┌─────────────┐
│ API Gateway │
└─────────────┘Quick Start Integration
Step 1: Set Up Your Development Environment
# Clone the repository
git clone https://github.com/yourusername/Sorcha.git
cd Sorcha
# Run with .NET Aspire
dotnet run --project src/Apps/Sorcha.AppHostThe platform will start with:
- API Gateway: http://localhost:5000
- Swagger/Scalar UI: http://localhost:5000/scalar/v1
Step 2: Create Your First Wallet
curl -X POST http://localhost:5000/api/wallets \
-H "Content-Type: application/json" \
-d '{
"title": "Integration Test Wallet",
"keyType": "ED25519"
}'Response:
{
"id": "wallet-abc123",
"walletAddress": "0x1234567890abcdef",
"title": "Integration Test Wallet",
"keyType": "ED25519"
}Save the id - you'll need it for subsequent operations.
Step 3: Create a Register
curl -X POST http://localhost:5000/api/registers \
-H "Content-Type: application/json" \
-d '{
"title": "Integration Test Register"
}'Response:
{
"id": "register-xyz789",
"title": "Integration Test Register"
}Step 4: Create and Publish a Blueprint
# Create blueprint
curl -X POST http://localhost:5000/api/blueprints \
-H "Content-Type: application/json" \
-d '{
"title": "Hello World Workflow",
"participants": [
{ "id": "sender", "name": "Message Sender" },
{ "id": "receiver", "name": "Message Receiver" }
],
"actions": [
{
"id": "0",
"title": "Send Message",
"sender": "sender"
}
]
}'
# Publish it
curl -X POST http://localhost:5000/api/blueprints/{blueprint-id}/publishStep 5: Submit Your First Action
curl -X POST http://localhost:5000/api/actions \
-H "Content-Type: application/json" \
-d '{
"blueprintId": "{blueprint-id}",
"actionId": "0",
"senderWallet": "wallet-abc123",
"registerAddress": "register-xyz789",
"payloadData": {
"message": "Hello, Sorcha!"
}
}'Congratulations! You've successfully submitted your first action. 🎉
Blueprint-Based Workflows
Understanding Blueprints
Blueprints define the structure and rules of your workflows:
Blueprint
├── Participants (who's involved)
├── Actions (what can happen)
│ ├── Data Schema (what data is required)
│ ├── Disclosures (who sees what)
│ ├── Calculations (computed fields)
│ └── Routing (what happens next)
└── MetadataCreating Complex Blueprints
Example: Multi-Step Approval Workflow
{
"title": "Invoice Approval Workflow",
"description": "Three-tier invoice approval process",
"participants": [
{ "id": "submitter", "name": "Invoice Submitter" },
{ "id": "manager", "name": "Department Manager" },
{ "id": "director", "name": "Finance Director" },
{ "id": "finance", "name": "Finance Department" }
],
"actions": [
{
"id": "0",
"title": "Submit Invoice",
"sender": "submitter",
"data": {
"type": "object",
"properties": {
"invoiceNumber": { "type": "string" },
"amount": { "type": "number" },
"vendor": { "type": "string" }
},
"required": ["invoiceNumber", "amount", "vendor"]
},
"routing": {
"next": "manager"
}
},
{
"id": "1",
"title": "Manager Approval",
"sender": "manager",
"condition": {
"if": { ">": [{ "var": "amount" }, 10000] },
"then": { "route": "director" },
"else": { "route": "finance" }
}
},
{
"id": "2",
"title": "Director Approval",
"sender": "director",
"routing": {
"next": "finance"
}
},
{
"id": "3",
"title": "Finance Processing",
"sender": "finance"
}
]
}Using the Fluent API (C#)
For .NET applications, use the Fluent API for type-safe blueprint creation:
using Sorcha.Blueprint.Fluent;
var blueprint = BlueprintBuilder.Create()
.WithTitle("Invoice Approval Workflow")
.WithDescription("Three-tier invoice approval process")
.AddParticipant("submitter", p => p.Named("Invoice Submitter"))
.AddParticipant("manager", p => p.Named("Department Manager"))
.AddParticipant("director", p => p.Named("Finance Director"))
.AddParticipant("finance", p => p.Named("Finance Department"))
.AddAction(0, a => a
.WithTitle("Submit Invoice")
.SentBy("submitter")
.RequiresData(d => d
.AddString("invoiceNumber", f => f.IsRequired())
.AddNumber("amount", f => f.IsRequired())
.AddString("vendor", f => f.IsRequired()))
.RouteConditionally(r => r
.When(w => w.GreaterThan("amount", 10000))
.ThenRoute("director")
.ElseRoute("manager")))
.Build();Wallet Integration
Cryptographic Operations
1. Signing Transactions
async function signTransaction(walletId, data) {
const response = await fetch(`http://localhost:5000/api/wallets/${walletId}/sign`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
data: btoa(data), // Base64 encode
algorithm: 'ED25519'
})
});
const result = await response.json();
return result.signature;
}2. Encrypting Sensitive Data
async function encryptPayload(walletId, data, recipientWalletId) {
const response = await fetch(`http://localhost:5000/api/wallets/${walletId}/encrypt`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
data: JSON.stringify(data),
recipientWalletId: recipientWalletId
})
});
const result = await response.json();
return result.encryptedData;
}3. Decrypting Payloads
async function decryptPayload(walletId, encryptedData) {
const response = await fetch(`http://localhost:5000/api/wallets/${walletId}/decrypt`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ encryptedData })
});
const result = await response.json();
return JSON.parse(result.data);
}Key Management Best Practices
Never expose private keys - they never leave the wallet service
Use appropriate algorithms:
- ED25519: Fast, small signatures, recommended for most use cases
- NISTP256: FIPS-compliant, good for regulated industries
- RSA: Large signatures, good for legacy compatibility
Implement key rotation - regularly rotate encryption keys
Use delegation - for temporary access, use wallet delegation instead of sharing keys
Register Integration
Submitting Transactions
Basic Transaction Submission
import requests
import base64
import json
def submit_transaction(register_id, wallet_address, payload_data):
payload_json = json.dumps(payload_data)
payload_base64 = base64.b64encode(payload_json.encode()).decode()
response = requests.post(
f"http://localhost:5000/api/registers/{register_id}/transactions",
json={
"transactionType": "Action",
"senderAddress": wallet_address,
"payload": payload_base64,
"metadata": {
"blueprintId": "bp-123",
"actionId": "0"
}
}
)
return response.json()Querying Transactions
Time-Based Queries
async function getTransactionsByTimeRange(registerId, startTime, endTime) {
const url = new URL(`http://localhost:5000/api/registers/${registerId}/transactions`);
url.searchParams.set('startTime', startTime.toISOString());
url.searchParams.set('endTime', endTime.toISOString());
const response = await fetch(url);
return await response.json();
}
// Example usage
const transactions = await getTransactionsByTimeRange(
'register-xyz789',
new Date('2025-11-17T00:00:00Z'),
new Date('2025-11-17T23:59:59Z')
);OData Queries
// Filter by sender and transaction type
const url = `http://localhost:5000/api/registers/${registerId}/transactions?$filter=senderAddress eq 'wallet-abc123' and transactionType eq 'Action'&$top=10`;
const response = await fetch(url);
const results = await response.json();Chain Validation
async function validateChain(registerId) {
const response = await fetch(`http://localhost:5000/api/registers/${registerId}/chain`);
const validation = await response.json();
if (!validation.isValid) {
console.error('Chain integrity compromised!');
console.error('Errors:', validation.errors);
}
return validation.isValid;
}Real-time Notifications
SignalR Integration (JavaScript)
Complete Example
import * as signalR from '@microsoft/signalr';
class SorchaNotificationClient {
constructor(hubUrl) {
this.connection = new signalR.HubConnectionBuilder()
.withUrl(hubUrl)
.withAutomaticReconnect({
nextRetryDelayInMilliseconds: (retryContext) => {
// Exponential backoff
return Math.min(1000 * Math.pow(2, retryContext.previousRetryCount), 30000);
}
})
.configureLogging(signalR.LogLevel.Information)
.build();
this.setupHandlers();
}
setupHandlers() {
this.connection.onreconnecting((error) => {
console.log('Connection lost. Reconnecting...', error);
});
this.connection.onreconnected((connectionId) => {
console.log('Reconnected with ID:', connectionId);
});
this.connection.onclose((error) => {
console.log('Connection closed:', error);
});
}
async start() {
try {
await this.connection.start();
console.log('SignalR Connected');
} catch (err) {
console.error('SignalR Connection Error:', err);
setTimeout(() => this.start(), 5000);
}
}
async subscribeToActions(walletAddress, registerAddress) {
await this.connection.invoke('SubscribeToActions', walletAddress, registerAddress);
}
async unsubscribeFromActions(walletAddress, registerAddress) {
await this.connection.invoke('UnsubscribeFromActions', walletAddress, registerAddress);
}
onActionConfirmed(callback) {
this.connection.on('ActionConfirmed', callback);
}
onActionPending(callback) {
this.connection.on('ActionPending', callback);
}
async stop() {
await this.connection.stop();
}
}
// Usage
const client = new SorchaNotificationClient('http://localhost:5000/actionshub');
client.onActionConfirmed((notification) => {
console.log('Action confirmed:', notification);
updateUIWithConfirmation(notification);
});
client.onActionPending((notification) => {
console.log('Action pending:', notification);
showPendingIndicator(notification);
});
await client.start();
await client.subscribeToActions('wallet-abc123', 'register-xyz789');SignalR Integration (C#)
using Microsoft.AspNetCore.SignalR.Client;
public class SorchaNotificationClient : IAsyncDisposable
{
private readonly HubConnection _connection;
public SorchaNotificationClient(string hubUrl)
{
_connection = new HubConnectionBuilder()
.WithUrl(hubUrl)
.WithAutomaticReconnect()
.Build();
SetupHandlers();
}
private void SetupHandlers()
{
_connection.On<ActionNotification>("ActionConfirmed", notification =>
{
Console.WriteLine($"Action confirmed: {notification.TransactionHash}");
OnActionConfirmed?.Invoke(notification);
});
_connection.Reconnecting += error =>
{
Console.WriteLine($"Connection lost. Reconnecting... {error}");
return Task.CompletedTask;
};
}
public event Action<ActionNotification>? OnActionConfirmed;
public async Task StartAsync()
{
await _connection.StartAsync();
}
public async Task SubscribeToActionsAsync(string walletAddress, string registerAddress)
{
await _connection.InvokeAsync("SubscribeToActions", walletAddress, registerAddress);
}
public async ValueTask DisposeAsync()
{
await _connection.DisposeAsync();
}
}Best Practices
1. Error Handling
async function robustApiCall(url, options) {
const maxRetries = 3;
const retryDelay = 1000; // 1 second
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (!response.ok) {
if (response.status >= 500) {
// Server error - retry
if (attempt < maxRetries) {
await new Promise(resolve => setTimeout(resolve, retryDelay * attempt));
continue;
}
}
const error = await response.json();
throw new Error(`API Error: ${error.error || response.statusText}`);
}
return await response.json();
} catch (error) {
if (attempt === maxRetries) {
throw error;
}
await new Promise(resolve => setTimeout(resolve, retryDelay * attempt));
}
}
}2. Connection Pooling (C#)
// Use a single HttpClient instance (singleton)
public class SorchaApiClient
{
private static readonly HttpClient _httpClient = new HttpClient
{
BaseAddress = new Uri("http://localhost:5000"),
Timeout = TimeSpan.FromSeconds(30)
};
public static HttpClient Client => _httpClient;
}3. Caching
class BlueprintCache {
constructor(ttl = 300000) { // 5 minutes
this.cache = new Map();
this.ttl = ttl;
}
async get(blueprintId) {
const cached = this.cache.get(blueprintId);
if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.data;
}
const response = await fetch(`http://localhost:5000/api/blueprints/${blueprintId}`);
const blueprint = await response.json();
this.cache.set(blueprintId, {
data: blueprint,
timestamp: Date.now()
});
return blueprint;
}
}4. Idempotency
// Use instance IDs for idempotent action submission
async function submitActionIdempotent(actionData) {
// Generate a deterministic instance ID
const instanceId = generateInstanceId(actionData);
const response = await fetch('http://localhost:5000/api/actions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...actionData,
instanceId // Ensures resubmissions are idempotent
})
});
return await response.json();
}Troubleshooting
Common Issues
1. "Blueprint not found" Error
Cause: Blueprint hasn't been published or ID is incorrect
Solution:
# List all blueprints
curl http://localhost:5000/api/blueprints
# Publish blueprint
curl -X POST http://localhost:5000/api/blueprints/{id}/publish2. Connection Refused
Cause: Services not running
Solution:
# Start services
dotnet run --project src/Apps/Sorcha.AppHost
# Check health
curl http://localhost:5000/api/health3. SignalR Connection Drops
Cause: Network instability or server restart
Solution: Use automatic reconnection (shown in examples above)
Advanced Topics
Custom Integrations
Webhook Integration (Future)
// Register webhook for transaction confirmations
await fetch('http://localhost:5000/api/webhooks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: 'https://your-app.com/webhooks/sorcha',
events: ['transaction.confirmed', 'action.pending'],
secret: 'your-webhook-secret'
})
});Monitoring and Observability
# Health check with metrics
curl http://localhost:5000/api/health
# Service-specific health
curl http://localhost:5000/api/blueprint/health
curl http://localhost:5000/api/wallet/health
curl http://localhost:5000/api/register/healthNext Steps
- Explore the API: Visit http://localhost:5000/scalar/v1
- Read API Documentation: See API-DOCUMENTATION.md
- Review Examples: Check
/examplesdirectory - Join Community: GitHub Discussions
Need Help?
- GitHub Issues: https://github.com/yourusername/Sorcha/issues
- Documentation: https://docs.sorcha.io
- Email: support@sorcha.io
Last Updated: 2025-11-17 Document Version: 1.0.0 Sprint: 7