Entity API

Disclaimer: This is unofficial, community-created documentation for Epicor Prophet 21 APIs. It is not affiliated with, endorsed by, or supported by Epicor Software Corporation. All product names, trademarks, and registered trademarks are property of their respective owners. Use at your own risk.


Status Update (February 2026): The Entity API is functional. Previous reports of it being non-functional were due to incorrect endpoint URLs. The customer/vendor/contact/address entities live under /api/entity/.


Terminology — Epicor's Naming (July 2026)

Epicor's own use of "Entity API" is broader than this document. In Epicor's naming (per the SOA middleware admin site):

Corroborated by the in-middleware SDK reference (/docs/p21sdk on a 2026.1 tenant), which is Epicor's own first-party catalog and lists exactly four APIs — no more:

SDK name Epicor's description
Transaction API "...previously known as the v2 API — a stateless REST API that works with most of our system using a metadata model."
Entity API "...a very specific set of stateless, fit for purpose REST endpoints using strongly typed business object models."
Interactive API "...allowing developers to interact with a stateful Prophet 21 session."
Data Services API "...secure reads from your Prophet 21 database using OData protocol."

Two things worth noting: Epicor confirms in its own words that the Transaction API is the artist formerly known as the "v2 API", and the SDK's four-way split is by capability, not by URL prefix — reinforcing that URL segments don't define API boundaries. The UDT Service and its 2026.1 Bulk Data API appear in none of the four, despite being documented, supported endpoints — so the SDK catalog is not exhaustive either.

Credit: Felipe Maurer (P21WWUG profile) — taxonomy correction and 25.1 middleware evidence in this forum topic; endpoint behavior re-verified live July 2026, SDK catalog cross-checked on 2026.1.


Overview

This document covers the stateless REST endpoints at /api/entity/ for CRUD (Create, Read, Update, Delete) operations on P21 business objects. They use domain object models and support four entities: Customer, Vendor, Contact, and Address.

The same REST API also provides /api/inventory/parts — see Inventory REST API — and an /api/sales/orders endpoint family (see Other REST Endpoint Families below).

Key Characteristics

When to Use

Limitations


Base URL

https://{hostname}/api/entity/{resource}

Examples: - https://play.p21server.com/api/entity/customers - https://play.p21server.com/api/entity/vendors - https://play.p21server.com/api/entity/contacts - https://play.p21server.com/api/entity/addresses

Warning: Customer/vendor/contact/address records are served only from /api/entity/ — category-style URLs for those records (e.g., /api/sales/customers) return 404. But do not generalize that to "category URLs don't work": /api/sales/orders exists and responds (verified July 2026 — see Other REST Endpoint Families), and /api/inventory/parts is fully functional (Inventory REST API). All of these are endpoint families of the same REST API.


Available Entities

Only four entities are available via the Entity API:

Entity Endpoint Key Format Fields
Customers /api/entity/customers {CompanyId}_{CustomerId} 102
Vendors /api/entity/vendors {CompanyId}_{VendorId} 50
Contacts /api/entity/contacts {Id} (simple numeric) 40
Addresses /api/entity/addresses {AddressId} (simple numeric) 27

Additionally, the same REST API provides inventory items at /api/inventory/parts (Inventory REST API) and sales orders at /api/sales/orders (below).

Composite Keys

Customers and Vendors require a composite key combining CompanyId and the entity ID, separated by an underscore:

/api/entity/customers/ACME_10          # CompanyId=ACME, CustomerId=10
/api/entity/vendors/ACME_28485        # CompanyId=ACME, VendorId=28485

Contacts and Addresses use simple numeric IDs:

/api/entity/contacts/1
/api/entity/addresses/10

Vendor ID vs Supplier ID

The Entity API VendorId is not the same as the OData supplier_id. These come from different database tables. To find the correct VendorId, query vendors through the Entity API or check the vendor table via OData.


Endpoints Per Entity

Customers, Vendors, Contacts

