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 State | Description | Underlying States |
|---|---|---|
RUNNING | Transfer in progress | INITIAL, PROVISIONING, PROVISIONED, REQUESTING, REQUESTED, STARTING, STARTED |
OK | Transfer completed successfully | COMPLETED, DEPROVISIONED |
ERROR | Transfer failed or terminated | TERMINATED, any error state |
Detailed Transfer States
For debugging, the full EDC state is also available:
| State | Description |
|---|---|
INITIAL | Transfer process created |
PROVISIONING | Resources being provisioned |
PROVISIONED | Resources ready |
REQUESTING | Requesting data from provider |
REQUESTED | Request sent to provider |
STARTING | Transfer starting |
STARTED | Data transfer in progress |
COMPLETING | Transfer finalizing |
COMPLETED | Transfer successful |
TERMINATING | Transfer being terminated |
TERMINATED | Transfer ended |
DEPROVISIONING | Cleaning up resources |
DEPROVISIONED | Resources 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
- Use Simplified States: The
simplifiedStatefield provides a clear success/failure/running indicator for UI displays - Monitor Transfer Progress: Poll the transfer history endpoint to track state changes
- Handle Errors Gracefully: Check
errorMessagefor failed transfers to understand the cause - Track by Direction: Use the
directionfield to separate consuming vs providing transfers - Use Custom Transfers Sparingly: Only use the custom transfer endpoint when standard options are insufficient
- Keep Transfer Records: Store
transferProcessIdvalues for audit and debugging purposes