Supplier Portal Access¶
This guide shows how to give a supplier read-only visibility into their own purchase orders - and nothing else. It uses a dedicated role with a Procurement / View privilege and a granular filter that restricts list results to a single supplier number.
When the supplier logs in and calls GET /api/purchase-order, they see only POs where Supplier Number = SUP-007. No other data is exposed.
How Granular Filters Work¶
Every role can carry a Granular Privileges object alongside its standard privilege list:
| Key | Description |
|---|---|
include |
Only records matching these values are returned |
exclude |
Records matching these values are hidden |
Each entry is a list of [id, display_text] pairs, where id is the internal database ID of the referenced record (e.g. the supplier's integer id from the Supplier Master, not the Supplier Number string).
Prerequisites¶
| Requirement | Detail |
|---|---|
| Auth | Active session + CSRF token |
| Privilege - manage roles | Users / Edit |
Step 1 - Look Up the Supplier's Internal ID¶
The granular filter requires the supplier's integer id from the Supplier Master. Fetch it by filtering on the supplier number:
Response:
{
"data": {
"rows": [
{
"id": 42,
"Supplier Number": "SUP-007",
"Supplier Name": "Acme Supplies Ltd."
}
]
}
}
Note the id - 42 in this example. This is the value used inside the granular filter, not the "SUP-007" string.
Step 2 - Create the Supplier Role¶
Create a role with:
Procurement / View- read-only access to purchase orders- A granular include filter on
Supplier Numberset to the supplier's internal ID
CSRF=$(grep csrf_access_token cookies.txt | awk '{print $NF}')
curl -b cookies.txt -X POST \
"https://acme.knosc.com/api/role" \
-H "Content-Type: application/json" \
-H "X-XSRF-TOKEN: $CSRF" \
-d '{
"Role Name": "Supplier Portal - Acme Supplies",
"Role Description": "Read-only procurement access restricted to Acme Supplies orders",
"Privileges": [
{
"Privilege Type": "Procurement",
"Privilege Description": "Read-only purchase order access",
"Privilege Access": "View"
}
],
"Granular Privileges": {
"include": {
"Supplier Number": [[42, "SUP-007"]]
},
"exclude": {}
}
}'
Response:
Note the role id - you need it for the next step.
Step 3 - Verify the Role¶
Fetch the role to confirm the privilege and granular filter were saved correctly:
Response:
{
"data": {
"id": 12,
"Role Name": "Supplier Portal - Acme Supplies",
"Role Description": "Read-only procurement access restricted to Acme Supplies orders",
"Privileges": [
{
"id": 55,
"Privilege Type": "Procurement",
"Privilege Description": "Read-only purchase order access",
"Privilege Access": "View"
}
],
"Granular Filters": [
{
"category": "Supplier Number",
"id": 42,
"text": "SUP-007",
"type": "autocomplete",
"exclude": false
}
]
}
}
"exclude": false confirms this is an include filter - only SUP-007 records are visible.
Step 4 - Create a User Account for the Supplier¶
Assign the new role when creating the user:
curl -b cookies.txt -X POST \
"https://acme.knosc.com/api/user" \
-H "Content-Type: application/json" \
-H "X-XSRF-TOKEN: $CSRF" \
-d '{
"User Fullname": "James Obi (Acme Supplies)",
"Username": "james.obi@acmesupplies.com",
"User Email": "james.obi@acmesupplies.com",
"Role Id": 12,
"password": "TempPass2024!"
}'
Response:
Step 5 - Test the Filtered View¶
Log in as the supplier user and call the purchase order list endpoint:
# Login as the supplier user
curl -c supplier-cookies.txt -X POST \
"https://acme.knosc.com/api/authenticate" \
-H "Content-Type: application/json" \
-d '{
"username": "james.obi@acmesupplies.com",
"password": "TempPass2024!"
}'
# Fetch purchase orders
curl -b supplier-cookies.txt "https://acme.knosc.com/api/purchase-order"
The response contains only POs where Supplier Number = SUP-007. Attempting to request another supplier's PO directly by ID returns 403 User.NotPrivileged.
What the Supplier Can and Cannot Do¶
| Action | Allowed |
|---|---|
GET /api/purchase-order (their POs only) |
Yes |
GET /api/purchase-order/{id} (their POs only) |
Yes |
GET /api/purchase-order/{id} (another supplier's PO) |
No - 403 |
POST /api/purchase-order |
No - View privilege only |
PUT /api/purchase-order/{id} |
No - View privilege only |
DELETE /api/purchase-order/{id} |
No - View privilege only |
| Any other module (Sales, Inventory, Users, etc.) | No - no privilege granted |
Supporting Multiple Suppliers on One Role¶
To create a shared role for a group of suppliers (e.g. a 3PL that manages multiple), add multiple entries to the include array:
"Granular Privileges": {
"include": {
"Supplier Number": [
[42, "SUP-007"],
[61, "SUP-019"],
[78, "SUP-034"]
]
},
"exclude": {}
}
All three suppliers' POs will be visible to users assigned this role.
Updating the Filter - Adding a New Supplier¶
Use PUT /api/role/{id} and include the full updated Granular Privileges object. The Privileges array must also be included in full - any privilege omitted will be removed.
First fetch the current role to avoid losing existing data:
Then update with the new supplier added:
curl -b cookies.txt -X PUT \
"https://acme.knosc.com/api/role/12" \
-H "Content-Type: application/json" \
-H "X-XSRF-TOKEN: $CSRF" \
-d '{
"id": 12,
"Role Name": "Supplier Portal - Acme Supplies",
"Role Description": "Read-only procurement access restricted to Acme Supplies orders",
"Privileges": [
{
"id": 55,
"Privilege Type": "Procurement",
"Privilege Description": "Read-only purchase order access",
"Privilege Access": "View"
}
],
"Granular Privileges": {
"include": {
"Supplier Number": [
[42, "SUP-007"],
[61, "SUP-019"]
]
},
"exclude": {}
}
}'
Important:
PUT /api/role/{id}replaces the full privilege and granular filter configuration. Always read the role first and include all existing privileges in the body to avoid unintentional removal.
Python - Full Setup Script¶
BASE_URL = "https://acme.knosc.com/api"
def setup_supplier_role(client, supplier_number: str) -> dict:
"""
Create a supplier portal role restricted to a single supplier.
Returns {"role_id": int, "supplier_id": int, "supplier_name": str}
"""
# 1. Resolve supplier internal ID
result = client.get(f"/supplier-master?filter[Supplier Number]={supplier_number}")
rows = result["data"]["rows"]
if not rows:
raise ValueError(f"Supplier not found: {supplier_number}")
supplier = rows[0]
supplier_id = supplier["id"]
supplier_name = supplier["Supplier Name"]
# 2. Create role with granular filter
role_body = {
"Role Name": f"Supplier Portal - {supplier_name}",
"Role Description": f"Read-only procurement access restricted to {supplier_name} orders",
"Privileges": [
{
"Privilege Type": "Procurement",
"Privilege Description": "Read-only purchase order access",
"Privilege Access": "View",
}
],
"Granular Privileges": {
"include": {
"Supplier Number": [[supplier_id, supplier_number]]
},
"exclude": {},
},
}
role_result = client.post("/role", role_body)
role_id = role_result["id"]
print(f"Role created: id={role_id} for {supplier_name} ({supplier_number})")
return {"role_id": role_id, "supplier_id": supplier_id, "supplier_name": supplier_name}
def provision_supplier_user(client, fullname: str, email: str, role_id: int, temp_password: str) -> int:
"""Create a user account for a supplier contact."""
result = client.post("/user", {
"User Fullname": fullname,
"Username": email,
"User Email": email,
"Role Id": role_id,
"password": temp_password,
})
user_id = result["id"]
print(f"User created: {email} (id={user_id})")
return user_id
# Example usage
role = setup_supplier_role(client, "SUP-007")
provision_supplier_user(client, "James Obi", "james.obi@acmesupplies.com", role["role_id"], "TempPass2024!")
Recommended Role Naming Convention¶
For clarity when managing many suppliers, use a consistent naming pattern:
| Pattern | Example |
|---|---|
Supplier Portal - {Supplier Name} |
Supplier Portal - Acme Supplies |
| One role per supplier | Easier to revoke access for a specific supplier without affecting others |
| One shared role per supplier group | Suitable when a 3PL or agent manages multiple supplier accounts |