Method Path Description
GET /api/entity/{resource}/ping Health check
GET /api/entity/{resource}/new Get blank template
GET /api/entity/{resource}/{key} Get single record
GET /api/entity/{resource}/ List all records (trailing slash required)
GET /api/entity/{resource}/?$query=... Query with filter
POST /api/entity/{resource} Create (no key in body)
PUT /api/entity/{resource}/{key} Update (key in URL)

Addresses (Limited)

Addresses have a reduced set of operations — no /new template and no update:

Method Path Description
GET /api/entity/addresses/ping Health check
GET /api/entity/addresses/{addressId} Get single address
GET /api/entity/addresses/ List all addresses
GET /api/entity/addresses/?$query=... Query with filter
POST /api/entity/addresses Create new address

No /new or PUT: The address resource does not define a template endpoint or an update method in the SDK interface. The /new endpoint returns 500 because it doesn't exist (not a bug). To update an address, use the Interactive API or direct SQL.

Trailing Slash on List Endpoints

List endpoints (GET /api/entity/customers) return a 307 redirect to the same URL with a trailing slash (/api/entity/customers/). Configure your HTTP client to follow redirects:

client = httpx.Client(follow_redirects=True, ...)
var handler = new HttpClientHandler { AllowAutoRedirect = true };
var client = new HttpClient(handler);

Note: Be cautious with unfiltered list queries. The customers endpoint returned 19,896 records, contacts returned 58,639. Always use $query to filter when possible.


Authentication

Include the Bearer token in the Authorization header:

GET /api/entity/customers/ACME_10 HTTP/1.1
Host: play.p21server.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Accept: application/json

See Authentication for token generation. Per the SDK, user-credential tokens expire after 24 hours; consumer-key tokens last far longer (years).

Consumer Key Behavior

When using Consumer Key authentication with the Entity API:

Scenario Behavior
No username Uses default P21 user (admin from Web.config)
With AD username Include domain (e.g., emea\user.name)
With SQL username Use plain username (e.g., admin)

CRUD Operations

Key Rule: Presence = Update, Absence = Insert

The Entity API determines whether to insert or update based on whether key fields are present:

This applies to all entities. When creating, omit or null-out the ID field. When updating, include the key both in the URL and body.

Read (GET)

Single record by key:

GET /api/entity/customers/ACME_10
Accept: application/json

Response:

{
    "CompanyId": "ACME",
    "CustomerId": 10,
    "CustomerName": "ABC Supply Company",
    "SalesrepId": "1100",
    "TermsId": "1",
    "CreditStatus": "GOOD",
    "CreditLimit": 1.0,
    "Taxable": "Y",
    "CurrencyId": 1,
    "CustomerType": "COM",
    "CustomerAddress": null,
    "CustomerContacts": null,
    "CustomerShipTos": null,
    "UserDefinedFields": {},
    "ObjectName": "customer"
}

Note: Nested objects (CustomerAddress, CustomerContacts, etc.) are null by default. Use extendedproperties to populate them.

Create (POST)

  1. Get a template: GET /api/entity/customers/new
  2. Fill in required fields
  3. POST without key fields (absence of key = insert)
POST /api/entity/customers
Content-Type: application/json

{
    "CompanyId": "ACME",
    "CustomerName": "New Customer Inc.",
    "SalesrepId": "1100",
    "TermsId": "1",
    "CodRequiredFlag": "N",
    "Taxable": "Y"
}

Update (PUT)

Include the composite key in the URL:

PUT /api/entity/customers/ACME_10
Content-Type: application/json

{
    "CompanyId": "ACME",
    "CustomerId": 10,
    "CustomerName": "Updated Name"
}

Delete

Per the SDK, set the Delete field to true on an update:

PUT /api/entity/customers/ACME_10
Content-Type: application/json

{
    "CompanyId": "ACME",
    "CustomerId": 10,
    "Delete": true
}

Note: Dedicated DELETE HTTP method is not documented in the SDK for any entity.

Address Limitations

The Address entity has a reduced API surface compared to other entities:

To update an existing address, use the Interactive API (Address Maintenance window) or direct SQL. See Error Handling - Entity API for the specific error codes these limitations produce.


Health Check (Ping)

Each entity has a /ping endpoint for verifying the service is running:

GET /api/entity/customers/ping

