SCC Compliance

zplflow exposes endpoints that satisfy the obligations imposed by the EU Standard Contractual Clauses adopted under Commission Implementing Decision (EU) 2021/915. Use them to register breach notification contacts and retrieve audit events.

Contents: Breach Contact · Audit Events · Regulatory References


Overview

Endpoint SCC Clause Purpose
PUT /v1/tenants/{id}/breach-contact 9.3 Register the email address that must be notified of a personal data breach
GET /v1/audit/events 7.6 Retrieve the log of events recorded for the authenticated tenant

Both endpoints are tenant-scoped. The tenant identifier is always taken from the API key authentication context; cross-tenant requests are rejected.


PUT /v1/tenants/{id}/breach-contact

Configure the email address that zplflow uses to notify the data controller of a personal data breach. The path {id} must match the tenant resolved from the API key; otherwise the request is rejected with 403 FORBIDDEN.

Idempotent: no (always overwrites the previous contact)
Content-Type: application/json

Path Parameters

Parameter Type Description
id string Tenant identifier; must match the authenticated tenant

Request Body

{
  "email": "dpo@company.com",
  "enabled": true
}
Field Type Required Description
email string yes RFC 5322 email address, max 254 characters
enabled bool yes When false, suppresses outbound notifications while keeping the contact on file

Response (200 OK)

{
  "status": "breach_contact_updated",
  "email": "dpo@company.com",
  "enabled": true
}

Errors

HTTP Code Cause
400 INVALID_PARAMS Malformed body or invalid email address
401 UNAUTHORIZED Missing or invalid API key
403 FORBIDDEN Path {id} does not match the authenticated tenant

Example

curl -X PUT "https://api.zplflow.io/v1/tenants/t-123/breach-contact" \
  -H "Authorization: Bearer lb_xxxx" \
  -H "Content-Type: application/json" \
  -d '{"email":"dpo@company.com","enabled":true}'
import requests

resp = requests.put(
    "https://api.zplflow.io/v1/tenants/t-123/breach-contact",
    headers={
        "Authorization": "Bearer lb_xxxx",
        "Content-Type": "application/json",
    },
    json={"email": "dpo@company.com", "enabled": True},
)
print(resp.json())
# {"status": "breach_contact_updated", "email": "dpo@company.com", "enabled": True}
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
)

func main() {
	body, _ := json.Marshal(map[string]interface{}{
		"email":   "dpo@company.com",
		"enabled": true,
	})
	req, _ := http.NewRequest("PUT",
		"https://api.zplflow.io/v1/tenants/t-123/breach-contact",
		bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer lb_xxxx")
	req.Header.Set("Content-Type", "application/json")

	resp, _ := http.DefaultClient.Do(req)
	defer resp.Body.Close()
	var out map[string]interface{}
	json.NewDecoder(resp.Body).Decode(&out)
	fmt.Println(out)
}
<?php
$ch = curl_init("https://api.zplflow.io/v1/tenants/t-123/breach-contact");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    "email"   => "dpo@company.com",
    "enabled" => true,
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer lb_xxxx",
    "Content-Type: application/json"
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
echo curl_exec($ch);

AS400 / SQL Native (IBM i)

SELECT SYSTOOLS.HTTPPUTCLOB(
    'https://api.zplflow.io/v1/tenants/t-123/breach-contact',
    '{"email":"dpo@company.com","enabled":true}',
    '{"Authorization":"Bearer lb_xxxx","Content-Type":"application/json"}'
) AS response
FROM SYSIBM.SYSDUMMY1;

AS400 / HTTPAPI (RPG)

dcl-s body varchar(500);
dcl-s response varchar(32000);
dcl-s rc int(10);

body = '{"email":"dpo@company.com","enabled":true}';

rc = http_req('PUT'
    : 'https://api.zplflow.io/v1/tenants/t-123/breach-contact'
    : *null
    : %trim(response)
    : 'Authorization: Bearer lb_xxxx'
    : 'Content-Type: application/json'
    : *null
    : 30000
    : %trim(body));

Note: Set enabled: false to suspend notifications without erasing the contact. The audit log still records every change.


GET /v1/audit/events

Retrieve the audit events recorded for the authenticated tenant. The tenant identifier is forced from the API key context; you cannot query another tenant through this endpoint.

Idempotent: yes (read-only)

Query Parameters

Parameter Type Default Description
from RFC3339 - Inclusive lower bound on timestamp
to RFC3339 - Inclusive upper bound on timestamp
action string - Exact-match filter on the event action field
limit int 100 Page size, 1-1000
offset int 0 Skip count, >= 0

Response (200 OK)

{
  "events": [
    {
      "id": "7c2b6c5e-1f4d-4c0a-9f1a-7e2b1e9d5b41",
      "tenant_id": "t-123",
      "actor_email": "ops@company.com",
      "action": "TENANT_DELETED",
      "resource_type": "tenant",
      "resource_id": "t-123",
      "details": "jobs_canceled=5",
      "timestamp": "2026-07-14T14:05:00Z",
      "ip_address": "203.0.113.42"
    }
  ],
  "meta": {
    "limit": 100,
    "offset": 0,
    "count": 42
  }
}
Field Type Description
events[].id string Event identifier (UUID)
events[].tenant_id string Owning tenant
events[].actor_email string Email of the user or service that triggered the action
events[].action string Action code, e.g. TENANT_DELETED, API_KEY_CREATED
events[].resource_type string Resource category (tenant, api_key, job, …)
events[].resource_id string Identifier of the affected resource
events[].details string Free-form context (key=value pairs)
events[].timestamp RFC3339 UTC event time
events[].ip_address string Source IP of the request
meta.count int Number of events returned in the current page
meta.limit int Effective page size
meta.offset int Effective offset

Errors

HTTP Code Cause
400 INVALID_PARAMS Invalid from, to, limit, or offset value
401 UNAUTHORIZED Missing or invalid API key

Example

# Last 24 hours of TENANT_DELETED events
FROM_TS=$(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ)

curl -G "https://api.zplflow.io/v1/audit/events" \
  -H "Authorization: Bearer lb_xxxx" \
  --data-urlencode "from=${FROM_TS}" \
  --data-urlencode "action=TENANT_DELETED" \
  --data-urlencode "limit=200"
import requests
from datetime import datetime, timedelta, timezone

headers = {"Authorization": "Bearer lb_xxxx"}
params = {
    "from": (datetime.now(timezone.utc) - timedelta(hours=24)).isoformat(),
    "action": "TENANT_DELETED",
    "limit": 200,
}

resp = requests.get("https://api.zplflow.io/v1/audit/events", headers=headers, params=params)
data = resp.json()
for event in data["events"]:
    print(f"{event['timestamp']}  {event['action']}  by {event.get('actor_email')}")
print(f"Returned {data['meta']['count']} events")
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"time"
)

