EDC Connector API
Concepts

Catalog

Discovering Data Offers in the sovity EDC Connector

Catalog

The Catalog enables data consumers to discover available data offers from other connectors in the data space. Each connector exposes its published assets through the catalog, allowing participants to browse, evaluate, and negotiate for data access.

How the Catalog Works

┌─────────────────────────────────────────────────────────────────┐
│                       Data Space                                 │
│                                                                  │
│  ┌───────────────┐    ┌───────────────┐    ┌───────────────┐   │
│  │  Provider A   │    │  Provider B   │    │  Provider C   │   │
│  │ ┌───────────┐ │    │ ┌───────────┐ │    │ ┌───────────┐ │   │
│  │ │  Assets   │ │    │ │  Assets   │ │    │ │  Assets   │ │   │
│  │ │  + Offers │ │    │ │  + Offers │ │    │ │  + Offers │ │   │
│  │ └───────────┘ │    │ └───────────┘ │    │ └───────────┘ │   │
│  └───────┬───────┘    └───────┬───────┘    └───────┬───────┘   │
│          │                    │                    │            │
│          └────────────────────┼────────────────────┘            │
│                               │                                  │
│                               ▼                                  │
│                    ┌───────────────────┐                        │
│                    │   Your Connector  │                        │
│                    │    (Consumer)     │                        │
│                    └───────────────────┘                        │
└─────────────────────────────────────────────────────────────────┘

When you query another connector's catalog, you receive their published data offers filtered by access policies that you satisfy.

Fetching Data Offers

Endpoint: GET /wrapper/ui/pages/catalog-page/data-offers

Query Parameters

ParameterRequiredDescription
connectorEndpointYesThe DSP endpoint URL of the target connector
participantIdNoThe participant ID of the target connector

If the connector endpoint URL includes a participantId query parameter, you don't need to provide it separately.

cURL Example

curl -X GET "https://api.your-connector-instance.prod.truzztbox.eu/api/management/wrapper/ui/pages/catalog-page/data-offers?connectorEndpoint=https://provider.example.com/api/dsp&participantId=provider-id" \
  -H "X-Api-Key: your-api-key"

TypeScript Example

interface UiDataOffer {
  endpoint: string;
  participantId: string;
  asset: UiAsset;
  contractOffers: UiContractOffer[];
}

async function fetchDataOffers(
  providerEndpoint: string,
  participantId?: string
): Promise<UiDataOffer[]> {
  const params = new URLSearchParams({
    connectorEndpoint: providerEndpoint,
  });
  
  if (participantId) {
    params.set('participantId', participantId);
  }
  
  const response = await fetch(
    `https://api.your-connector-instance.prod.truzztbox.eu/api/management/wrapper/ui/pages/catalog-page/data-offers?${params}`,
    {
      headers: { 'X-Api-Key': 'your-api-key' },
    }
  );
  
  if (!response.ok) {
    throw new Error(`Failed to fetch catalog: ${response.statusText}`);
  }
  
  return response.json();
}

// Usage
const offers = await fetchDataOffers(
  'https://museum-connector.example.com/api/dsp',
  'museum-participant-id'
);

console.log(`Found ${offers.length} data offers`);

Understanding Data Offers

Each UiDataOffer contains complete information about an available asset and its contract terms.

UiDataOffer Structure

interface UiDataOffer {
  // Connection details
  endpoint: string;        // Provider's DSP endpoint
  participantId: string;   // Provider's participant ID
  
  // Asset information
  asset: UiAsset;
  
  // Available contract terms
  contractOffers: UiContractOffer[];
}

Asset Information (UiAsset)

interface UiAsset {
  assetId: string;
  title: string;
  description?: string;
  descriptionShortText?: string;
  
  // Availability
  dataSourceAvailability: 'LIVE' | 'ON_REQUEST';
  
  // For ON_REQUEST assets
  onRequestContactEmail?: string;
  onRequestContactEmailSubject?: string;
  
  // Parameterization hints (for LIVE assets)
  httpDatasourceHintsProxyMethod?: boolean;
  httpDatasourceHintsProxyPath?: boolean;
  httpDatasourceHintsProxyQueryParams?: boolean;
  httpDatasourceHintsProxyBody?: boolean;
  
  // Metadata
  creatorOrganizationName: string;
  language?: string;
  version?: string;
  keywords?: string[];
  mediaType?: string;
  dataCategory?: string;
  dataSubcategory?: string;
  
  // Additional fields...
}

Data Source Availability

ValueDescription
LIVEAsset has an automated data source (HTTP API, etc.)
ON_REQUESTAsset requires manual contact to obtain data

For ON_REQUEST assets, use the contact email to request access:

