SAP S/4HANA & Business One
Integra SAP S/4HANA (Cloud y On-Premise) y SAP Business One con Certyo para anclar documentos de material, recepciones de mercancía y datos de serie/lote en la blockchain.
Descripción general
Los entornos SAP varían ampliamente en modelo de despliegue y superficie de API. Esta guía cubre patrones listos para producción para los dos productos SAP más comunes:
- SAP S/4HANA — Usa SAP Integration Suite (Cloud Integration) con iFlows para transformar eventos de documentos de material en registros Certyo. Funciona tanto con S/4HANA Cloud como on-premise vía Cloud Connector.
- SAP Business One — Usa la API REST del Service Layer y B1if (Integration Framework) o llamadas HTTP directas para enviar recepciones de mercancía y eventos de inventario a Certyo.
Requisitos previos
- Cuenta de SAP BTP (Business Technology Platform) con derecho a Integration Suite
- Sistema SAP S/4HANA (Cloud u on-premise con Cloud Connector) o SAP Business One 10.0+
- Clave API de Certyo obtenida del backoffice (
certyo_sk_live_...) - Communication Arrangement en S/4HANA para el escenario SAP_COM_0A08 (Material Document) o el escenario de Business Event relevante
- Para Business One: acceso al Service Layer con un usuario de integración dedicado
Arquitectura
La arquitectura recomendada usa SAP Integration Suite como capa de middleware. S/4HANA publica eventos, el iFlow transforma el payload específico de SAP al formato de registro de Certyo, y el adaptador HTTP lo entrega:
┌─────────────────────┐ ┌──────────────────────┐ ┌──────────────────┐ ┌──────────────┐
│ 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 securelyIntegración con S/4HANA
SAP S/4HANA expone eventos de documentos de material a través de su framework Enterprise Event Enablement. Cuando se registra una recepción de mercancía (transacción MIGO) o una salida de mercancía, el sistema puede publicar un evento a SAP Integration Suite vía Event Mesh o webhook directo.
Paso 1: Configurar el Communication Arrangement
En S/4HANA, crea un Communication Arrangement usando el escenario SAP_COM_0A08 (Material Document - Created) o SAP_COM_0A09 (Material Document - Changed). Apunta la configuración de salida a tu tenant de Integration Suite.
Paso 2: Crear el iFlow
En SAP Integration Suite, crea un flujo de integración (iFlow) que reciba el evento de S/4HANA, extraiga los campos del documento de material, los transforme al esquema de registro de Certyo y los envíe vía adaptador HTTP.
Paso 3: Desplegar y monitorear
Despliega el iFlow y monitorea el procesamiento de mensajes en el panel de monitoreo de Integration Suite. Los mensajes fallidos se reintentan automáticamente con backoff exponencial.
Integración con Business One
SAP Business One proporciona el Service Layer (REST/OData) para acceso programático. El patrón recomendado usa el B1if Integration Framework para activarse en eventos de documentos, o una aplicación personalizada que consulta el Service Layer.
Patrón de eventos del Service Layer
Business One 10.0+ soporta alertas del Service Layer. Configura una alerta en la creación de GoodsReceiptDocument, que llama a una URL de webhook. Enruta el webhook a través de B1if o directamente a un servicio intermediario que llama a Certyo.
Patrón de polling
Si las alertas no están disponibles, consulta el endpoint PurchaseDeliveryNotes del Service Layer con un $filter en UpdateDate. Un cron job o función programada obtiene nuevos documentos cada 30 segundos y los reenvía a Certyo.
Ejemplos de código
Este script Groovy se ejecuta dentro de un iFlow de SAP Integration Suite. Recibe el payload del evento de documento de material de S/4HANA (OData JSON), extrae los campos relevantes y los transforma al formato de registro de Certyo.
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
}Este handler del modelo SAP Cloud Application Programming (CAP) escucha cambios de entidad en una entidad personalizada MaterialDocuments y llama a Certyo cuando se crean nuevos documentos. Despliégalo como una aplicación CAP Node.js en 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;Este script Python consulta la API OData de SAP para nuevos documentos de material y los envía a Certyo. Adecuado para programación basada en cron o ejecución como proceso de larga duración. Funciona tanto con S/4HANA como con el Service Layer de Business One.
"""
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()Configuración del iFlow de SAP Integration Suite
Al desplegar el script Groovy dentro de un iFlow, configura los siguientes artefactos:
Adaptador de envío (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
}Adaptador de recepción (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
}
}Parámetros externalizados
Almacena las credenciales de forma segura usando los artefactos de Security Material de SAP Integration Suite. La clave API de Certyo debe almacenarse como un Secure Parameter y referenciarse en el 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/tokenAutenticación
Lado de SAP
SAP soporta múltiples métodos de autenticación según el despliegue:
- S/4HANA Cloud: Credenciales de cliente OAuth 2.0 vía Communication Arrangements. Crea un Communication System y un Communication User, luego genera credenciales OAuth.
- S/4HANA On-Premise: Autenticación básica o certificados de cliente X.509 a través de SAP Cloud Connector. El Cloud Connector establece un túnel seguro desde BTP a tu sistema on-premise.
- Business One: Autenticación basada en sesión vía el endpoint POST /b1s/v1/Login del Service Layer. Usa un usuario de integración dedicado con autorizaciones mínimas.
# 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"Lado de Certyo
Certyo usa el encabezado X-API-Key. En SAP Integration Suite, almacena la clave como un Secure Parameter. En aplicaciones personalizadas, usa variables de entorno o el Credential Store de SAP en 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..."}'Alineación con GS1 EPCIS
Los números de material, números de lote y números de serie de SAP se mapean directamente a identificadores GS1 EPCIS, habilitando interoperabilidad en la cadena de suministro. Al construir el payload del registro Certyo, incluye estos mapeos:
{
"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"
}
}
]
}
}Este mapeo permite que los consumidores posteriores de tus certificados Certyo los correlacionen con eventos GS1 EPCIS, lo cual es crítico para la serialización farmacéutica (DSCSA), seguridad alimentaria (FSMA 204) y trazabilidad de bienes de lujo.
Verificación y escritura de vuelta a SAP
Después de que Certyo ancla el registro (60-90 segundos), verifica y actualiza el registro de Quality Inspection de SAP o una tabla Z personalizada con los detalles del certificado. Este ejemplo usa la API OData de S/4HANA para actualizar un campo personalizado en el documento de material:
"""
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()Pruebas
- Configura el iFlow de SAP Integration Suite con tu clave sandbox de Certyo y despliégalo en un tenant de prueba. (
certyo_sk_test_...) - Registra una recepción de mercancía en S/4HANA (transacción MIGO, tipo de movimiento 101) o crea una nota de entrega de compra en Business One.
- Monitorea la ejecución del iFlow en SAP Integration Suite > Monitor > Message Processing. Confirma que la llamada a Certyo retornó 202 Accepted y un recordHash.
- Espera 60-90 segundos para el anclaje, luego llama a POST /api/v1/verify/record con los datos del documento de material para confirmar el anclaje on-chain.
- Ejecuta el script de verificación y escritura de vuelta y confirma que los datos del certificado aparecen en los campos personalizados o tabla Z de SAP.
Solución de problemas
- 401 from S/4HANA OData — Verifica que el Communication Arrangement esté activo y que las credenciales OAuth no hayan expirado. Para on-premise, verifica que el túnel de Cloud Connector esté activo.
- Empty items array in Certyo — Asegúrate de que el parámetro $expand de OData incluya to_MaterialDocumentItem. Sin él, las líneas del documento no se obtienen.
- Duplicate records in Certyo — El patrón de idempotencyKey usa materialDocument-year-postingDate. Si vuelves a registrar el mismo documento el mismo día, Certyo retorna la respuesta original con idempotencyReplayed: true.
- CSRF token errors on SAP write-back — Siempre obtén un token CSRF fresco antes de operaciones PATCH/POST. Los tokens expiran después del tiempo de espera de sesión de SAP (30 minutos por defecto).
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 vía SAP BTP y credenciales de Integration Suite
- Architecture — Documento de material → iFlow de SAP Integration Suite → adaptador HTTP → Certyo
- Field mapping — MATNR, CHARG, SERNR, BWART a campos de Certyo con alineación GS1 EPCIS
- Code examples — Transformación Groovy en iFlow, handler JavaScript CAP, poller Python OData
- Verification — PATCH OData con manejo de token CSRF para escritura de vuelta a SAP
- Integration patterns — S/4HANA Cloud vía Integration Suite, on-premise vía 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 "Genera un iFlow de SAP Integration Suite que envíe documentos de material a 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,
genera un iflow de sap integration suite que envíe documentos de material a 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