Recipes — Copy-and-Run Task Pages

One page per task, self-contained: the complete working payload, a full runnable script (Python and C#), the verified gotchas, and a verify step. Doing one task? Load one recipe — not the whole manual. For concept depth, each recipe links into the manual; for the complete field list of any service, load its JSON from definitions/.

If you're an AI assistant: read this file, then only the recipe for your task. The recipe is the source of truth for that process; the manual sections it links to are the deep dive.

Index

Recipe Task API
update-contract-lines Update or insert job-contract lines, prices, commission costs Transaction
edit-contract-bins Change contract bin min/max/reorder/capacity Transaction (IgnoreDisabled), Interactive fallback
create-bins Bulk-create warehouse bins Transaction (BinLocation)
create-sales-order Create a sales order Transaction (Order)
order-with-assembly Order a line that explodes / spawns a production order Interactive
set-primary-bin-supplier Set an item's primary bin or primary supplier at a location Transaction (Item), Interactive fallback
generate-pick-ticket-pdf Generate/reprint pick tickets and POs as PDF pdfreport (m_* services)
production-order-runbook Full production cycle: create → print → confirm → complete → ship Transaction + Interactive
record-labor-time Post labor hours to a production order Transaction (TimeEntry)
inventory-adjustment Adjust on-hand quantity (write-offs) Transaction (InventoryAdjustment)
update-supplier-contact Write a supplier's email / central phone (shared address record) Transaction (Address)
create-customer Create a customer master record (salesrep + default_branch gotchas) Transaction (Customer)
create-requisition-po Create a requisition PO (po_type 'R'; vendor vs supplier) Transaction (RequisitionPurchaseOrder)

Shared conventions (recipes don't repeat these)

Environment. Examples use https://play.p21server.com with user api_user and generic data (ACME, WIDGET-001, customer 100198). Substitute your own. Always run against a test/play environment first.

Auth preamble. Every script below starts with this (shown once here; recipes reference p21_auth() / P21Session.CreateAsync()):

import httpx

def p21_auth(base_url: str, username: str, password: str) -> tuple[str, str, dict]:
    """Authenticate and resolve the UI server. Returns (token, ui_server, headers)."""
    auth = httpx.post(
        f"{base_url}/api/security/token/v2",
        json={"username": username, "password": password},
        headers={"Accept": "application/json"},
        verify=False,
    )
    auth.raise_for_status()
    token = auth.json()["AccessToken"]

    router = httpx.get(
        f"{base_url}/api/ui/router/v1/?urlType=external",  # trailing slash avoids a 307
        headers={"Authorization": f"Bearer {token}", "Accept": "application/json"},
        verify=False, follow_redirects=True,
    )
    router.raise_for_status()
    ui_server = router.json()["Url"].rstrip("/")

    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json",
        "Accept": "application/json",
    }
    return token, ui_server, headers
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using Newtonsoft.Json.Linq;

public sealed class P21Session
{
    public required HttpClient Http { get; init; }
    public required string UiServer { get; init; }

    public static async Task<P21Session> CreateAsync(
        string baseUrl, string username, string password)
    {
        var http = new HttpClient();
        http.DefaultRequestHeaders.Add("Accept", "application/json");

        var authBody = new JObject { ["username"] = username, ["password"] = password };
        var authResp = await http.PostAsync(
            $"{baseUrl}/api/security/token/v2",
            new StringContent(authBody.ToString(), Encoding.UTF8, "application/json"));
        authResp.EnsureSuccessStatusCode();
        var token = JObject.Parse(await authResp.Content.ReadAsStringAsync())["AccessToken"]!.ToString();

        http.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", token);

        // trailing slash avoids a 307 redirect on some installs
        var routerResp = await http.GetAsync($"{baseUrl}/api/ui/router/v1/?urlType=external");
        routerResp.EnsureSuccessStatusCode();
        var uiServer = JObject.Parse(await routerResp.Content.ReadAsStringAsync())["Url"]!
            .ToString().TrimEnd('/');

        return new P21Session { Http = http, UiServer = uiServer };
    }
}

Validate payloads before posting. Shape mistakes (a string where an array belongs, IgnoreDisabled at the wrong level, booleans in quotes) fail in confusing ways. Run scripts/validate_payload.py on your payload file (JSON or XML) — it checks structure and field names against definitions/ offline. See Payload Anatomy.

Check the result, not the HTTP status. The Transaction API returns HTTP 200 even when everything failed. Every recipe checks Summary.Succeeded / Summary.Failed and prints Messages on failure; transactions in one POST pass/fail independently.

Verify after writing. A Succeeded response is not proof the value landed (field-order cascades and silent no-ops exist). Each recipe ends with a read-back — OData or POST /api/v2/transaction/get.

Full field lists. Recipes show the fields that matter for the task. For every field a service accepts (names, types, keys, labels, payload template), load definitions/{Service}.json — see the definitions README.

Snippets vs end-to-end files. The tabs on each page are self-contained snippets (portable — paste anywhere). Each recipe also links complete runnable files under examples/python/recipes/ (Python) and examples/csharp/Recipes/ (C#): they use the repo's shared config/auth helpers, run against your .env, dry-run by default (print the payload; --execute / typing EXECUTE posts), and end with the verify read-back.

Credit: the cookbook pattern and much of the verified content come from Alex Westemeier's process playbook.