EDC Connector API
Concepts

Transfer Process

Understanding the data transfer workflow in the EDC Connector

Transfer Process

The Transfer Process is the mechanism by which data is exchanged between connectors after a Contract Agreement has been established. It handles the complete lifecycle of a data transfer from initiation to completion.

Overview

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Contract      │    │    Transfer     │    │     Data        │
│   Agreement     │ ─▶ │    Process      │ ─▶ │    Received     │
│   (Required)    │    │    (States)     │    │    (Complete)   │
└─────────────────┘    └─────────────────┘    └─────────────────┘

The sovity EDC UI API provides simplified endpoints for managing transfers, with clear state tracking and transfer history.

Transfer States

The sovity UI API uses a simplified state model (TransferProcessSimplifiedState) that maps complex EDC states to three categories:

Simplified StateDescriptionUnderlying States
RUNNINGTransfer in progressINITIAL, PROVISIONING, PROVISIONED, REQUESTING, REQUESTED, STARTING, STARTED
OKTransfer completed successfullyCOMPLETED, DEPROVISIONED
ERRORTransfer failed or terminatedTERMINATED, any error state

Detailed Transfer States

For debugging, the full EDC state is also available:

StateDescription
INITIALTransfer process created
PROVISIONINGResources being provisioned
PROVISIONEDResources ready
REQUESTINGRequesting data from provider
REQUESTEDRequest sent to provider
STARTINGTransfer starting
STARTEDData transfer in progress
COMPLETINGTransfer finalizing
COMPLETEDTransfer successful
TERMINATINGTransfer being terminated
TERMINATEDTransfer ended
DEPROVISIONINGCleaning up resources
DEPROVISIONEDResources cleaned up

Transfer Types

The EDC supports two transfer patterns:

PULL Transfer (Consumer-Initiated)

The consumer pulls data from the provider using an Endpoint Data Reference (EDR):

{
  "transferType": "HttpData-PULL",
  "dataSinkProperties": {
    "type": "HttpProxy"
  }
}

PUSH Transfer (Provider-Initiated)

The provider pushes data to the consumer's specified destination:

{
  "transferType": "HttpData-PUSH",
  "dataSinkProperties": {
    "type": "HttpData",
    "baseUrl": "https://consumer-storage.example.com/incoming"
  }
}

Initiating a Transfer

After obtaining a Contract Agreement, you can initiate a data transfer using the UI API.

InitiateTransferRequest Schema

interface InitiateTransferRequest {
  contractAgreementId: string;       // Required: The contract agreement ID
  transferType: string;              // Required: e.g., "HttpData-PULL" or "HttpData-PUSH"
  dataSinkProperties: {              // Required: Data destination properties
    [key: string]: string;
  };
  transferProcessProperties?: {      // Optional: Additional transfer properties
    [key: string]: string;
  };
}

Standard Transfer

curl -X POST "https://api.your-connector-instance.prod.truzztbox.eu/api/management/wrapper/ui/pages/contract-agreement-page/transfers" \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: your-api-key" \
  -d '{
    "contractAgreementId": "contract-agreement-abc123",
    "transferType": "HttpData-PULL",
    "dataSinkProperties": {
      "type": "HttpProxy"
    },
    "transferProcessProperties": {}
  }'
const BASE_URL = 'https://api.your-connector-instance.prod.truzztbox.eu/api/management';

interface InitiateTransferRequest {
  contractAgreementId: string;
  transferType: string;
  dataSinkProperties: Record<string, string>;
  transferProcessProperties: Record<string, string>;
}

async function initiateTransfer(
  contractAgreementId: string,
  transferType: 'HttpData-PULL' | 'HttpData-PUSH' = 'HttpData-PULL',
  dataSinkProperties: Record<string, string> = { type: 'HttpProxy' }
): Promise<{ id: string; lastUpdatedDate: string }> {
  const request: InitiateTransferRequest = {
    contractAgreementId,
    transferType,
    dataSinkProperties,
    transferProcessProperties: {},
  };

  const response = await fetch(
    `${BASE_URL}/wrapper/ui/pages/contract-agreement-page/transfers`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Api-Key': 'your-api-key',
      },
      body: JSON.stringify(request),
    }
  );

  if (!response.ok) {
    throw new Error(`Failed to initiate transfer: ${response.statusText}`);
  }

  return response.json();
}

