Contracts
Understanding Contract Definitions, Negotiations, and Agreements in the sovity EDC
Contracts
Contracts are the foundation of data exchange in the EDC. They define the terms under which data can be accessed and used. The contract system consists of three key components: Contract Definitions, Contract Negotiations, and Contract Agreements.
Contract Lifecycle
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ Contract │ │ Contract │ │ Contract │
│ Definition │ ──▶ │ Negotiation │ ──▶ │ Agreement │
│ (Provider creates)│ │ (Both parties) │ │ (Binding contract)│
└───────────────────┘ └───────────────────┘ └───────────────────┘
│ │ │
│ │ │
▼ ▼ ▼
Makes asset visible Consumer initiates Enables data
in the catalog from catalog offer transferContract Definitions
A Contract Definition links an Asset to policies, making it available in the catalog for consumers to discover.
Structure
| Field | Required | Description |
|---|---|---|
contractDefinitionId | Yes | Unique identifier |
accessPolicyId | Yes | Policy that determines who can see the offer |
contractPolicyId | Yes | Policy that governs data usage after agreement |
assetSelector | Yes | Criteria to select which assets this definition applies to |
Asset Selectors
Asset selectors use UiCriterion objects to filter which assets are included:
interface UiCriterion {
operandLeft: string; // Property to match
operator: 'EQ' | 'IN' | 'LIKE'; // Comparison operator
operandRight: UiCriterionLiteral; // Value(s) to match
}
interface UiCriterionLiteral {
type: 'VALUE' | 'VALUE_LIST';
value?: string; // For single value
valueList?: string[]; // For multiple values (with IN operator)
}Selector Examples
Select a specific asset by ID:
const selector = [{
operandLeft: 'https://w3id.org/edc/v0.0.1/ns/id',
operator: 'EQ',
operandRight: { type: 'VALUE', value: 'museum-collection-2024' }
}];Select multiple assets:
const selector = [{
operandLeft: 'https://w3id.org/edc/v0.0.1/ns/id',
operator: 'IN',
operandRight: {
type: 'VALUE_LIST',
valueList: ['asset-1', 'asset-2', 'asset-3']
}
}];Select by custom property pattern:
const selector = [{
operandLeft: 'dataCategory',
operator: 'LIKE',
operandRight: { type: 'VALUE', value: 'Cultural%' }
}];Creating a Contract Definition
Endpoint: POST /wrapper/ui/pages/contract-definition-page/contract-definitions
cURL Example
curl -X POST "https://api.your-connector-instance.prod.truzztbox.eu/api/management/wrapper/ui/pages/contract-definition-page/contract-definitions" \
-H "Content-Type: application/json" \
-H "X-Api-Key: your-api-key" \
-d '{
"contractDefinitionId": "museum-data-contract",
"accessPolicyId": "public-access-policy",
"contractPolicyId": "research-usage-policy",
"assetSelector": [
{
"operandLeft": "https://w3id.org/edc/v0.0.1/ns/id",
"operator": "EQ",
"operandRight": {
"type": "VALUE",
"value": "museum-collection-2024"
}
}
]
}'TypeScript Example
interface ContractDefinitionRequest {
contractDefinitionId: string;
accessPolicyId: string;
contractPolicyId: string;
assetSelector: UiCriterion[];
}
const contractDefinition: ContractDefinitionRequest = {
contractDefinitionId: 'museum-data-contract',
accessPolicyId: 'public-access-policy',
contractPolicyId: 'research-usage-policy',
assetSelector: [
{
operandLeft: 'https://w3id.org/edc/v0.0.1/ns/id',
operator: 'EQ',
operandRight: { type: 'VALUE', value: 'museum-collection-2024' },
},
],
};
const response = await fetch(
'https://api.your-connector-instance.prod.truzztbox.eu/api/management/wrapper/ui/pages/contract-definition-page/contract-definitions',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': 'your-api-key',
},
body: JSON.stringify(contractDefinition),
}
);
const result = await response.json();
console.log('Created contract definition:', result.id);Listing Contract Definitions
Endpoint: GET /wrapper/ui/pages/contract-definition-page
interface ContractDefinitionPage {
contractDefinitions: ContractDefinitionEntry[];
}
interface ContractDefinitionEntry {
contractDefinitionId: string;
accessPolicyId: string;
contractPolicyId: string;
assetSelector: UiCriterion[];
}
const response = await fetch(
'https://api.your-connector-instance.prod.truzztbox.eu/api/management/wrapper/ui/pages/contract-definition-page',
{ headers: { 'X-Api-Key': 'your-api-key' } }
);
const page: ContractDefinitionPage = await response.json();
console.log(`Found ${page.contractDefinitions.length} contract definitions`);Deleting a Contract Definition
Endpoint: DELETE /wrapper/ui/pages/contract-definition-page/contract-definitions/{contractDefinitionId}
curl -X DELETE "https://api.your-connector-instance.prod.truzztbox.eu/api/management/wrapper/ui/pages/contract-definition-page/contract-definitions/museum-data-contract" \
-H "X-Api-Key: your-api-key"Contract Negotiations
A Contract Negotiation is the process where a consumer and provider agree on the terms for data access. Negotiations are initiated by the consumer after discovering an offer in the catalog.
Negotiation States
The sovity EDC provides simplified states for easier handling:
| Simplified State | Description |
|---|---|
IN_PROGRESS | Negotiation is ongoing |
AGREED | Both parties have agreed, contract is established |
TERMINATED | Negotiation was terminated (rejected or failed) |
Initiating a Negotiation
Endpoint: POST /wrapper/ui/pages/catalog-page/contract-negotiations
After fetching data offers from the catalog, you can initiate a negotiation:
cURL Example
curl -X POST "https://api.your-connector-instance.prod.truzztbox.eu/api/management/wrapper/ui/pages/catalog-page/contract-negotiations" \
-H "Content-Type: application/json" \
-H "X-Api-Key: your-api-key" \
-d '{
"counterPartyId": "provider-participant-id",
"counterPartyAddress": "https://provider-connector.example.com/api/dsp",
"contractOfferId": "contract-offer-id-from-catalog",
"assetId": "museum-collection-2024",
"policyJsonLd": "{\"@type\": \"Set\", ...}"
}'TypeScript Example
interface ContractNegotiationRequest {
counterPartyId: string;
counterPartyAddress: string;
contractOfferId: string;
assetId: string;
policyJsonLd: string;
}
// Values typically come from a catalog data offer
const negotiationRequest: ContractNegotiationRequest = {
counterPartyId: dataOffer.participantId,
counterPartyAddress: dataOffer.endpoint,
contractOfferId: dataOffer.contractOffers[0].contractOfferId,
assetId: dataOffer.asset.assetId,
policyJsonLd: dataOffer.contractOffers[0].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: UiContractNegotiation = await response.json();
console.log('Negotiation started:', negotiation.contractNegotiationId);
console.log('State:', negotiation.state.simplifiedState);Checking Negotiation Status
Endpoint: GET /wrapper/ui/pages/catalog-page/contract-negotiations/{contractNegotiationId}
interface UiContractNegotiation {
contractNegotiationId: string;
createdAt: string;
contractAgreementId?: string; // Only set when AGREED
state: {
name: string;
code: number;
simplifiedState: 'IN_PROGRESS' | 'AGREED' | 'TERMINATED';
};
}
async function waitForAgreement(negotiationId: string): Promise<string> {
while (true) {
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: UiContractNegotiation = await response.json();
if (negotiation.state.simplifiedState === 'AGREED') {
return negotiation.contractAgreementId!;
}
if (negotiation.state.simplifiedState === 'TERMINATED') {
throw new Error('Negotiation was terminated');
}
// Wait before polling again
await new Promise(resolve => setTimeout(resolve, 1000));
}
}Contract Agreements
A Contract Agreement is the finalized, binding contract between provider and consumer. Once established, it enables data transfers.
Agreement Structure
interface ContractAgreementCard {
contractAgreementId: string;
contractNegotiationId: string;
direction: 'CONSUMING' | 'PROVIDING';
counterPartyAddress: string;
counterPartyId: string;
contractSigningDate: string;
asset: UiAsset;
contractPolicy: UiPolicy;
transferProcesses: ContractAgreementTransferProcess[];
terminationStatus: 'ONGOING' | 'TERMINATED';
terminationInformation?: ContractAgreementTerminationInfo;
}Listing Contract Agreements
Endpoint: POST /wrapper/ui/pages/contract-agreement-page
You can optionally filter by termination status:
cURL Example (All Agreements)
curl -X POST "https://api.your-connector-instance.prod.truzztbox.eu/api/management/wrapper/ui/pages/contract-agreement-page" \
-H "Content-Type: application/json" \
-H "X-Api-Key: your-api-key" \
-d '{}'cURL Example (Only Active)
curl -X POST "https://api.your-connector-instance.prod.truzztbox.eu/api/management/wrapper/ui/pages/contract-agreement-page" \
-H "Content-Type: application/json" \
-H "X-Api-Key: your-api-key" \
-d '{"terminationStatus": "ONGOING"}'TypeScript Example
interface ContractAgreementPageQuery {
terminationStatus?: 'ONGOING' | 'TERMINATED';
}
interface ContractAgreementPage {
contractAgreements: ContractAgreementCard[];
}
// Get all active agreements
const response = await fetch(
'https://api.your-connector-instance.prod.truzztbox.eu/api/management/wrapper/ui/pages/contract-agreement-page',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': 'your-api-key',
},
body: JSON.stringify({ terminationStatus: 'ONGOING' }),
}
);
const page: ContractAgreementPage = await response.json();
// Separate by direction
const consuming = page.contractAgreements.filter(a => a.direction === 'CONSUMING');
const providing = page.contractAgreements.filter(a => a.direction === 'PROVIDING');
console.log(`Consuming ${consuming.length} data offers`);
console.log(`Providing ${providing.length} data offers`);Getting a Single Agreement
Endpoint: GET /wrapper/ui/pages/contract-agreement-page/{contractAgreementId}
const response = await fetch(
'https://api.your-connector-instance.prod.truzztbox.eu/api/management/wrapper/ui/pages/contract-agreement-page/agreement-123',
{ headers: { 'X-Api-Key': 'your-api-key' } }
);
const agreement: ContractAgreementCard = await response.json();
console.log('Asset:', agreement.asset.title);
console.log('Counter Party:', agreement.counterPartyId);
console.log('Signed:', agreement.contractSigningDate);
console.log('Transfers:', agreement.transferProcesses.length);Terminating a Contract Agreement
Endpoint: POST /wrapper/ui/pages/content-agreement-page/{contractAgreementId}/terminate
Terminating a contract agreement is irreversible. All ongoing transfers will be stopped and no new transfers can be initiated.
cURL Example
curl -X POST "https://api.your-connector-instance.prod.truzztbox.eu/api/management/wrapper/ui/pages/content-agreement-page/agreement-123/terminate" \
-H "Content-Type: application/json" \
-H "X-Api-Key: your-api-key" \
-d '{
"reason": "Contract period ended",
"detail": "The agreed contract period of 12 months has ended. Please contact us to renew the agreement if you wish to continue accessing the data."
}'TypeScript Example
interface ContractTerminationRequest {
reason: string; // Max 100 characters
detail: string; // Max 1000 characters
}
const termination: ContractTerminationRequest = {
reason: 'Contract period ended',
detail: 'The agreed contract period of 12 months has ended.',
};
await fetch(
'https://api.your-connector-instance.prod.truzztbox.eu/api/management/wrapper/ui/pages/content-agreement-page/agreement-123/terminate',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': 'your-api-key',
},
body: JSON.stringify(termination),
}
);Termination Information
When an agreement is terminated, the terminationInformation field contains details:
interface ContractAgreementTerminationInfo {
terminatedAt: string;
reason: string;
detail: string;
terminatedBy: 'SELF' | 'COUNTERPARTY';
}
// Check who terminated
if (agreement.terminationStatus === 'TERMINATED') {
const info = agreement.terminationInformation!;
if (info.terminatedBy === 'COUNTERPARTY') {
console.log(`Terminated by provider: ${info.reason}`);
} else {
console.log(`Terminated by us: ${info.reason}`);
}
}Complete Workflow
Provider Workflow
// 1. Create a policy (see Policies documentation)
const policyId = await createPolicy();
// 2. Create an asset (see Assets documentation)
const assetId = await createAsset();
// 3. Create a contract definition linking asset and policy
const contractDef = await fetch(
'https://api.your-connector-instance.prod.truzztbox.eu/api/management/wrapper/ui/pages/contract-definition-page/contract-definitions',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': 'your-api-key',
},
body: JSON.stringify({
contractDefinitionId: `${assetId}-contract`,
accessPolicyId: policyId,
contractPolicyId: policyId,
assetSelector: [{
operandLeft: 'https://w3id.org/edc/v0.0.1/ns/id',
operator: 'EQ',
operandRight: { type: 'VALUE', value: assetId },
}],
}),
}
);
// Asset is now visible in the catalog!Consumer Workflow
// 1. Fetch data offers from provider (see Catalog documentation)
const offers = await fetchCatalogOffers(providerEndpoint);
// 2. Select an offer and initiate negotiation
const selectedOffer = offers[0];
const negotiation = await initiateNegotiation(selectedOffer);
// 3. Wait for agreement
const agreementId = await waitForAgreement(negotiation.contractNegotiationId);
// 4. Initiate transfer (see Transfer Process documentation)
await initiateTransfer(agreementId);Best Practices
- Use descriptive IDs: Make contract definition IDs meaningful (e.g.,
museum-research-access-2024) - Separate policies: Use different access and contract policies for flexibility
- Monitor negotiations: Poll negotiation status or implement webhooks
- Track agreements: Keep records of all contract agreements for compliance
- Handle terminations: Implement proper cleanup when agreements are terminated
- Validate before creation: Use the validation endpoints to check ID availability