The crm module serves customers, customer parts and their revisions, customer RFQs with lines, line materials and packages, and the CRM inbox. All 57 endpoints sit under /api/v1/crm. For business meaning see the CRM module page.
Group facts¶
| Item | Value | Source |
|---|---|---|
| Base path | /api/v1/crm |
src/cluster_erp/core/module_base.py:37-38, src/cluster_erp/modules/crm/module.py:65 |
| Routers | 7 router objects in 7 files; listed in the section Endpoint summary | src/cluster_erp/modules/crm/module.py:39 |
| Endpoints | 57, counted from the route decorators and add_api_route calls of the routers that register_routes mounts |
src/cluster_erp/modules/crm/customer_part_revisions/routes.py, src/cluster_erp/modules/crm/customer_parts/routes.py, src/cluster_erp/modules/crm/customers/routes.py ... |
| Authentication | Bearer header or session cookie, resolved by get_current_user; see Authentication and headers |
src/cluster_erp/modules/auth/guards.py:85-167 |
| Permission guard | 57 of 57 routes declare require_permission |
src/cluster_erp/modules/auth/guards.py:205-240 |
| App entitlement gate | require_app("crm") on every mounted router |
src/cluster_erp/modules/crm/module.py:74 |
| Error format | JSON body {"error": {...}}; see Errors |
src/cluster_erp/server/error_handler.py:33-46 |
Endpoint summary¶
One row per sub-module folder. The router prefix is added to the base path.
| Sub-module | Routers | Endpoints | Router source |
|---|---|---|---|
customers |
router |
6 | src/cluster_erp/modules/crm/customers/routes.py:28 |
customer_parts |
router |
6 | src/cluster_erp/modules/crm/customer_parts/routes.py:27 |
customer_part_revisions |
router |
7 | src/cluster_erp/modules/crm/customer_part_revisions/routes.py:21 |
rfqs |
router |
18 | src/cluster_erp/modules/crm/rfqs/routes.py:36 |
rfq_line_items |
router |
6 | src/cluster_erp/modules/crm/rfq_line_items/routes.py:20 |
rfq_line_materials |
router |
6 | src/cluster_erp/modules/crm/rfq_line_materials/routes.py:26 |
inbox |
router |
8 | src/cluster_erp/modules/crm/inbox/routes.py:22 |
Customers endpoints¶
Customer records. Routers in this module have no prefix of their own; each route path starts with its resource segment.
| Method | Path | Handler | Permission | Request | Query | Response | Status | Source |
|---|---|---|---|---|---|---|---|---|
GET |
/api/v1/crm/customers/ |
list_customers |
customer.read |
none | search, active_only, party_id, limit, offset |
dict |
200 (default) | src/cluster_erp/modules/crm/customers/routes.py:37 |
POST |
/api/v1/crm/customers/ |
create_customer |
customer.create |
CustomerCreate |
none | CustomerRead |
201 |
src/cluster_erp/modules/crm/customers/routes.py:73 |
GET |
/api/v1/crm/customers/{customer_id} |
get_customer |
customer.read |
none | none | CustomerRead |
200 (default) | src/cluster_erp/modules/crm/customers/routes.py:92 |
PUT |
/api/v1/crm/customers/{customer_id} |
update_customer |
customer.update |
CustomerUpdate |
none | CustomerRead |
200 (default) | src/cluster_erp/modules/crm/customers/routes.py:113 |
DELETE |
/api/v1/crm/customers/{customer_id} |
delete_customer |
customer.delete |
none | none | none | 204 |
src/cluster_erp/modules/crm/customers/routes.py:135 |
POST |
/api/v1/crm/customers/bulk-delete |
bulk_delete_customers |
customer.bulk_delete |
BulkDeleteRequest |
none | BulkDeleteResponse |
200 (default) | src/cluster_erp/modules/crm/customers/routes.py:147 |
Customer parts endpoints¶
Customer part records.
| Method | Path | Handler | Permission | Request | Query | Response | Status | Source |
|---|---|---|---|---|---|---|---|---|
GET |
/api/v1/crm/customer-parts/ |
list_customer_parts |
customer_part.read |
none | customer_id, rfq_id, search, active_only, limit, offset |
dict |
200 (default) | src/cluster_erp/modules/crm/customer_parts/routes.py:47 |
POST |
/api/v1/crm/customer-parts/ |
create_customer_part |
customer_part.create |
CustomerPartCreate |
none | CustomerPartRead |
201 |
src/cluster_erp/modules/crm/customer_parts/routes.py:88 |
GET |
/api/v1/crm/customer-parts/{customer_part_id} |
get_customer_part |
customer_part.read |
none | none | CustomerPartRead |
200 (default) | src/cluster_erp/modules/crm/customer_parts/routes.py:106 |
PUT |
/api/v1/crm/customer-parts/{customer_part_id} |
update_customer_part |
customer_part.update |
CustomerPartUpdate |
none | CustomerPartRead |
200 (default) | src/cluster_erp/modules/crm/customer_parts/routes.py:125 |
DELETE |
/api/v1/crm/customer-parts/{customer_part_id} |
delete_customer_part |
customer_part.delete |
none | none | none | 204 |
src/cluster_erp/modules/crm/customer_parts/routes.py:145 |
POST |
/api/v1/crm/customer-parts/bulk-delete |
bulk_delete_customer_parts |
customer_part.bulk_delete |
BulkDeleteRequest |
none | BulkDeleteResponse |
200 (default) | src/cluster_erp/modules/crm/customer_parts/routes.py:157 |
Customer part revisions endpoints¶
Revisions under a customer part, with activate and supersede.
| Method | Path | Handler | Permission | Request | Query | Response | Status | Source |
|---|---|---|---|---|---|---|---|---|
GET |
/api/v1/crm/customer-parts/{customer_part_id}/revisions |
list_revisions |
customer_part_revision.read |
none | none | list[CustomerPartRevisionRead] |
200 (default) | src/cluster_erp/modules/crm/customer_part_revisions/routes.py:24 |
POST |
/api/v1/crm/customer-parts/{customer_part_id}/revisions |
create_revision |
customer_part_revision.create |
CustomerPartRevisionCreate |
none | CustomerPartRevisionRead |
201 |
src/cluster_erp/modules/crm/customer_part_revisions/routes.py:39 |
GET |
/api/v1/crm/customer-part-revisions/{revision_id} |
get_revision |
customer_part_revision.read |
none | none | CustomerPartRevisionRead |
200 (default) | src/cluster_erp/modules/crm/customer_part_revisions/routes.py:56 |
PUT |
/api/v1/crm/customer-part-revisions/{revision_id} |
update_revision |
customer_part_revision.update |
CustomerPartRevisionUpdate |
none | CustomerPartRevisionRead |
200 (default) | src/cluster_erp/modules/crm/customer_part_revisions/routes.py:73 |
DELETE |
/api/v1/crm/customer-part-revisions/{revision_id} |
delete_revision |
customer_part_revision.delete |
none | none | none | 204 |
src/cluster_erp/modules/crm/customer_part_revisions/routes.py:91 |
POST |
/api/v1/crm/customer-part-revisions/{revision_id}/activate |
activate_revision |
customer_part_revision.activate |
none | none | CustomerPartRevisionRead |
200 (default) | src/cluster_erp/modules/crm/customer_part_revisions/routes.py:103 |
POST |
/api/v1/crm/customer-part-revisions/{revision_id}/supersede |
supersede_revision |
customer_part_revision.activate |
none | none | CustomerPartRevisionRead |
200 (default) | src/cluster_erp/modules/crm/customer_part_revisions/routes.py:118 |
Customer RFQs endpoints¶
RFQ records, completeness check, assign, quote, decline, expire, win, lose, reopen, and the RFQ package.
| Method | Path | Handler | Permission | Request | Query | Response | Status | Source |
|---|---|---|---|---|---|---|---|---|
GET |
/api/v1/crm/rfqs/ |
list_rfqs |
rfq.read |
none | search, status, customer_id, limit, offset |
dict |
200 (default) | src/cluster_erp/modules/crm/rfqs/routes.py:51 |
POST |
/api/v1/crm/rfqs/ |
create_rfq |
rfq.create |
RfqCreate |
none | RfqRead |
201 |
src/cluster_erp/modules/crm/rfqs/routes.py:85 |
GET |
/api/v1/crm/rfqs/{rfq_id} |
get_rfq |
rfq.read |
none | none | RfqRead |
200 (default) | src/cluster_erp/modules/crm/rfqs/routes.py:97 |
PUT |
/api/v1/crm/rfqs/{rfq_id} |
update_rfq |
rfq.update |
RfqUpdate |
none | RfqRead |
200 (default) | src/cluster_erp/modules/crm/rfqs/routes.py:111 |
DELETE |
/api/v1/crm/rfqs/{rfq_id} |
delete_rfq |
rfq.delete |
none | none | none | 204 |
src/cluster_erp/modules/crm/rfqs/routes.py:126 |
POST |
/api/v1/crm/rfqs/bulk-delete |
bulk_delete_rfqs |
rfq.bulk_delete |
BulkDeleteRequest |
none | BulkDeleteResponse |
200 (default) | src/cluster_erp/modules/crm/rfqs/routes.py:138 |
GET |
/api/v1/crm/rfqs/{rfq_id}/completeness |
get_rfq_completeness |
rfq.read |
none | none | dict |
200 (default) | src/cluster_erp/modules/crm/rfqs/routes.py:153 |
POST |
/api/v1/crm/rfqs/{rfq_id}/assign |
assign_rfq |
rfq.assign |
RfqAssign |
none | RfqRead |
200 (default) | src/cluster_erp/modules/crm/rfqs/routes.py:170 |
POST |
/api/v1/crm/rfqs/{rfq_id}/quote |
quote_rfq |
rfq.quote |
RfqTransition |
none | RfqRead |
200 (default) | src/cluster_erp/modules/crm/rfqs/routes.py:188 |
POST |
/api/v1/crm/rfqs/{rfq_id}/decline |
decline_rfq |
rfq.decline |
RfqDecline |
none | RfqRead |
200 (default) | src/cluster_erp/modules/crm/rfqs/routes.py:201 |
POST |
/api/v1/crm/rfqs/{rfq_id}/expire |
expire_rfq |
rfq.expire |
RfqTransition |
none | RfqRead |
200 (default) | src/cluster_erp/modules/crm/rfqs/routes.py:214 |
POST |
/api/v1/crm/rfqs/{rfq_id}/win |
win_rfq |
rfq.win |
RfqTransition |
none | RfqRead |
200 (default) | src/cluster_erp/modules/crm/rfqs/routes.py:227 |
POST |
/api/v1/crm/rfqs/{rfq_id}/lose |
lose_rfq |
rfq.lose |
RfqLose |
none | RfqRead |
200 (default) | src/cluster_erp/modules/crm/rfqs/routes.py:240 |
POST |
/api/v1/crm/rfqs/{rfq_id}/reopen |
reopen_rfq |
rfq.reopen |
RfqTransition |
none | RfqRead |
200 (default) | src/cluster_erp/modules/crm/rfqs/routes.py:253 |
GET |
/api/v1/crm/rfqs/{rfq_id}/rfq-package |
get_rfq_package |
rfq_package.read |
none | none | RfqPackageRead \| None |
200 (default) | src/cluster_erp/modules/crm/rfqs/routes.py:269 |
POST |
/api/v1/crm/rfqs/{rfq_id}/rfq-package |
create_rfq_package |
rfq_package.create |
RfqPackageCreate |
none | RfqPackageRead |
201 |
src/cluster_erp/modules/crm/rfqs/routes.py:283 |
PUT |
/api/v1/crm/rfq-packages/{package_id} |
update_rfq_package |
rfq_package.update |
RfqPackageUpdate |
none | RfqPackageRead |
200 (default) | src/cluster_erp/modules/crm/rfqs/routes.py:300 |
DELETE |
/api/v1/crm/rfq-packages/{package_id} |
delete_rfq_package |
rfq_package.delete |
none | none | none | 204 |
src/cluster_erp/modules/crm/rfqs/routes.py:318 |
RFQ line items endpoints¶
Line items under an RFQ.
| Method | Path | Handler | Permission | Request | Query | Response | Status | Source |
|---|---|---|---|---|---|---|---|---|
GET |
/api/v1/crm/rfqs/{rfq_id}/line-items |
list_line_items |
rfq_line_item.read |
none | search |
list[RfqLineItemRead] |
200 (default) | src/cluster_erp/modules/crm/rfq_line_items/routes.py:23 |
POST |
/api/v1/crm/rfqs/{rfq_id}/line-items |
create_line_item |
rfq_line_item.create |
RfqLineItemCreate |
none | RfqLineItemRead |
201 |
src/cluster_erp/modules/crm/rfq_line_items/routes.py:39 |
GET |
/api/v1/crm/line-items/{line_item_id} |
get_line_item |
rfq_line_item.read |
none | none | RfqLineItemRead |
200 (default) | src/cluster_erp/modules/crm/rfq_line_items/routes.py:56 |
PUT |
/api/v1/crm/line-items/{line_item_id} |
update_line_item |
rfq_line_item.update |
RfqLineItemUpdate |
none | RfqLineItemRead |
200 (default) | src/cluster_erp/modules/crm/rfq_line_items/routes.py:73 |
DELETE |
/api/v1/crm/line-items/{line_item_id} |
delete_line_item |
rfq_line_item.delete |
none | none | none | 204 |
src/cluster_erp/modules/crm/rfq_line_items/routes.py:91 |
POST |
/api/v1/crm/line-items/bulk-delete |
bulk_delete_line_items |
rfq_line_item.bulk_delete |
BulkDeleteRequest |
none | BulkDeleteResponse |
200 (default) | src/cluster_erp/modules/crm/rfq_line_items/routes.py:103 |
RFQ line materials endpoints¶
Materials under a line item, including an import from a BOM.
| Method | Path | Handler | Permission | Request | Query | Response | Status | Source |
|---|---|---|---|---|---|---|---|---|
GET |
/api/v1/crm/line-items/{line_item_id}/materials |
list_line_materials |
rfq_line_material.read |
none | none | list[RfqLineMaterialRead] |
200 (default) | src/cluster_erp/modules/crm/rfq_line_materials/routes.py:29 |
POST |
/api/v1/crm/line-items/{line_item_id}/materials |
create_line_material |
rfq_line_material.create |
RfqLineMaterialCreate |
none | RfqLineMaterialRead |
201 |
src/cluster_erp/modules/crm/rfq_line_materials/routes.py:44 |
POST |
/api/v1/crm/line-items/{line_item_id}/materials/from-bom |
import_line_materials_from_bom |
rfq_line_material.create |
none | none | dict |
200 (default) | src/cluster_erp/modules/crm/rfq_line_materials/routes.py:65 |
PUT |
/api/v1/crm/line-materials/{material_id} |
update_line_material |
rfq_line_material.update |
RfqLineMaterialUpdate |
none | RfqLineMaterialRead |
200 (default) | src/cluster_erp/modules/crm/rfq_line_materials/routes.py:86 |
DELETE |
/api/v1/crm/line-materials/{material_id} |
delete_line_material |
rfq_line_material.delete |
none | none | none | 204 |
src/cluster_erp/modules/crm/rfq_line_materials/routes.py:108 |
POST |
/api/v1/crm/line-materials/bulk-delete |
bulk_delete_line_materials |
rfq_line_material.delete |
BulkDeleteRequest |
none | BulkDeleteResponse |
200 (default) | src/cluster_erp/modules/crm/rfq_line_materials/routes.py:123 |
Inbox endpoints¶
Inbox messages, manual match to a record, and spam marking.
| Method | Path | Handler | Permission | Request | Query | Response | Status | Source |
|---|---|---|---|---|---|---|---|---|
GET |
/api/v1/crm/inbox/ |
list_inbox_messages |
inbox_message.read |
none | search, match_status, rfq_id, limit, offset |
dict |
200 (default) | src/cluster_erp/modules/crm/inbox/routes.py:25 |
POST |
/api/v1/crm/inbox/ |
create_inbox_message |
inbox_message.create |
InboxMessageCreate |
none | InboxMessageRead |
201 |
src/cluster_erp/modules/crm/inbox/routes.py:51 |
GET |
/api/v1/crm/inbox/{message_id} |
get_inbox_message |
inbox_message.read |
none | none | InboxMessageRead |
200 (default) | src/cluster_erp/modules/crm/inbox/routes.py:63 |
PUT |
/api/v1/crm/inbox/{message_id} |
update_inbox_message |
inbox_message.update |
InboxMessageUpdate |
none | InboxMessageRead |
200 (default) | src/cluster_erp/modules/crm/inbox/routes.py:77 |
DELETE |
/api/v1/crm/inbox/{message_id} |
delete_inbox_message |
inbox_message.delete |
none | none | none | 204 |
src/cluster_erp/modules/crm/inbox/routes.py:92 |
POST |
/api/v1/crm/inbox/bulk-delete |
bulk_delete_inbox_messages |
inbox_message.bulk_delete |
BulkDeleteRequest |
none | BulkDeleteResponse |
200 (default) | src/cluster_erp/modules/crm/inbox/routes.py:104 |
POST |
/api/v1/crm/inbox/{message_id}/match |
manual_match_message |
inbox_message.match |
InboxManualMatch |
none | InboxMessageRead |
200 (default) | src/cluster_erp/modules/crm/inbox/routes.py:119 |
POST |
/api/v1/crm/inbox/{message_id}/spam |
mark_spam |
inbox_message.spam |
none | none | InboxMessageRead |
200 (default) | src/cluster_erp/modules/crm/inbox/routes.py:134 |
Error codes¶
The table lists the error codes that code under src/cluster_erp/modules/crm/ raises, found by a syntax-tree scan for AppError(...), the factories in src/cluster_erp/core/errors.py (not_found, conflict, protected, permission_denied, privilege_escalation, check_version), and HTTPException(...). A status of 400 is the AppError default (src/cluster_erp/core/errors.py:23). Guard errors (401, 403), the entitlement codes, and validation errors (422) apply to every route and are listed on Errors.
| Code | HTTP status | Raised in | First site |
|---|---|---|---|
CONCURRENCY_VERSION_CONFLICT |
409 |
rfq_line_materials, rfqs |
src/cluster_erp/modules/crm/rfq_line_materials/services.py:161 |
CRM_RFQ_BOM_MATERIALS_EMPTY |
422 |
rfq_line_materials |
src/cluster_erp/modules/crm/rfq_line_materials/services.py:119 |
CRM_RFQ_LINE_NO_PRODUCT |
422 |
rfq_line_materials |
src/cluster_erp/modules/crm/rfq_line_materials/services.py:108 |
RESOURCE_CONFLICT |
409 |
rfqs |
src/cluster_erp/modules/crm/rfqs/services.py:576 |
RESOURCE_NOT_FOUND |
404 |
customer_part_revisions, customer_parts, customers, inbox, rfq_line_items, rfq_line_materials, rfqs |
src/cluster_erp/modules/crm/customer_part_revisions/routes.py:69 |
RFQ_ARIBA_DUE_DATE_IMMUTABLE |
400 |
rfqs |
src/cluster_erp/modules/crm/rfqs/services.py:225 |
RFQ_DUPLICATE_SUBJECT |
409 |
rfqs |
src/cluster_erp/modules/crm/rfqs/services.py:521 |
RFQ_INCOMPLETE |
409 |
rfqs |
src/cluster_erp/modules/crm/rfqs/services.py:340 |
RFQ_INVALID_STATUS |
409 |
rfqs |
src/cluster_erp/modules/crm/rfqs/services.py:291 |
Events emitted¶
The table lists the contract classes that code under src/cluster_erp/modules/crm/ passes directly to EventBus.emit, found by a syntax-tree scan. Events built in a variable first are not counted. The contracts are described in Contracts reference.
| Contract | Sites | First site |
|---|---|---|
CustomerCreated |
1 | src/cluster_erp/modules/crm/customers/services.py:118 |
CustomerUpdated |
1 | src/cluster_erp/modules/crm/customers/services.py:186 |
PartyRoleSet |
2 | src/cluster_erp/modules/crm/customers/services.py:137 |
RfqAssigned |
1 | src/cluster_erp/modules/crm/rfqs/services.py:321 |
RfqDeclined |
1 | src/cluster_erp/modules/crm/rfqs/services.py:434 |
RfqExpired |
1 | src/cluster_erp/modules/crm/rfqs/services.py:451 |
RfqLost |
1 | src/cluster_erp/modules/crm/rfqs/services.py:487 |
RfqPackageCompleted |
1 | src/cluster_erp/modules/crm/rfqs/services.py:612 |
RfqReceived |
1 | src/cluster_erp/modules/crm/rfqs/services.py:192 |
RfqUpdated |
1 | src/cluster_erp/modules/crm/rfqs/services.py:253 |
RfqWon |
1 | src/cluster_erp/modules/crm/rfqs/services.py:468 |
Related pages¶
Verified against commit 92f8051 on 2026-11-13.