Skip to content

The ERP receive endpoint

Receiving orders

For: ERP developers, integration partners

This 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 }
    ]
  }
}
PartNotes
X-API-KEY headerVerify on every call (constant-time compare). Reject with 401 otherwise.
request_idAIOTIC's order id. Stable across retries — the key for idempotency. Store it on your sales order as external reference.
purchase_orderThe ErpPurchaseOrder: reviewed values with operator corrections applied. Every key present; unknown values null.
purchase_order.customer.customer_idYour debtor number. Can be null only if an operator explicitly sent an unidentified order — decide whether you accept that.
purchase_order.items[].article_numberYour 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, pipeline
python
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-KEY verified, constant-time
  • [ ] Idempotent on request_id (same id → same order_number, no second booking)
  • [ ] Answers within a few seconds; heavy work (PDF archiving, e-mails) happens after the response
  • [ ] Business problems → success: false with a human-readable error; infrastructure problems → any status, ideally also success: false
  • [ ] Never returns HTML (a reverse-proxy error page is treated as a server error and the send rolls back)
  • [ ] Logs the request_id with every line so support can correlate

Next: Response contract

Documentation revision 3 · Published 8 September 2026 · commit 6862d5e. Verified against AIOTIC API v1.0.0. AIOTIC is a product of DevOps Company.