// Usage: PULL transfer
const pullTransfer = await initiateTransfer('contract-abc123', 'HttpData-PULL', {
  type: 'HttpProxy',
});
console.log('Transfer initiated:', pullTransfer.id);

// Usage: PUSH transfer
const pushTransfer = await initiateTransfer('contract-abc123', 'HttpData-PUSH', {
  type: 'HttpData',
  baseUrl: 'https://my-storage.example.com/incoming',
});

Custom Transfer

For advanced scenarios, use the custom transfer endpoint which accepts raw JSON-LD:

curl -X POST "https://api.your-connector-instance.prod.truzztbox.eu/api/management/wrapper/ui/pages/contract-agreement-page/transfers/custom" \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: your-api-key" \
  -d '{
    "contractAgreementId": "contract-agreement-abc123",
    "transferProcessRequestJsonLd": "{\"@context\": {\"edc\": \"https://w3id.org/edc/v0.0.1/ns/\"}, \"@type\": \"TransferRequest\", \"protocol\": \"dataspace-protocol-http\", \"transferType\": \"HttpData-PULL\", \"dataDestination\": {\"@type\": \"DataAddress\", \"type\": \"HttpProxy\"}}"
  }'
interface InitiateCustomTransferRequest {
  contractAgreementId: string;
  transferProcessRequestJsonLd: string;
}

async function initiateCustomTransfer(
  contractAgreementId: string,
  customProperties: object
): Promise<{ id: string; lastUpdatedDate: string }> {
  const transferRequestJsonLd = {
    '@context': { edc: 'https://w3id.org/edc/v0.0.1/ns/' },
    '@type': 'TransferRequest',
    protocol: 'dataspace-protocol-http',
    transferType: 'HttpData-PULL',
    dataDestination: {
      '@type': 'DataAddress',
      type: 'HttpProxy',
    },
    ...customProperties,
  };

  const request: InitiateCustomTransferRequest = {
    contractAgreementId,
    transferProcessRequestJsonLd: JSON.stringify(transferRequestJsonLd),
  };

  const response = await fetch(
    `${BASE_URL}/wrapper/ui/pages/contract-agreement-page/transfers/custom`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Api-Key': 'your-api-key',
      },
      body: JSON.stringify(request),
    }
  );

  if (!response.ok) {
    throw new Error(`Failed to initiate custom transfer: ${response.statusText}`);
  }

  return response.json();
}

// Usage with custom callbackAddresses
const customTransfer = await initiateCustomTransfer('contract-abc123', {
  callbackAddresses: [
    {
      uri: 'https://my-app.example.com/transfer-callback',
      events: ['transfer.process.started', 'transfer.process.completed'],
    },
  ],
});

The custom transfer endpoint automatically fills in participantId, connectorEndpoint, assetId, and contractId from the contract agreement. You only need to provide additional customizations.

Transfer History

The Transfer History Page endpoint provides a complete view of all transfers:

curl -X GET "https://api.your-connector-instance.prod.truzztbox.eu/api/management/wrapper/ui/pages/transfer-history-page" \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: your-api-key"
interface TransferHistoryEntry {
  transferProcessId: string;
  createdDate: string;
  lastUpdatedDate: string;
  state: {
    name: string;
    code: number;
    simplifiedState: 'RUNNING' | 'OK' | 'ERROR';
  };
  contractAgreementId: string;
  direction: 'CONSUMING' | 'PROVIDING';
  counterPartyConnectorEndpoint: string;
  counterPartyParticipantId: string;
  assetName: string;
  assetId: string;
  errorMessage?: string;
}

interface TransferHistoryPage {
  transferEntries: TransferHistoryEntry[];
}

async function getTransferHistory(): Promise<TransferHistoryPage> {
  const response = await fetch(
    `${BASE_URL}/wrapper/ui/pages/transfer-history-page`,
    {
      headers: {
        'Content-Type': 'application/json',
        'X-Api-Key': 'your-api-key',
      },
    }
  );

  if (!response.ok) {
    throw new Error(`Failed to get transfer history: ${response.statusText}`);
  }

  return response.json();
}