if (offer.asset.dataSourceAvailability === 'ON_REQUEST') {
  console.log('Contact:', offer.asset.onRequestContactEmail);
  console.log('Subject:', offer.asset.onRequestContactEmailSubject);
}

Contract Offers (UiContractOffer)

Each asset can have one or more contract offers with different terms:

interface UiContractOffer {
  contractOfferId: string;
  policy: UiPolicy;
}

interface UiPolicy {
  policyJsonLd: string;        // Full policy in JSON-LD (needed for negotiation)
  expression: UiPolicyExpression;  // Parsed policy expression
  errors: string[];            // Any parsing errors
}

The expression field provides a structured view of the policy:

interface UiPolicyExpression {
  type: 'EMPTY' | 'CONSTRAINT' | 'AND' | 'OR' | 'XONE';
  constraint?: UiPolicyConstraint;    // For type CONSTRAINT
  expressions?: UiPolicyExpression[]; // For AND, OR, XONE
}

interface UiPolicyConstraint {
  left: string;
  operator: 'EQ' | 'NEQ' | 'GT' | 'GEQ' | 'LT' | 'LEQ' | 'IN' | 'HAS_PART' | 'IS_A' | 'IS_ALL_OF' | 'IS_ANY_OF' | 'IS_NONE_OF';
  right: UiPolicyLiteral;
}

Processing Catalog Results

Display Available Offers

function displayOffers(offers: UiDataOffer[]): void {
  for (const offer of offers) {
    console.log(`\n📦 ${offer.asset.title}`);
    console.log(`   ID: ${offer.asset.assetId}`);
    console.log(`   Provider: ${offer.asset.creatorOrganizationName}`);
    console.log(`   Type: ${offer.asset.dataSourceAvailability}`);
    
    if (offer.asset.description) {
      console.log(`   ${offer.asset.descriptionShortText}`);
    }
    
    console.log(`   Contract Offers: ${offer.contractOffers.length}`);
    
    for (const contractOffer of offer.contractOffers) {
      const policyType = contractOffer.policy.expression?.type || 'UNKNOWN';
      console.log(`   - ${contractOffer.contractOfferId} (${policyType})`);
    }
  }
}

Filter Offers

// Filter by category
const culturalOffers = offers.filter(
  o => o.asset.dataCategory === 'Cultural Heritage'
);

// Filter by availability
const liveOffers = offers.filter(
  o => o.asset.dataSourceAvailability === 'LIVE'
);

// Filter by keywords
const museumOffers = offers.filter(
  o => o.asset.keywords?.includes('museum')
);

// Filter by unrestricted policy
const unrestrictedOffers = offers.filter(
  o => o.contractOffers.some(
    co => co.policy.expression?.type === 'EMPTY'
  )
);

Evaluate Policy Constraints

function canSatisfyPolicy(expression: UiPolicyExpression): boolean {
  switch (expression.type) {
    case 'EMPTY':
      return true; // No constraints
      
    case 'CONSTRAINT':
      // Check if you can satisfy this constraint
      return evaluateConstraint(expression.constraint!);
      
    case 'AND':
      // Must satisfy all sub-expressions
      return expression.expressions!.every(canSatisfyPolicy);
      
    case 'OR':
      // Must satisfy at least one
      return expression.expressions!.some(canSatisfyPolicy);
      
    case 'XONE':
      // Must satisfy exactly one
      const satisfied = expression.expressions!.filter(canSatisfyPolicy);
      return satisfied.length === 1;
      
    default:
      return false;
  }
}

function evaluateConstraint(constraint: UiPolicyConstraint): boolean {
  // Implement your constraint evaluation logic
  // This depends on your participant's credentials
  console.log(`Checking: ${constraint.left} ${constraint.operator} ${constraint.right}`);
  return true; // Simplified
}

From Catalog to Negotiation

Once you've selected an offer, initiate a contract negotiation:

Complete Workflow

async function discoverAndNegotiate(
  providerEndpoint: string,
  participantId: string,
  assetId: string
): Promise<string> {
  // 1. Fetch data offers
  const offers = await fetchDataOffers(providerEndpoint, participantId);
  
  // 2. Find the desired asset
  const offer = offers.find(o => o.asset.assetId === assetId);
  if (!offer) {
    throw new Error(`Asset ${assetId} not found in catalog`);
  }
  
  // 3. Select a contract offer (usually the first one)
  const contractOffer = offer.contractOffers[0];
  if (!contractOffer) {
    throw new Error('No contract offers available');
  }
  
  // 4. Initiate negotiation
  const negotiationRequest = {
    counterPartyId: offer.participantId,
    counterPartyAddress: offer.endpoint,
    contractOfferId: contractOffer.contractOfferId,
    assetId: offer.asset.assetId,
    policyJsonLd: contractOffer.policy.policyJsonLd,
  };
  
  const response = await fetch(
    'https://api.your-connector-instance.prod.truzztbox.eu/api/management/wrapper/ui/pages/catalog-page/contract-negotiations',
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Api-Key': 'your-api-key',
      },
      body: JSON.stringify(negotiationRequest),
    }
  );
  
  const negotiation = await response.json();
  console.log('Negotiation started:', negotiation.contractNegotiationId);
  
  // 5. Wait for agreement
  return await waitForAgreement(negotiation.contractNegotiationId);
}