Response:

{
    "ResponseMessage": "success"
}

Query Syntax

Filter results using the $query parameter on list endpoints:

GET /api/entity/customers/?$query=startswith(CustomerName, 'ABC')

Remember: List endpoints redirect (307), so use a trailing slash or enable redirect following.

Comparison Operators

Operator Description Example
eq Equal CompanyId eq 'ACME'
ne Not equal LastName ne null
gt Greater than CreditLimit gt 1000
ge Greater than or equal CreditLimit ge 1000
lt Less than CreditLimit lt 1000
le Less than or equal CreditLimit le 1000

Logical Operators

Operator Description Example
and Logical AND CreditLimit ge 1000 and CreditStatus eq 'GOOD'
or Logical OR CreditStatus eq 'GOOD' or CreditStatus eq 'HOLD'
not Logical NOT not (CustomerId eq 0)

String Functions

Function Description Example
startswith Starts with startswith(CustomerName, 'ABC')
endswith Ends with endswith(CustomerName, 'Inc.')
substringof Contains substringof('Supply', VendorName)

Verified Query Examples

# Find customers by name prefix (returned 7 results)
GET /api/entity/customers/?$query=startswith(CustomerName, 'ABC')

# Find vendors by name prefix (returned 6 results)
GET /api/entity/vendors/?$query=startswith(VendorName, 'ABC')

# Filter by company
GET /api/entity/customers/?$query=CompanyId eq 'ACME'

Extended Properties

Include related/nested data using the extendedproperties parameter:

All nested objects:

GET /api/entity/customers/ACME_10?extendedproperties=*

Specific nested object:

GET /api/entity/customers/ACME_10?extendedproperties=CustomerAddress

Available Extended Properties by Entity

Customer: - CustomerAddress - Mailing and physical address - CustomerEDITransactions - EDI configuration - CustomerSalesreps - Assigned sales reps - CustomerTerms - Payment terms - CustomerDealerTypes - Dealer type assignments - CustomerRestrictedClasses - Restricted product classes - CustomerContacts - Associated contacts - CustomerShipTos - Ship-to addresses - CustomerPriceLibraries - Price library assignments

Vendor: - VendorSuppliers - Associated supplier records - VendorAddress - Mailing and physical address - VendorPurchaseAccounts - Purchase account configuration - VendorContract - Contract information

Inventory: see the Inventory REST API extended properties reference for the full list for /api/inventory/parts.

Contact: - ContactDocuments - Associated documents - ContactLeadSources - Lead source tracking - ContactLinks - Related entity links - ContactLists - Mailing list membership - ContactSalesreps - Assigned sales reps

Example: Customer with Address

GET /api/entity/customers/ACME_10?extendedproperties=CustomerAddress
{
    "CompanyId": "ACME",
    "CustomerId": 10,
    "CustomerName": "ABC Supply Company",
    "CustomerAddress": {
        "CorpAddressId": 10,
        "MailAddress1": "123 Industrial Parkway",
        "MailCity": "Springfield",
        "MailState": "IL",
        "MailPostalCode": "62701",
        "MailCountry": "USA",
        "CentralPhoneNumber": "555-555-1234",
        "PhysAddress1": "123 Industrial Parkway",
        "PhysCity": "Springfield",
        "PhysState": "IL"
    }
}

User Defined Fields

Entity API supports User Defined Fields (UDFs) when enabled in the P21 middleware.

Enabling UDFs

  1. Go to the Administration page: https://{hostname}/docs/admin.aspx
  2. Toggle "User Defined Field Enabled Setting" to Enabled
  3. Click "Regenerate User Defined Fields" if fields are not appearing

UDF Behavior


Response Format

Single Record

{
    "CompanyId": "ACME",
    "CustomerId": 10,
    "CustomerName": "ABC Supply Company",
    "SalesrepId": "1100",
    "CreditStatus": "GOOD",
    "UserDefinedFields": {},
    "ObjectName": "customer"
}

Collection (List/Query)