// Get all transfers
const history = await getTransferHistory();

// Filter by state
const runningTransfers = history.transferEntries.filter(
  (t) => t.state.simplifiedState === 'RUNNING'
);
const failedTransfers = history.transferEntries.filter(
  (t) => t.state.simplifiedState === 'ERROR'
);

// Filter by direction
const consumingTransfers = history.transferEntries.filter(
  (t) => t.direction === 'CONSUMING'
);
const providingTransfers = history.transferEntries.filter(
  (t) => t.direction === 'PROVIDING'
);

console.log(`Total: ${history.transferEntries.length}`);
console.log(`Running: ${runningTransfers.length}`);
console.log(`Failed: ${failedTransfers.length}`);
console.log(`Consuming: ${consumingTransfers.length}`);
console.log(`Providing: ${providingTransfers.length}`);

Get Transfer Asset Details

Retrieve detailed asset information for a specific transfer:

curl -X GET "https://api.your-connector-instance.prod.truzztbox.eu/api/management/wrapper/ui/pages/transfer-history-page/transfer-processes/{transferProcessId}/asset" \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: your-api-key"
async function getTransferAsset(transferProcessId: string): Promise<UiAsset> {
  const response = await fetch(
    `${BASE_URL}/wrapper/ui/pages/transfer-history-page/transfer-processes/${transferProcessId}/asset`,
    {
      headers: {
        'Content-Type': 'application/json',
        'X-Api-Key': 'your-api-key',
      },
    }
  );

  if (!response.ok) {
    throw new Error(`Failed to get transfer asset: ${response.statusText}`);
  }

  return response.json();
}

const asset = await getTransferAsset('transfer-process-123');
console.log('Asset title:', asset.title);
console.log('Data source availability:', asset.dataSourceAvailability);

Contract Agreement Transfers

Transfers are also accessible through the Contract Agreement page, which shows transfers grouped by contract:

interface ContractAgreementTransferProcess {
  transferProcessId: string;
  lastUpdatedDate: string;
  state: {
    name: string;
    code: number;
    simplifiedState: 'RUNNING' | 'OK' | 'ERROR';
  };
  errorMessage?: string;
}

interface ContractAgreementCard {
  contractAgreementId: string;
  contractNegotiationId: string;
  direction: 'CONSUMING' | 'PROVIDING';
  counterPartyAddress: string;
  counterPartyId: string;
  contractSigningDate: string;
  asset: UiAsset;
  contractPolicy: UiPolicy;
  transferProcesses: ContractAgreementTransferProcess[];
  terminationStatus: 'ONGOING' | 'TERMINATED';
}

// Get contract agreement with its transfers
async function getContractAgreementCard(
  contractAgreementId: string
): Promise<ContractAgreementCard> {
  const response = await fetch(
    `${BASE_URL}/wrapper/ui/pages/contract-agreement-page/${contractAgreementId}`,
    {
      headers: {
        'Content-Type': 'application/json',
        'X-Api-Key': 'your-api-key',
      },
    }
  );

  if (!response.ok) {
    throw new Error(`Failed to get contract agreement: ${response.statusText}`);
  }

  return response.json();
}

const agreement = await getContractAgreementCard('contract-abc123');
console.log('Contract transfers:', agreement.transferProcesses.length);
agreement.transferProcesses.forEach((transfer) => {
  console.log(`  ${transfer.transferProcessId}: ${transfer.state.simplifiedState}`);
});

Complete Transfer Workflow

Get Contract Agreement

After a successful negotiation, retrieve the contract agreement ID.

const agreementPage = await fetch(
  `${BASE_URL}/wrapper/ui/pages/contract-agreement-page`,
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Api-Key': 'your-api-key',
    },
    body: JSON.stringify({ terminationStatus: 'ONGOING' }),
  }
).then((r) => r.json());

const agreement = agreementPage.contractAgreements[0];
console.log('Contract Agreement ID:', agreement.contractAgreementId);

Initiate Transfer

Start a PULL or PUSH transfer based on your use case.

const transferResult = await initiateTransfer(
  agreement.contractAgreementId,
  'HttpData-PULL',
  { type: 'HttpProxy' }
);
console.log('Transfer Process ID:', transferResult.id);