async function waitForAgreement(negotiationId: string): Promise<string> {
  const maxAttempts = 60;
  const delayMs = 1000;
  
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    const response = await fetch(
      `https://api.your-connector-instance.prod.truzztbox.eu/api/management/wrapper/ui/pages/catalog-page/contract-negotiations/${negotiationId}`,
      { headers: { 'X-Api-Key': 'your-api-key' } }
    );
    
    const negotiation = await response.json();
    
    switch (negotiation.state.simplifiedState) {
      case 'AGREED':
        console.log('Agreement established!');
        return negotiation.contractAgreementId;
        
      case 'TERMINATED':
        throw new Error(`Negotiation terminated: ${negotiation.state.name}`);
        
      case 'IN_PROGRESS':
        await new Promise(resolve => setTimeout(resolve, delayMs));
        break;
    }
  }
  
  throw new Error('Negotiation timed out');
}

Querying Multiple Providers

To discover offers across the data space, query multiple connectors:

interface ProviderConfig {
  endpoint: string;
  participantId: string;
  name: string;
}

async function discoverAcrossDataSpace(
  providers: ProviderConfig[]
): Promise<Map<string, UiDataOffer[]>> {
  const results = new Map<string, UiDataOffer[]>();
  
  await Promise.all(
    providers.map(async provider => {
      try {
        const offers = await fetchDataOffers(
          provider.endpoint,
          provider.participantId
        );
        results.set(provider.name, offers);
        console.log(`${provider.name}: ${offers.length} offers`);
      } catch (error) {
        console.error(`${provider.name}: Failed to fetch`, error);
        results.set(provider.name, []);
      }
    })
  );
  
  return results;
}

// Usage with preconfigured counterparties
const providers: ProviderConfig[] = [
  {
    endpoint: 'https://museum-a.example.com/api/dsp',
    participantId: 'museum-a',
    name: 'Museum A',
  },
  {
    endpoint: 'https://archive-b.example.com/api/dsp',
    participantId: 'archive-b',
    name: 'Archive B',
  },
];

const allOffers = await discoverAcrossDataSpace(providers);

Error Handling

Common Errors

ErrorCauseSolution
Connection timeoutProvider offlineRetry later or check endpoint
401 UnauthorizedInvalid credentialsVerify API key and participant ID
Empty responseNo published offers or access deniedCheck access policies
Policy errorsCannot parse provider's policiesCheck errors field in UiPolicy

Handling Errors

async function safeFetchOffers(
  endpoint: string,
  participantId: string
): Promise<UiDataOffer[]> {
  try {
    const offers = await fetchDataOffers(endpoint, participantId);
    
    // Check for policy parsing errors
    for (const offer of offers) {
      for (const co of offer.contractOffers) {
        if (co.policy.errors.length > 0) {
          console.warn(
            `Policy parsing warnings for ${offer.asset.assetId}:`,
            co.policy.errors
          );
        }
      }
    }
    
    return offers;
  } catch (error) {
    if (error instanceof TypeError) {
      console.error('Network error - provider may be offline');
    } else {
      console.error('Failed to fetch catalog:', error);
    }
    return [];
  }
}

Caching Strategies

For frequently accessed catalogs, implement caching:

interface CacheEntry {
  offers: UiDataOffer[];
  fetchedAt: number;
}

const cache = new Map<string, CacheEntry>();
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes

async function getCachedOffers(
  endpoint: string,
  participantId: string
): Promise<UiDataOffer[]> {
  const cacheKey = `${endpoint}:${participantId}`;
  const cached = cache.get(cacheKey);
  
  if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
    console.log('Returning cached offers');
    return cached.offers;
  }
  
  const offers = await fetchDataOffers(endpoint, participantId);
  cache.set(cacheKey, { offers, fetchedAt: Date.now() });
  
  return offers;
}

Best Practices

  1. Handle offline providers: Not all connectors are always available
  2. Cache catalog results: Catalogs don't change frequently
  3. Check policy errors: The errors field indicates unsupported policy features
  4. Validate before negotiating: Ensure you can satisfy policy constraints
  5. Use participant IDs: Always include participant ID for reliable addressing
  6. Log catalog queries: Track what offers are available for auditing

Next Steps

On this page