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: falseto 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.countis the number of events returned in the current page, not the total match count. Iterate by incrementingoffsetbylimituntil 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
- Authentication — Bearer tokens, error responses, required headers
- Best Practices — Idempotency, error handling, rate limits
- Code Examples — Multi-language snippets