Monitor Transfer Progress

Poll the transfer history until completion.

async function waitForTransfer(
  transferProcessId: string,
  timeoutMs: number = 60000
): Promise<TransferHistoryEntry> {
  const startTime = Date.now();

  while (Date.now() - startTime < timeoutMs) {
    const history = await getTransferHistory();
    const transfer = history.transferEntries.find(
      (t) => t.transferProcessId === transferProcessId
    );

    if (!transfer) {
      throw new Error(`Transfer ${transferProcessId} not found`);
    }

    if (transfer.state.simplifiedState === 'OK') {
      return transfer;
    }

    if (transfer.state.simplifiedState === 'ERROR') {
      throw new Error(`Transfer failed: ${transfer.errorMessage}`);
    }

    // Wait before polling again
    await new Promise((resolve) => setTimeout(resolve, 2000));
  }

  throw new Error('Transfer timeout');
}

const completedTransfer = await waitForTransfer(transferResult.id);
console.log('Transfer completed at:', completedTransfer.lastUpdatedDate);

Access Data (PULL transfers)

For PULL transfers, use the EDR to access the data.

// For PULL transfers, fetch the data using the EDR endpoint
// The EDR provides authentication to access the provider's data plane
const dataResponse = await fetch(edrEndpoint, {
  headers: {
    Authorization: edrAuthToken,
  },
});
const data = await dataResponse.json();

Dashboard Transfer Statistics

The Dashboard API provides aggregate transfer statistics:

interface DashboardTransferAmounts {
  numTotal: number;
  numRunning: number;
  numOk: number;
  numError: number;
}

interface DashboardPage {
  transferProcessesConsuming: DashboardTransferAmounts;
  transferProcessesProviding: DashboardTransferAmounts;
  // ... other dashboard fields
}

async function getDashboard(): Promise<DashboardPage> {
  const response = await fetch(`${BASE_URL}/wrapper/ui/pages/dashboard-page`, {
    headers: {
      'Content-Type': 'application/json',
      'X-Api-Key': 'your-api-key',
    },
  });
  return response.json();
}

const dashboard = await getDashboard();

console.log('Consuming transfers:');
console.log(`  Total: ${dashboard.transferProcessesConsuming.numTotal}`);
console.log(`  Running: ${dashboard.transferProcessesConsuming.numRunning}`);
console.log(`  OK: ${dashboard.transferProcessesConsuming.numOk}`);
console.log(`  Error: ${dashboard.transferProcessesConsuming.numError}`);

console.log('Providing transfers:');
console.log(`  Total: ${dashboard.transferProcessesProviding.numTotal}`);
console.log(`  Running: ${dashboard.transferProcessesProviding.numRunning}`);
console.log(`  OK: ${dashboard.transferProcessesProviding.numOk}`);
console.log(`  Error: ${dashboard.transferProcessesProviding.numError}`);

Error Handling

interface TransferError {
  transferProcessId: string;
  errorMessage: string;
  state: string;
}

async function handleTransferErrors(): Promise<TransferError[]> {
  const history = await getTransferHistory();

  const errors = history.transferEntries
    .filter((t) => t.state.simplifiedState === 'ERROR')
    .map((t) => ({
      transferProcessId: t.transferProcessId,
      errorMessage: t.errorMessage || 'Unknown error',
      state: t.state.name,
    }));

  return errors;
}

const errors = await handleTransferErrors();
errors.forEach((error) => {
  console.error(`Transfer ${error.transferProcessId} failed:`);
  console.error(`  State: ${error.state}`);
  console.error(`  Message: ${error.errorMessage}`);
});

Best Practices

  1. Use Simplified States: The simplifiedState field provides a clear success/failure/running indicator for UI displays
  2. Monitor Transfer Progress: Poll the transfer history endpoint to track state changes
  3. Handle Errors Gracefully: Check errorMessage for failed transfers to understand the cause
  4. Track by Direction: Use the direction field to separate consuming vs providing transfers
  5. Use Custom Transfers Sparingly: Only use the custom transfer endpoint when standard options are insufficient
  6. Keep Transfer Records: Store transferProcessId values for audit and debugging purposes

Next Steps

On this page