SAP S/4HANA & Business One
Integrate SAP S/4HANA (Cloud and On-Premise) and SAP Business One with Certyo to anchor material documents, goods receipts, and serial/batch data on the blockchain.
Overview
SAP environments vary widely in deployment model and API surface. This guide covers production-ready patterns for the two most common SAP products:
- SAP S/4HANA — Uses SAP Integration Suite (Cloud Integration) with iFlows to transform material document events into Certyo records. Works with both S/4HANA Cloud and on-premise via Cloud Connector.
- SAP Business One — Uses the Service Layer REST API and B1if (Integration Framework) or direct HTTP calls to push Goods Receipts and inventory events to Certyo.
Prerequisites
- SAP BTP (Business Technology Platform) account with Integration Suite entitlement
- SAP S/4HANA system (Cloud or on-premise with Cloud Connector) or SAP Business One 10.0+
- Certyo API key from the backoffice (
certyo_sk_live_...) - Communication Arrangement in S/4HANA for the SAP_COM_0A08 (Material Document) or relevant Business Event scenario
- For Business One: Service Layer access with a dedicated integration user
Architecture
The recommended architecture uses SAP Integration Suite as the middleware layer. S/4HANA publishes events, the iFlow transforms the SAP-specific payload into the Certyo record format, and the HTTP adapter delivers it:
┌─────────────────────┐ ┌──────────────────────┐ ┌──────────────────┐ ┌──────────────┐
│ SAP S/4HANA │ │ SAP Integration │ │ HTTP Adapter │ │ Certyo API │
│ │────>│ Suite │────>│ (outbound) │────>│ │
│ Material Document │ │ │ │ │ │ POST │
│ posted (MIGO) │ │ iFlow: │ │ TLS 1.2+ │ │ /api/v1/ │
│ or Goods Receipt │ │ SAP-to-Certyo │ │ X-API-Key │ │ records │
│ │ │ Transformation │ │ header │ │ │
└─────────────────────┘ └──────────────────────┘ └──────────────────┘ └──────────────┘
│
SAP Business One: │ For S/4HANA on-premise:
Service Layer webhook ──────────>│ SAP Cloud Connector
or B1if scenario ──────────>│ tunnels the connection
│ to BTP securelyS/4HANA Integration
SAP S/4HANA exposes material document events through its Enterprise Event Enablement framework. When a goods receipt (MIGO transaction) or goods issue is posted, the system can publish an event to SAP Integration Suite via the Event Mesh or direct webhook.
Step 1: Configure the Communication Arrangement
In S/4HANA, create a Communication Arrangement using scenario SAP_COM_0A08 (Material Document - Created) or SAP_COM_0A09 (Material Document - Changed). Point the outbound configuration to your Integration Suite tenant.
Step 2: Create the iFlow
In SAP Integration Suite, create an integration flow (iFlow) that receives the S/4HANA event, extracts the material document fields, transforms them into the Certyo record schema, and sends them via HTTP adapter.
Step 3: Deploy and monitor
Deploy the iFlow and monitor message processing in the Integration Suite monitoring dashboard. Failed messages are automatically retried with exponential backoff.
Business One Integration
SAP Business One provides the Service Layer (REST/OData) for programmatic access. The recommended pattern uses the B1if Integration Framework to trigger on document events, or a custom application that polls the Service Layer.
Service Layer event pattern
Business One 10.0+ supports Service Layer Alerts. Configure an alert on GoodsReceiptDocument creation, which calls a webhook URL. Route the webhook through B1if or directly to an intermediary service that calls Certyo.
Polling pattern
If alerts are not available, poll the Service Layer PurchaseDeliveryNotes endpoint with a $filter on UpdateDate. A cron job or scheduled function fetches new documents every 30 seconds and forwards them to Certyo.
Code Samples
This Groovy script runs inside an SAP Integration Suite iFlow. It receives the S/4HANA material document event payload (OData JSON), extracts the relevant fields, and transforms them into the Certyo record format.
import com.sap.gateway.ip.core.customdev.util.Message
import groovy.json.JsonSlurper
import groovy.json.JsonOutput
def Message processData(Message message) {
def body = message.getBody(String)
def slurper = new JsonSlurper()
def sapEvent = slurper.parseText(body)
// Extract material document header fields
def matDoc = sapEvent.d ?: sapEvent
def materialDocument = matDoc.MaterialDocument ?: matDoc.MaterialDocumentNumber
def materialDocumentYear = matDoc.MaterialDocumentYear ?: new Date().format("yyyy")
def postingDate = matDoc.PostingDate ?: matDoc.DocumentDate
def headerText = matDoc.MaterialDocumentHeaderText ?: ""
// Extract line items
def items = matDoc.to_MaterialDocumentItem?.results ?: matDoc.Items ?: []
def recordLines = items.collect { item ->
[
itemNumber : item.MaterialDocumentItem ?: item.LineNumber,
material : item.Material ?: item.ItemCode,
plant : item.Plant ?: item.Warehouse,
storageLocation : item.StorageLocation ?: "",
batch : item.Batch ?: "",
serialNumber : item.SerialNumber ?: "",
quantity : item.QuantityInEntryUnit ?: item.Quantity,
entryUnit : item.EntryUnit ?: item.UnitOfMeasure,
movementType : item.GoodsMovementType ?: item.MovementType,
purchaseOrder : item.PurchaseOrder ?: "",
purchaseOrderItem: item.PurchaseOrderItem ?: ""
]
}
// Build Certyo record payload
def tenantId = message.getProperty("CertyoTenantId")
def certyoRecord = [
tenantId : tenantId,
database : "sap-s4hana",
collection : "MaterialDocuments",
recordId : "${materialDocument}-${materialDocumentYear}",
recordVersion : "1",
operationType : "insert",
sourceTimestamp : postingDate,
idempotencyKey : "${materialDocument}-${materialDocumentYear}-${postingDate?.take(10) ?: 'unknown'}",
recordPayload : [
materialDocument : materialDocument,
materialDocumentYear : materialDocumentYear,
postingDate : postingDate,
headerText : headerText,
documentType : matDoc.MaterialDocumentType ?: "WE",
referenceDocument : matDoc.ReferenceDocument ?: "",
items : recordLines
]
]
message.setBody(JsonOutput.toJson(certyoRecord))
message.setHeader("Content-Type", "application/json")
message.setHeader("X-API-Key", message.getProperty("CertyoApiKey"))
return message
}This SAP Cloud Application Programming (CAP) model handler listens for entity changes on a custom MaterialDocuments entity and calls Certyo when new documents are created. Deploy as a CAP Node.js app on SAP BTP.
const cds = require("@sap/cds");
class CertyoIntegrationService extends cds.ApplicationService {
async init() {
const s4 = await cds.connect.to("API_MATERIAL_DOCUMENT_SRV");
const { MaterialDocumentHeaders } = s4.entities;
// React to new material documents from S/4HANA
this.on("MaterialDocumentCreated", async (req) => {
const { materialDocument, materialDocumentYear } = req.data;
// Fetch full document with line items from S/4HANA
const doc = await s4.run(
SELECT.one
.from(MaterialDocumentHeaders)
.where({
MaterialDocument: materialDocument,
MaterialDocumentYear: materialDocumentYear,
})
.columns((h) => {
h("*");
h.to_MaterialDocumentItem((i) => i("*"));
})
);
if (!doc) {
console.warn(
`Material document ${materialDocument}/${materialDocumentYear} not found`
);
return;
}
// Transform to Certyo record format
const certyoRecord = {
tenantId: process.env.CERTYO_TENANT_ID,
database: "sap-s4hana",
collection: "MaterialDocuments",
recordId: `${doc.MaterialDocument}-${doc.MaterialDocumentYear}`,
recordVersion: "1",
operationType: "insert",
sourceTimestamp: doc.PostingDate,
idempotencyKey: `${doc.MaterialDocument}-${doc.MaterialDocumentYear}-${doc.PostingDate}`,
recordPayload: {
materialDocument: doc.MaterialDocument,
materialDocumentYear: doc.MaterialDocumentYear,
postingDate: doc.PostingDate,
documentDate: doc.DocumentDate,
headerText: doc.MaterialDocumentHeaderText,
referenceDocument: doc.ReferenceDocument,
items: (doc.to_MaterialDocumentItem || []).map((item) => ({
itemNumber: item.MaterialDocumentItem,
material: item.Material,
plant: item.Plant,
storageLocation: item.StorageLocation,
batch: item.Batch,
serialNumber: item.SerialNumber,
quantity: parseFloat(item.QuantityInEntryUnit),
entryUnit: item.EntryUnit,
movementType: item.GoodsMovementType,
purchaseOrder: item.PurchaseOrder,
purchaseOrderItem: item.PurchaseOrderItem,
})),
},
};
// Call Certyo API
const response = await fetch("https://www.certyos.com/api/v1/records", {
method: "POST",
headers: {
"X-API-Key": process.env.CERTYO_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify(certyoRecord),
});
if (!response.ok) {
const errorBody = await response.text();
console.error(
`Certyo ingestion failed for ${materialDocument}: ${response.status} ${errorBody}`
);
throw new Error(`Certyo returned ${response.status}`);
}
const result = await response.json();
console.log(
`Ingested ${materialDocument} -> recordHash: ${result.recordHash}`
);
return result;
});
await super.init();
}
}
module.exports = CertyoIntegrationService;This Python script polls the SAP OData API for new material documents and pushes them to Certyo. Suitable for cron-based scheduling or running as a long-lived process. Works with both S/4HANA and Business One Service Layer.
"""
SAP Material Document Poller -> Certyo Ingestion
Polls SAP OData API for new material documents and ingests them into Certyo.
Supports S/4HANA Cloud, S/4HANA On-Premise (via Cloud Connector), and Business One.
"""
import os
import sys
import time
import logging
from datetime import datetime, timedelta, timezone
from dataclasses import dataclass, field
import requests
from requests.auth import HTTPBasicAuth
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)
# --- Configuration ---
SAP_BASE_URL = os.environ["SAP_BASE_URL"] # e.g. https://my-s4.example.com/sap/opu/odata/sap
SAP_USERNAME = os.environ["SAP_USERNAME"]
SAP_PASSWORD = os.environ["SAP_PASSWORD"]
CERTYO_API_KEY = os.environ["CERTYO_API_KEY"]
CERTYO_TENANT_ID = os.environ["CERTYO_TENANT_ID"]
CERTYO_BASE_URL = os.environ.get("CERTYO_BASE_URL", "https://www.certyos.com")
POLL_INTERVAL_SECONDS = int(os.environ.get("POLL_INTERVAL_SECONDS", "30"))
# S/4HANA Material Document OData endpoint
MATDOC_ENDPOINT = "/API_MATERIAL_DOCUMENT_SRV/A_MaterialDocumentHeader"
@dataclass
class MaterialDocument:
material_document: str
material_document_year: str
posting_date: str
document_date: str
header_text: str = ""
reference_document: str = ""
items: list = field(default_factory=list)
def fetch_new_material_documents(since: datetime) -> list[MaterialDocument]:
"""Fetch material documents created after the given timestamp."""
since_str = since.strftime("%Y-%m-%dT%H:%M:%S")
url = f"{SAP_BASE_URL}{MATDOC_ENDPOINT}"
params = {
"$filter": f"CreationDate gt datetime'{since_str}'",
"$expand": "to_MaterialDocumentItem",
"$orderby": "CreationDate asc",
"$top": 100,
"$format": "json",
}
response = requests.get(
url,
params=params,
auth=HTTPBasicAuth(SAP_USERNAME, SAP_PASSWORD),
headers={"Accept": "application/json"},
timeout=30,
)
response.raise_for_status()
data = response.json()
results = data.get("d", {}).get("results", [])
documents = []
for doc in results:
items = []
for item in doc.get("to_MaterialDocumentItem", {}).get("results", []):
items.append({
"itemNumber": item.get("MaterialDocumentItem", ""),
"material": item.get("Material", ""),
"plant": item.get("Plant", ""),
"storageLocation": item.get("StorageLocation", ""),
"batch": item.get("Batch", ""),
"serialNumber": item.get("SerialNumber", ""),
"quantity": float(item.get("QuantityInEntryUnit", 0)),
"entryUnit": item.get("EntryUnit", ""),
"movementType": item.get("GoodsMovementType", ""),
"purchaseOrder": item.get("PurchaseOrder", ""),
"purchaseOrderItem": item.get("PurchaseOrderItem", ""),
})
documents.append(MaterialDocument(
material_document=doc["MaterialDocument"],
material_document_year=doc["MaterialDocumentYear"],
posting_date=doc.get("PostingDate", ""),
document_date=doc.get("DocumentDate", ""),
header_text=doc.get("MaterialDocumentHeaderText", ""),
reference_document=doc.get("ReferenceDocument", ""),
items=items,
))
return documents
def ingest_to_certyo(doc: MaterialDocument) -> dict:
"""Send a single material document to Certyo for blockchain anchoring."""
record_id = f"{doc.material_document}-{doc.material_document_year}"
posting_date_short = doc.posting_date[:10] if doc.posting_date else "unknown"
payload = {
"tenantId": CERTYO_TENANT_ID,
"database": "sap-s4hana",
"collection": "MaterialDocuments",
"recordId": record_id,
"recordVersion": "1",
"operationType": "insert",
"sourceTimestamp": doc.posting_date,
"idempotencyKey": f"{record_id}-{posting_date_short}",
"recordPayload": {
"materialDocument": doc.material_document,
"materialDocumentYear": doc.material_document_year,
"postingDate": doc.posting_date,
"documentDate": doc.document_date,
"headerText": doc.header_text,
"referenceDocument": doc.reference_document,
"items": doc.items,
},
}
response = requests.post(
f"{CERTYO_BASE_URL}/api/v1/records",
headers={
"X-API-Key": CERTYO_API_KEY,
"Content-Type": "application/json",
},
json=payload,
timeout=15,
)
if response.status_code == 202:
result = response.json()
logger.info("Ingested %s -> recordHash: %s", record_id, result["recordHash"])
return result
else:
logger.error(
"Certyo rejected %s: %s %s", record_id, response.status_code, response.text
)
response.raise_for_status()
return {}
def bulk_ingest_to_certyo(documents: list[MaterialDocument]) -> dict:
"""Send up to 1000 material documents in a single bulk request."""
records = []
for doc in documents:
record_id = f"{doc.material_document}-{doc.material_document_year}"
posting_date_short = doc.posting_date[:10] if doc.posting_date else "unknown"
records.append({
"database": "sap-s4hana",
"collection": "MaterialDocuments",
"recordId": record_id,
"recordVersion": "1",
"operationType": "insert",
"sourceTimestamp": doc.posting_date,
"idempotencyKey": f"{record_id}-{posting_date_short}",
"recordPayload": {
"materialDocument": doc.material_document,
"materialDocumentYear": doc.material_document_year,
"postingDate": doc.posting_date,
"documentDate": doc.document_date,
"headerText": doc.header_text,
"referenceDocument": doc.reference_document,
"items": doc.items,
},
})
response = requests.post(
f"{CERTYO_BASE_URL}/api/v1/records/bulk",
headers={
"X-API-Key": CERTYO_API_KEY,
"Content-Type": "application/json",
},
json={
"tenantId": CERTYO_TENANT_ID,
"records": records,
},
timeout=30,
)
if response.status_code == 202:
result = response.json()
logger.info("Bulk ingested %d records", len(records))
return result
else:
logger.error("Bulk ingest failed: %s %s", response.status_code, response.text)
response.raise_for_status()
return {}
def run_poll_loop():
"""Main polling loop. Tracks high-water mark to avoid reprocessing."""
high_water_mark = datetime.now(timezone.utc) - timedelta(hours=1)
logger.info("Starting SAP -> Certyo poller (interval: %ds)", POLL_INTERVAL_SECONDS)
while True:
try:
documents = fetch_new_material_documents(high_water_mark)
if documents:
logger.info("Found %d new material documents", len(documents))
if len(documents) > 10:
bulk_ingest_to_certyo(documents)
else:
for doc in documents:
ingest_to_certyo(doc)
# Advance high-water mark
high_water_mark = datetime.now(timezone.utc)
else:
logger.debug("No new documents")
except requests.RequestException as e:
logger.error("Poll cycle failed: %s", e)
time.sleep(POLL_INTERVAL_SECONDS)
if __name__ == "__main__":
run_poll_loop()SAP Integration Suite iFlow Configuration
When deploying the Groovy script within an iFlow, configure the following artifacts:
Sender adapter (S/4HANA)
{
"adapterType": "OData",
"connectionType": "Internet",
"address": "https://my-s4hana.example.com/sap/opu/odata/sap/API_MATERIAL_DOCUMENT_SRV",
"authType": "OAuth2ClientCredentials",
"credentialName": "S4HANA_OAUTH_CREDENTIALS",
"entitySet": "A_MaterialDocumentHeader",
"expand": "to_MaterialDocumentItem",
"deltaToken": true,
"pageSize": 100
}Receiver adapter (Certyo)
{
"adapterType": "HTTP",
"address": "https://www.certyos.com/api/v1/records",
"method": "POST",
"authenticationType": "None",
"requestHeaders": [
{ "name": "X-API-Key", "value": "${property.CertyoApiKey}" },
{ "name": "Content-Type", "value": "application/json" }
],
"timeout": 15000,
"retryPolicy": {
"maxRetries": 3,
"retryInterval": 5000,
"exponentialBackoff": true
}
}Externalized parameters
Store credentials securely using SAP Integration Suite's Security Material artifacts. The Certyo API key should be stored as a Secure Parameter and referenced in the iFlow:
# Externalized parameters in the iFlow
CertyoApiKey = {{SecureParameter:CERTYO_API_KEY}}
CertyoTenantId = your-tenant-id
SapOAuthTokenUrl = https://my-s4hana.example.com/sap/bc/sec/oauth2/tokenAuthentication
SAP side
SAP supports multiple authentication methods depending on the deployment:
- S/4HANA Cloud: OAuth 2.0 client credentials via Communication Arrangements. Create a Communication System and Communication User, then generate OAuth credentials.
- S/4HANA On-Premise: Basic Authentication or X.509 client certificates through SAP Cloud Connector. The Cloud Connector establishes a secure tunnel from BTP to your on-premise system.
- Business One: Session-based authentication via the Service Layer POST /b1s/v1/Login endpoint. Use a dedicated integration user with minimal authorizations.
# Authenticate to SAP Business One Service Layer
curl -X POST "https://b1-server:50000/b1s/v1/Login" \
-H "Content-Type: application/json" \
-d '{
"CompanyDB": "SBODEMOUS",
"UserName": "certyo_integration",
"Password": "'"$SAP_B1_PASSWORD"'"
}'
# Response includes a session cookie (B1SESSION) for subsequent requests
# Use this session to query GoodsReceiptDocuments:
curl "https://b1-server:50000/b1s/v1/PurchaseDeliveryNotes?\$filter=UpdateDate ge '2026-04-14'" \
-H "Cookie: B1SESSION=<session-id>" \
-H "Accept: application/json"Certyo side
Certyo uses the X-API-Key header. In SAP Integration Suite, store the key as a Secure Parameter. In custom applications, use environment variables or the SAP Credential Store on BTP:
# Store Certyo API key in SAP BTP Credential Store
cf create-service credstore standard certyo-credentials
cf bind-service certyo-sap-integration certyo-credentials \
-c '{"key": "certyo-api-key", "value": "certyo_sk_live_abc123..."}'GS1 EPCIS Alignment
SAP material numbers, batch numbers, and serial numbers map directly to GS1 EPCIS identifiers, enabling supply chain interoperability. When building your Certyo record payload, include these mappings:
{
"tenantId": "pharma-corp",
"database": "sap-s4hana",
"collection": "MaterialDocuments",
"recordId": "4900000123-2026",
"recordPayload": {
"materialDocument": "4900000123",
"materialDocumentYear": "2026",
"postingDate": "2026-04-14",
"items": [
{
"material": "MAT-001234",
"batch": "BATCH-2026-Q2-001",
"serialNumber": "SN-00001",
"quantity": 500,
"entryUnit": "EA",
"movementType": "101",
"epcis": {
"gtin": "01234567890128",
"sgtin": "urn:epc:id:sgtin:0123456.078901.SN-00001",
"lot": "BATCH-2026-Q2-001",
"bizStep": "urn:epcglobal:cbv:bizstep:receiving",
"disposition": "urn:epcglobal:cbv:disp:in_progress",
"readPoint": "urn:epc:id:sgln:0123456.00001.0"
}
}
]
}
}This mapping enables downstream consumers of your Certyo certificates to correlate them with GS1 EPCIS events, which is critical for pharmaceutical serialization (DSCSA), food safety (FSMA 204), and luxury goods traceability.
Verification & SAP Write-Back
After Certyo anchors the record (60-90 seconds), verify it and update the SAP Quality Inspection record or a custom Z-table with the certificate details. This example uses the S/4HANA OData API to update a custom field on the material document:
"""
Verify Certyo records and write certificate data back to SAP S/4HANA.
Runs as a scheduled job (e.g., every 2 minutes via cron or SAP BTP Job Scheduler).
"""
import os
import time
import requests
from requests.auth import HTTPBasicAuth
CERTYO_BASE_URL = os.environ.get("CERTYO_BASE_URL", "https://www.certyos.com")
CERTYO_API_KEY = os.environ["CERTYO_API_KEY"]
CERTYO_TENANT_ID = os.environ["CERTYO_TENANT_ID"]
SAP_BASE_URL = os.environ["SAP_BASE_URL"]
SAP_USERNAME = os.environ["SAP_USERNAME"]
SAP_PASSWORD = os.environ["SAP_PASSWORD"]
def get_csrf_token() -> tuple[str, requests.Session]:
"""Fetch CSRF token from SAP for write operations."""
session = requests.Session()
session.auth = HTTPBasicAuth(SAP_USERNAME, SAP_PASSWORD)
response = session.head(
f"{SAP_BASE_URL}/sap/opu/odata/sap/API_MATERIAL_DOCUMENT_SRV",
headers={"X-CSRF-Token": "Fetch"},
timeout=10,
)
csrf_token = response.headers.get("X-CSRF-Token", "")
return csrf_token, session
def verify_and_write_back():
"""Query Certyo for anchored records and write verification back to SAP."""
# 1. Query Certyo for recently anchored records
query_response = requests.get(
f"{CERTYO_BASE_URL}/api/v1/records",
params={
"tenantId": CERTYO_TENANT_ID,
"database": "sap-s4hana",
"status": "Anchored",
"limit": 50,
},
headers={"X-API-Key": CERTYO_API_KEY},
timeout=15,
)
query_response.raise_for_status()
records = query_response.json().get("items", [])
if not records:
print("No anchored records to verify")
return
csrf_token, sap_session = get_csrf_token()
for record in records:
record_id = record["recordId"]
payload_data = record.get("recordPayload", {})
# 2. Verify the record on-chain
verify_response = requests.post(
f"{CERTYO_BASE_URL}/api/v1/verify/record",
headers={
"X-API-Key": CERTYO_API_KEY,
"Content-Type": "application/json",
},
json={
"tenantId": CERTYO_TENANT_ID,
"database": "sap-s4hana",
"collection": "MaterialDocuments",
"recordId": record_id,
"payload": payload_data,
},
timeout=15,
)
verification = verify_response.json()
if not verification.get("verified"):
print(f"Verification failed for {record_id}: {verification.get('reasonCategory')}")
continue
# 3. Write certificate data back to SAP Quality Inspection
mat_doc = payload_data.get("materialDocument", "")
mat_doc_year = payload_data.get("materialDocumentYear", "")
# Update custom Z-fields via SAP OData PATCH
sap_session.patch(
f"{SAP_BASE_URL}/sap/opu/odata/sap/ZCERTYO_CERTIFICATE_SRV/CertificateSet(MaterialDocument='{mat_doc}',MaterialDocumentYear='{mat_doc_year}')",
json={
"CertyoVerified": True,
"CertyoRecordHash": verification.get("snapshotHash", ""),
"CertyoMerkleRoot": verification.get("merkleRoot", ""),
"CertyoPolygonTx": verification.get("onChainProof", {}).get("polygonScanEventUrl", ""),
"CertyoIpfsCid": verification.get("ipfsEvidence", {}).get("ipfsCid", ""),
"CertyoVerifiedAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
},
headers={
"X-CSRF-Token": csrf_token,
"Content-Type": "application/json",
"If-Match": "*",
},
timeout=10,
)
print(
f"Wrote certificate to SAP for {mat_doc}/{mat_doc_year}: "
f"{verification['onChainProof']['polygonScanEventUrl']}"
)
if __name__ == "__main__":
verify_and_write_back()Testing
- Configure the SAP Integration Suite iFlow with your Certyo sandbox key and deploy to a test tenant. (
certyo_sk_test_...) - Post a Goods Receipt in S/4HANA (transaction MIGO, movement type 101) or create a Purchase Delivery Note in Business One.
- Monitor the iFlow execution in SAP Integration Suite > Monitor > Message Processing. Confirm the Certyo call returned 202 Accepted and a recordHash.
- Wait 60-90 seconds for anchoring, then call POST /api/v1/verify/record with the material document data to confirm on-chain anchoring.
- Run the verification write-back script and confirm the certificate data appears in the SAP custom fields or Z-table.
Troubleshooting
- 401 from S/4HANA OData — Check that the Communication Arrangement is active and the OAuth credentials have not expired. For on-premise, verify the Cloud Connector tunnel is up.
- Empty items array in Certyo — Ensure the OData $expand parameter includes to_MaterialDocumentItem. Without it, the line items are not fetched.
- Duplicate records in Certyo — The idempotencyKey pattern uses materialDocument-year-postingDate. If you repost the same document on the same day, Certyo returns the original response with idempotencyReplayed: true.
- CSRF token errors on SAP write-back — Always fetch a fresh CSRF token before PATCH/POST operations. Tokens expire after the SAP session timeout (default 30 minutes).
AI Integration Skill
Download a skill file that enables AI agents to generate working SAP + Certyo integration code for any language or framework.
What's inside
- Authentication — OAuth 2.0 / X.509 via SAP BTP and Integration Suite credentials
- Architecture — Material document → SAP Integration Suite iFlow → HTTP adapter → Certyo
- Field mapping — MATNR, CHARG, SERNR, BWART to Certyo fields with GS1 EPCIS alignment
- Code examples — Groovy iFlow transform, JavaScript CAP handler, Python OData poller
- Verification — OData PATCH with CSRF token handling for write-back to SAP
- Integration patterns — S/4HANA Cloud via Integration Suite, on-premise via Cloud Connector
How to use
Claude Code
Place the file in your project's .claude/commands/ directory, then use it as a slash command:
# Download the skill file
mkdir -p .claude/commands
curl -o .claude/commands/certyo-sap.md \
https://www.certyos.com/developers/skills/certyo-sap-skill.md
# Use it in Claude Code
/certyo-sap "Generate an SAP Integration Suite iFlow that sends material documents to Certyo"Cursor / Copilot / Any AI Agent
Add the file to your project root or attach it to a conversation. The AI agent will use the SAP-specific patterns, field mappings, and code examples to generate correct integration code.
# Add to your project
curl -o CERTYO_SAP.md \
https://www.certyos.com/developers/skills/certyo-sap-skill.md
# Then in your AI agent:
"Using the Certyo SAP spec in CERTYO_SAP.md,
generate an sap integration suite iflow that sends material documents to certyo"CLAUDE.md Context File
Append the skill file to your project's CLAUDE.md so every Claude conversation has SAP + Certyo context automatically.
# Append to your project's CLAUDE.md
echo "" >> CLAUDE.md
echo "## Certyo SAP Integration" >> CLAUDE.md
cat CERTYO_SAP.md >> CLAUDE.md