[
    {
        "CompanyId": "ACME",
        "CustomerId": 10,
        "CustomerName": "ABC Supply Company"
    },
    {
        "CompanyId": "ACME",
        "CustomerId": 20,
        "CustomerName": "ABC Supply Company"
    }
]

Error Response

{
    "DateTimeStamp": "2026-02-11T15:30:00",
    "ErrorMessage": "Your query did not yield any results. No resources found for query string \"GetById 100198\".",
    "ErrorType": "ResourceNotFoundException",
    "HostName": "P21SERVER",
    "InnerException": null,
    "LogId": "...",
    "StackTrace": "...",
    "UserId": "api_user"
}

Content Types

Both JSON and XML are supported. Set via the Accept header:

Accept: application/json
Accept: application/xml

XML response example (ping):

<?xml version="1.0"?>
<PingResponse>
    <ResponseMessage>success</ResponseMessage>
</PingResponse>

Code Examples

Setup

import httpx

base_url = "https://play.p21server.com"

# Get token
token_resp = httpx.post(
    f"{base_url}/api/security/token/v2",
    json={"username": "api_user", "password": "password"},
    headers={"Accept": "application/json"},
    verify=False,
)
token = token_resp.json()["AccessToken"]

# Create client (must follow redirects for list endpoints)
client = httpx.Client(
    headers={
        "Authorization": f"Bearer {token}",
        "Accept": "application/json",
        "Content-Type": "application/json",
    },
    verify=False,
    follow_redirects=True,
)
using System.Net.Http;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

var baseUrl = "https://play.p21server.com";

// Get token
var handler = new HttpClientHandler
{
    AllowAutoRedirect = true,
    ServerCertificateCustomValidationCallback = (_, _, _, _) => true
};
var client = new HttpClient(handler) { BaseAddress = new Uri(baseUrl) };
client.DefaultRequestHeaders.Add("Accept", "application/json");

var tokenPayload = JsonConvert.SerializeObject(new
{
    username = "api_user",
    password = "password"
});
var tokenResp = await client.PostAsync(
    "/api/security/token/v2",
    new StringContent(tokenPayload, Encoding.UTF8, "application/json")
);
tokenResp.EnsureSuccessStatusCode();
var tokenJson = JObject.Parse(await tokenResp.Content.ReadAsStringAsync());
var token = tokenJson["AccessToken"]!.ToString();

// Set auth header for all subsequent requests
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");

Health Check

resp = client.get(f"{base_url}/api/entity/customers/ping")
resp.raise_for_status()
print(resp.json())  # {"ResponseMessage": "success"}
var resp = await client.GetAsync("/api/entity/customers/ping");
resp.EnsureSuccessStatusCode();
var json = JObject.Parse(await resp.Content.ReadAsStringAsync());
Console.WriteLine(json);  // {"ResponseMessage": "success"}

Get Single Customer

resp = client.get(f"{base_url}/api/entity/customers/ACME_10")
resp.raise_for_status()
customer = resp.json()
print(f"{customer['CustomerId']}: {customer['CustomerName']}")
# 10: ABC Supply Company
var resp = await client.GetAsync("/api/entity/customers/ACME_10");
resp.EnsureSuccessStatusCode();
var customer = JObject.Parse(await resp.Content.ReadAsStringAsync());
Console.WriteLine($"{customer["CustomerId"]}: {customer["CustomerName"]}");
// 10: ABC Supply Company

Get Customer with Extended Properties

resp = client.get(
    f"{base_url}/api/entity/customers/ACME_10",
    params={"extendedproperties": "CustomerAddress"},
)
resp.raise_for_status()
customer = resp.json()
addr = customer["CustomerAddress"]
print(f"{addr['MailCity']}, {addr['MailState']} {addr['MailPostalCode']}")
# Springfield, IL 62701
var resp = await client.GetAsync(
    "/api/entity/customers/ACME_10?extendedproperties=CustomerAddress"
);
resp.EnsureSuccessStatusCode();
var customer = JObject.Parse(await resp.Content.ReadAsStringAsync());
var addr = customer["CustomerAddress"]!;
Console.WriteLine($"{addr["MailCity"]}, {addr["MailState"]} {addr["MailPostalCode"]}");
// Springfield, IL 62701