func main() {
	q := url.Values{}
	q.Set("from", time.Now().Add(-24*time.Hour).UTC().Format(time.RFC3339))
	q.Set("action", "TENANT_DELETED")
	q.Set("limit", "200")

	req, _ := http.NewRequest("GET",
		"https://api.zplflow.io/v1/audit/events?"+q.Encode(), nil)
	req.Header.Set("Authorization", "Bearer lb_xxxx")

	resp, _ := http.DefaultClient.Do(req)
	defer resp.Body.Close()

	var data struct {
		Events []struct {
			Timestamp  string `json:"timestamp"`
			Action     string `json:"action"`
			ActorEmail string `json:"actor_email"`
		} `json:"events"`
		Meta struct {
			Count int `json:"count"`
		} `json:"meta"`
	}
	json.NewDecoder(resp.Body).Decode(&data)
	for _, e := range data.Events {
		fmt.Printf("%s  %s  by %s\n", e.Timestamp, e.Action, e.ActorEmail)
	}
	fmt.Printf("Returned %d events\n", data.Meta.Count)
}
<?php
$from = gmdate('Y-m-d\TH:i:s\Z', time() - 86400);
$url  = "https://api.zplflow.io/v1/audit/events?" . http_build_query([
    "from"   => $from,
    "action" => "TENANT_DELETED",
    "limit"  => 200,
]);

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer lb_xxxx"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$data = json_decode(curl_exec($ch), true);
foreach ($data['events'] as $e) {
    echo "{$e['timestamp']}  {$e['action']}  by {$e['actor_email']}\n";
}
echo "Returned {$data['meta']['count']} events\n";

AS400 / SQL Native (IBM i)

SELECT SYSTOOLS.HTTPGETCLOB(
    'https://api.zplflow.io/v1/audit/events?from=2026-07-14T00:00:00Z&action=TENANT_DELETED&limit=200',
    NULL,
    '{"Authorization":"Bearer lb_xxxx"}'
) AS response
FROM SYSIBM.SYSDUMMY1;

AS400 / HTTPAPI (RPG)

dcl-s url varchar(500);
dcl-s response varchar(65000);
dcl-s rc int(10);

url = 'https://api.zplflow.io/v1/audit/events'
    + '?from=2026-07-14T00:00:00Z'
    + '&action=TENANT_DELETED'
    + '&limit=200';

rc = http_req('GET'
    : %trim(url)
    : *null
    : %trim(response)
    : 'Authorization: Bearer lb_xxxx'
    : *null
    : *null
    : 30000
    : *null);

Note: meta.count is the number of events returned in the current page, not the total match count. Iterate by incrementing offset by limit until the returned page is empty.


Regulatory References

  • Commission Implementing Decision (EU) 2021/915 of 4 June 2021 on standard contractual clauses for the transfer of personal data to third countries.
  • SCC Clause 7.6 — Documenting and auditing processing activities.
  • SCC Clause 9.3 — Notification of a personal data breach to the data controller.

Next Steps