Appearance
The ERP receive endpoint
Receiving orders
For: ERP developers, integration partnersThis is the one thing every integration must implement: an HTTPS endpoint that AIOTIC calls with a reviewed purchase order, and that answers whether your ERP accepted it — normally by creating a sales order and returning its number.
Rendering diagram…
Triggered by an operator in the app or by POST /erp/send/{request_id}. AIOTIC waits for your answer.
The request
http
POST https://integration.example.com/aiotic/orders HTTP/1.1
Content-Type: application/json
X-API-KEY: <the key you gave AIOTIC>
{
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"purchase_order": {
"order_number": "EB2500011645",
"order_date": "2026-01-14",
"delivery_date": "2026-02-01",
"currency": "EUR",
"total_price": 1234.56,
"additional_information": "Please deliver before noon.",
"supplier": { "company": "Acme Supplies BV", "contact_person": "P. Jansen", "email": "orders@acme.example",
"address": { "street": "Industrieweg 5", "postal_code": "1234 AB", "city": "Amsterdam", "country": "NL" } },
"customer": { "customer_id": "58931", "company": "LUMITECH INSTALLATIES", "contact_person": "J. de Boer",
"email": "info@lumitech.example", "phone": "+31 55 123 4567", "vat_id": "NL001234567B01", "iban": null, "bic": null,
"address": { "street": "Ambachtsweg 12", "postal_code": "7327 AA", "city": "Apeldoorn", "country": "NL" } },
"shipping_details": { "recipient": { "company": "LUMITECH INSTALLATIES", "department": null, "contact_person": "J. de Boer",
"email": null, "phone": null,
"address": { "street": "Ambachtsweg 12", "postal_code": "7327 AA", "city": "Apeldoorn", "country": "NL" } },
"special_instructions": null },
"items": [
{ "article_number": "PROD-001", "description": "LED Driver 48V", "quantity": 10, "unit": "ST",
"price": 12.34, "currency": "EUR", "line_total": 123.40 }
]
}
}| Part | Notes |
|---|---|
X-API-KEY header | Verify on every call (constant-time compare). Reject with 401 otherwise. |
request_id | AIOTIC's order id. Stable across retries — the key for idempotency. Store it on your sales order as external reference. |
purchase_order | The ErpPurchaseOrder: reviewed values with operator corrections applied. Every key present; unknown values null. |
purchase_order.customer.customer_id | Your debtor number. Can be null only if an operator explicitly sent an unidentified order — decide whether you accept that. |
purchase_order.items[].article_number | Your SKU. Can be null only on an explicit override send of an ATTENTION order. |
The complete field-by-field reference of this payload — every JSON path with type, nullability and meaning — is in the API reference → Outbound: webhooks, and the JSON Schema is in the OpenAPI document under webhooks.erpReceiveOrder.
The response
Answer with JSON. The body decides; the HTTP status does not.
json
{ "success": true, "order_number": "SO-2026-00981" }json
{ "success": false, "error": "Unknown article number: PROD-999" }Details in Response contract.
Minimal implementations
python
from aiotic.service import build_app
from aiotic.erp.memory import InMemoryErp # → your adapter, see /sdk/erp-adapters
app = build_app(erp=InMemoryErp()) # POST /aiotic/orders with key check, idempotency, pipelinepython
import hmac, os
from fastapi import FastAPI, Header, Request
app = FastAPI()
KEY = os.environ["AIOTIC_ERP_RECEIVE_KEY"]
@app.post("/aiotic/orders")
async def receive(request: Request, x_api_key: str = Header(alias="X-API-KEY")):
if not hmac.compare_digest(x_api_key, KEY):
return {"success": False, "error": "unauthorized"}
body = await request.json()
rid, po = body["request_id"], body["purchase_order"]
existing = erp.find_by_external_ref(rid) # idempotency
if existing:
return {"success": True, "order_number": existing}
try:
number = erp.create_sales_order(external_ref=rid, **map_order(po))
except BusinessError as e: # unknown item, blocked customer, …
return {"success": False, "error": str(e)}
return {"success": True, "order_number": number}csharp
app.MapPost("/aiotic/orders", async (HttpRequest http, ErpService erp) =>
{
if (!CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(http.Headers["X-API-KEY"].ToString()),
Encoding.UTF8.GetBytes(config.ReceiveKey)))
return Results.Json(new { success = false, error = "unauthorized" }, statusCode: 401);
var body = await http.ReadFromJsonAsync<ErpReceiveRequest>();
var existing = await erp.FindByExternalRef(body.RequestId);
if (existing != null) return Results.Json(new { success = true, order_number = existing });
try {
var number = await erp.CreateSalesOrder(body.RequestId, body.PurchaseOrder);
return Results.Json(new { success = true, order_number = number });
} catch (BusinessRuleException e) {
return Results.Json(new { success = false, error = e.Message });
}
});Checklist for this endpoint
- [ ] HTTPS with a valid certificate; reachable from the internet (or allow-listed for the tenant's egress IP)
- [ ]
X-API-KEYverified, constant-time - [ ] Idempotent on
request_id(same id → sameorder_number, no second booking) - [ ] Answers within a few seconds; heavy work (PDF archiving, e-mails) happens after the response
- [ ] Business problems →
success: falsewith a human-readableerror; infrastructure problems → any status, ideally alsosuccess: false - [ ] Never returns HTML (a reverse-proxy error page is treated as a server error and the send rolls back)
- [ ] Logs the
request_idwith every line so support can correlate
Next: Response contract →