Query Customers

resp = client.get(
    f"{base_url}/api/entity/customers/",
    params={"$query": "startswith(CustomerName, 'ABC')"},
)
resp.raise_for_status()
customers = resp.json()
print(f"Found {len(customers)} customers")
for c in customers:
    print(f"  {c['CompanyId']}_{c['CustomerId']}: {c['CustomerName']}")
var resp = await client.GetAsync(
    "/api/entity/customers/?$query=startswith(CustomerName, 'ABC')"
);
resp.EnsureSuccessStatusCode();
var customers = JArray.Parse(await resp.Content.ReadAsStringAsync());
Console.WriteLine($"Found {customers.Count} customers");
foreach (var c in customers)
{
    Console.WriteLine($"  {c["CompanyId"]}_{c["CustomerId"]}: {c["CustomerName"]}");
}

Get Contact

resp = client.get(f"{base_url}/api/entity/contacts/1")
resp.raise_for_status()
contact = resp.json()
print(f"{contact['FirstName']} {contact['LastName']}")
# John Smith
var resp = await client.GetAsync("/api/entity/contacts/1");
resp.EnsureSuccessStatusCode();
var contact = JObject.Parse(await resp.Content.ReadAsStringAsync());
Console.WriteLine($"{contact["FirstName"]} {contact["LastName"]}");
// John Smith

Create Customer

# Get template first
resp = client.get(f"{base_url}/api/entity/customers/new")
resp.raise_for_status()
template = resp.json()

# Fill required fields
template["CompanyId"] = "ACME"
template["CustomerName"] = "New Customer Inc."
template["SalesrepId"] = "1100"
template["TermsId"] = "1"

# Create (POST without CustomerId = insert)
resp = client.post(
    f"{base_url}/api/entity/customers",
    json=template,
)
resp.raise_for_status()
// Get template first
var resp = await client.GetAsync("/api/entity/customers/new");
resp.EnsureSuccessStatusCode();
var template = JObject.Parse(await resp.Content.ReadAsStringAsync());

// Fill required fields
template["CompanyId"] = "ACME";
template["CustomerName"] = "New Customer Inc.";
template["SalesrepId"] = "1100";
template["TermsId"] = "1";

// Create (POST without CustomerId = insert)
resp = await client.PostAsync(
    "/api/entity/customers",
    new StringContent(template.ToString(), Encoding.UTF8, "application/json")
);
resp.EnsureSuccessStatusCode();

Update Customer

resp = client.put(
    f"{base_url}/api/entity/customers/ACME_10",
    json={
        "CompanyId": "ACME",
        "CustomerId": 10,
        "CustomerName": "Updated Customer Name",
    },
)
resp.raise_for_status()
var payload = new JObject
{
    ["CompanyId"] = "ACME",
    ["CustomerId"] = 10,
    ["CustomerName"] = "Updated Customer Name"
};
var resp = await client.PutAsync(
    "/api/entity/customers/ACME_10",
    new StringContent(payload.ToString(), Encoding.UTF8, "application/json")
);
resp.EnsureSuccessStatusCode();

Template Fields (Complete Reference)

Every field for each entity is listed below, sourced directly from the /new template endpoints. Extended properties (nested objects) are listed separately from data fields.

Customer Template (102 fields)

Complete field list from GET /api/entity/customers/new:

Extended Properties (null by default, populate via extendedproperties parameter):

# Field Type Default
1 CustomerAddress null null
2 CustomerEDITransactions null null
3 CustomerSalesreps null null
4 CustomerTerms null null
5 CustomerDealerTypes null null
6 CustomerRestrictedClasses null null
7 CustomerContacts null null
8 CustomerShipTos null null
9 CustomerPriceLibraries null null

Data Fields:

# Field Type Default
10 CompanyId string ""
11 CustomerId null null
12 CustomerName string ""
13 SalesrepId string ""
14 TermsId string ""
15 CodRequiredFlag string ""
16 GenerateFinanceCharges string ""
17 FcCycle string ""
18 MinimumFinanceCharge null null
19 FinanceChargeShipToId null null
20 FcGraceDays null null
21 FcPercentage null null
22 LastFcDate null null
23 CreditStatus string ""
24 CreditLimit null null
25 CreditLimitPerOrder null null
26 CreditLimitCheckAtShipment string ""
27 OverrideRevenueByItem string ""
28 OrderAcknowledgments string ""
29 PoNoRequired string ""
30 LimitMaxShipmentsPerOrder null null
31 MinimumOrderDollarAmount null null
32 PickTicketType string ""
33 DefaultDisposition string ""
34 BillToContactId string ""
35 InvoicePrintQty int 1
36 GenerateCustomerStatements string ""
37 Taxable string ""
38 StatementBalance string "O"
39 SicCode null null
40 Class1id string ""
41 Class2id string ""
42 Class3id string ""
43 Class4id string ""
44 Class5id string ""
45 StateSalesTaxExemptionNo string ""
46 StateExciseTaxExemptionNo string ""
47 FederalExemptionNumber string ""
48 OtherExemptionNumber string ""
49 SecurityInfo string ""
50 InterchgReceiverId string ""
51 InternationalSAN string ""
52 TradingPartnerName string ""
53 PrintPricesOnPackinglist string ""
54 InvoiceBatchNumber null null
55 StatementBatchNumber null null
56 JobPricing string "N"
57 DefaultRebateLocationId null null
58 CurrencyId null null
59 OverrideProfitLimit string ""
60 MinimumOrderLineProfit null null
61 MaximumOrderLineProfit null null
62 MinimumOrderProfit null null
63 MaximumOrderProfit null null
64 DateAcctOpened null null
65 PrintPackinglistInShipping string ""
66 InclNonAllocOnTixText string ""
67 ExcludeCanceldFromPickTix string ""
68 InclNonAllocOnListText string ""
69 ExcludeCanceldFromPackList string ""
70 AlwaysUseJobPrice string ""
71 AllowNonJobItem string ""
72 PromptForNonJobItem string ""
73 AllowExceedJobQty string ""
74 PrintLotAttribOnInvoice string ""
75 PrintLotAttribOnPacklist string ""
76 OverTolerancePercentage null null
77 UnderTolerancePercentage null null
78 CustomerType string ""
79 LeadSourceId string ""
80 UseAllContracts string ""
81 JobNumberRequired string ""
82 PromiseDateBuffer null null
83 FreightChargeId string ""
84 OrderPriorityId string ""
85 UseSystemUPSHandlingCharge string ""
86 UpsHandlingCharge null null
87 DaysOverdueForCreditHold null null
88 Delete bool false
89 SendOutsideUseDocs string "N"
90 SendOutsideUsePrint string "N"
91 SendOutsideUseFax string "N"
92 SendOutsideUseEmail string "N"
93 LegacyId string ""
94 PricingMethod string ""
95 RequirePaymentUponReleaseOfItems bool false
96 CreditLimitUsed null null
97 CreditLimitAvailable null null
98 CorporateCreditLimit null null
99 CorporateCreditLimitAvailable null null
100 WebEnabledFlag string "N"
101 UserDefinedFields object {}
102 ObjectName string "customer"

Vendor Template (50 fields)

Complete field list from GET /api/entity/vendors/new:

Extended Properties (null by default, populate via extendedproperties parameter):

# Field Type Default
1 VendorSuppliers null null
2 VendorAddress null null
3 VendorPurchaseAccounts null null
4 VendorContract null null

Data Fields:

# Field Type Default
5 CompanyId string ""
6 VendorName string ""
7 CurrencyId null null
8 CurrencyDesc string ""
9 ApAccountNo string ""
10 DefaultTermsId string ""
11 DefaultInvoiceDesc string ""
12 AlwaysTakeTerms string ""
13 JobIdRequired string "N"
14 Class1id string ""
15 Class2id string ""
16 Class3id string ""
17 Class4id string ""
18 Class5id string ""
19 Incorporated string "N"
20 Default1099Type int 7
21 SecurityInfo string ""
22 InterchgReceiverId string ""
23 IntlSan string ""
24 DefaultPayFreightTo string "V"
25 GeneralLiabExpirationDate null null
26 WorkersCompExpirationDate null null
27 TradingPartnerName string ""
28 VendorId null null
29 TrackRebates string ""
30 RebateAccountNo string ""
31 RebateAllowanceAccountNo string ""
32 AttorneyFeeFlag string ""
33 Irs1099State1 string ""
34 Irs1099State1IdNo string ""
35 Irs1099State2 string ""
36 Irs1099State2IdNo string ""
37 CommissionReceivableAcct string ""
38 CommissionRevenueAcct string ""
39 CommissionAllowanceAcct string ""
40 WarrantyReceivableAcct string ""
41 WarrantyRevenueAcct string ""
42 WarrantyAllowanceAcct string ""
43 VendorTypeDesc string ""
44 IntrastatFlag string ""
45 ConsignmentCountry string ""
46 EuMemberFlag string "N"
47 Delete bool false
48 LegacyId string ""
49 UserDefinedFields object {}
50 ObjectName string "vendor"

Contact Template (40 fields)

Complete field list from GET /api/entity/contacts/new:

Extended Properties (null by default, populate via extendedproperties parameter):

# Field Type Default
1 ContactDocuments null null
2 ContactLeadSources null null
3 ContactLinks null null
4 ContactLists null null
5 ContactSalesreps null null

Data Fields:

# Field Type Default
6 Salutation string ""
7 FirstName string ""
8 Mi string ""
9 LastName string ""
10 Title string ""
11 AddressId null null
12 Mailstop string ""
13 NoOfCycleDays null null
14 Comments string ""
15 DirectPhone string ""
16 PhoneExt string ""
17 DirectFax string ""
18 FaxExt string ""
19 Beeper string ""
20 Cellular string ""
21 Class1id string ""
22 Class2id string ""
23 Class3id string ""
24 Class4id string ""
25 Class5id string ""
26 HomeAddress1 string ""
27 HomeAddress2 string ""
28 HomePhone string ""
29 HomeFax string ""
30 HomeEmailAddress string ""
31 Birthday null null
32 Anniversary null null
33 EmailAddress string ""
34 Url string ""
35 CellularExt string ""
36 Id string ""
37 DeleteFlag string "N"
38 LoginId string ""
39 UserDefinedFields object {}
40 ObjectName string "contacts"

Address Fields (27 fields)

Note: The Address resource does not have a /new template endpoint (this is by design, not a bug). The field list below is from an existing address record (GET /api/entity/addresses/10).

Complete field list:

# Field Type Description
1 CorpAddressId int Corporate address ID
2 MailAddress1 string Mailing address line 1
3 MailAddress2 string Mailing address line 2
4 MailAddress3 string Mailing address line 3
5 MailCity string Mailing city
6 MailState string Mailing state
7 MailPostalCode string Mailing postal code
8 MailCountry string Mailing country
9 CentralPhoneNumber string Main phone number
10 CentralFaxNumber string Main fax number
11 Alternative1099Name string Alternative name for 1099
12 NameControl string Name control
13 PhysAddress1 string Physical address line 1
14 PhysAddress2 string Physical address line 2
15 PhysAddress3 string Physical address line 3
16 PhysCity string Physical city
17 PhysState string Physical state
18 PhysPostalCode string Physical postal code
19 PhysCountry string Physical country
20 Incorporated string Incorporated flag (Y/N)
21 EmailAddress string Email address
22 Url string Website URL
23 AddressId int Address ID (key)
24 Name string Address/company name
25 PhysCounty string Physical county
26 UserDefinedFields object UDF container
27 ObjectName string Always "address"

Additional Endpoints

SOAP Endpoints

In addition to the REST endpoints documented above, the Entity API also exposes SOAP web services:

Version Endpoint Pattern Example
SOAP v1 /api/entity/{Entity}Service /api/entity/CustomerService
SOAP v2 /api/entity/v2/{Entity}Service /api/entity/v2/CustomerService

Available for: Customer, Vendor, Contact, Address. Use these if your integration platform prefers SOAP over REST.

Mobile Endpoints

The P21 middleware also exposes mobile-specific entity endpoints with additional entities not available via the standard REST API:

Endpoint Entity
mobile/entity/customers Customers
mobile/entity/vendors Vendors
mobile/entity/contacts Contacts
mobile/entity/suppliers Suppliers (not in REST API)
mobile/entity/users Users (not in REST API)
mobile/entity/companies Companies (not in REST API)

Note: The mobile endpoints are designed for the P21 mobile application. Their request/response format may differ from the REST API. Use with caution for custom integrations.

Endpoint Discovery

You can browse all available Entity API endpoints from the P21 middleware home page:

https://{hostname}/docs/

This lists every registered endpoint including REST, SOAP, and mobile resources.


Error Codes

Code Name Description
200 OK Request processed successfully
202 Accepted Async request acknowledged (for async endpoints)
301 Moved Permanently Resource moved — check Location header
307 Temporary Redirect List endpoint without trailing slash — follow redirect
400 Bad Request Invalid parameters or data — check server logs
401 Unauthorized Invalid token, expired token, or user marked as deleted in P21
404 Not Found Resource doesn't exist, wrong key format, or wrong URL pattern
5xx Server Error Server-side error — check p21soa.log and p21api.log

Log File Locations

When troubleshooting 400/500 errors, check the server-side log files:

Log Purpose
p21soa.log P21 SOA Architecture (routing, auth, middleware)
p21api.log P21 Core Business Logic (validation, data operations)

Other REST Endpoint Families

The REST API exposes more than /api/entity/ and /api/inventory/parts. Verified against a 25.2 tenant (July 2026):

/api/sales/orders — exists and responds

Call Result
GET /api/sales/orders/ping 200
GET /api/sales/orders/new 200 — full order template (Lines, Notes, Salesreps, BuilderSelectionSheets, Samples, …)
GET /api/sales/orders/{order_no} 200 — ~70 top-level fields
GET /api/sales/orders/{order_no}/approve 405 — route exists; the middleware documents PUT for this action (PUT untested)
GET /api/sales/orders/?$query=... 500 with an unrecognized field name — list query syntax not yet discovered

Writes and the approve action are untested — treat this family as read-verified only until documented further.

Families that 404 on the tested tenant

sales/customers, sales/quotes, sales/invoices, sales/shipments, sales/payments, purchasing/*, ar/*, inventory/items, inventory/lots — all returned 404 on ping. Availability may vary by version; the SOA middleware admin site lists the endpoint families your server actually exposes.

Credit: Felipe Maurer (P21WWUG profile) — surfaced /api/sales/orders and the middleware endpoint listing in this forum topic.


Common Errors

Error Cause Solution
307 Redirect List endpoint without trailing slash Add trailing slash or enable follow_redirects
401 Unauthorized Invalid/expired token Refresh token
404 "No resources found" Wrong key format or record doesn't exist Use composite key (ACME_10) for customers/vendors
404 HTML page Endpoint doesn't exist Verify you're using /api/entity/ base path
500 Server Error Internal error (e.g., addresses /new) Try a different approach or entity

Common Mistakes

  1. Wrong URL pattern for the 4 entities - Use /api/entity/customers, NOT /api/sales/customers
  2. Simple ID for customers - Use ACME_10, NOT just 10
  3. Missing redirect handling - List endpoints return 307, must follow redirect
  4. Confusing VendorId with supplier_id - These are different database tables
  5. Expecting orders/items at /api/entity/ - Only 4 entities exist there (customers, vendors, contacts, addresses). Inventory uses /api/inventory/parts (Inventory REST API); orders use /api/sales/orders (Other REST Endpoint Families)

Entity API vs Other APIs

Feature Entity API OData Transaction Interactive
Operations CRUD Read-only Bulk CRUD Stateful CRUD
Entities 4 (see also Inventory REST API) Any table/view Many services Any P21 window
Format Domain objects Table rows XML payloads Window fields
Session Stateless Stateless Stateless Stateful
Queries $query $filter (OData) N/A N/A
Extended data extendedproperties N/A N/A Tab navigation
Best for Customer/vendor CRUD Reporting, lookups Bulk operations Complex workflows