MedBridge Sales Ops — Quote & Engagement Reconciliation
Overview
The MedBridge Sales Ops API serves shared CRM, product-catalog, quoting, freight, invoice, payment, revenue-recognition, event, and voucher data. Use it to build quote decision packages and account-ready reconciliations from a single source of truth. Every response field that names an ID, code, date, amount, or controlled status must come from API records — never invent identifiers or infer statuses from task narrative alone.
API conventions
Base URL
The task runner provides the base URL. All endpoints are read-only GET under
/api/.
Collections
| Endpoint | Key entity |
|---|---|
/api/customers[/<id>] |
Customer record |
/api/products[/<code>] |
Product with price tiers |
/api/quotes[/<id>] |
Quote header and line items |
/api/rfqs[/<id>] |
RFQ header and requested modules |
/api/freight-quotes |
All freight records (filter by quote_id) |
/api/policies |
Business rules |
/api/opportunities[/<id>] |
CRM opportunity with phases |
/api/invoices |
Invoices (filter by opportunity_id or customer_id) |
/api/payments |
Payment records |
/api/revenue-journals |
Revenue recognition journal entries |
/api/events |
Client events |
/api/vouchers |
Event voucher/discount codes |
/api/search?q=<text> |
Full-text search across all collections |
Quote workflows
1. Resolve the customer
Fetch the customer by the customer_id embedded in the quote or RFQ record.
Payment terms are decided by the customer's payment_profile and the
applicable policy — see Payment terms below.
2. Select the product and price tier
Fetch the product by product_code. Each product has a price_tiers array.
Pick the tier where confirmed_quantity falls in [min_qty, max_qty]. When
max_qty is null, the tier is unbounded above.
Tier fields to capture:
unit_price_usd— always from the matched tier, NOT from a prior quote'sprior_unit_price_usd(the quote'ssource_notesor catalog tier overrides any prior price)lead_time_days— from the matched tiershelf_life_months— product-level field, not tier-specific
EXW total: confirmed_quantity × unit_price_usd (no rounding tricks; keep
two decimals).
3. Freight options
The /api/freight-quotes endpoint returns all freight records across all
quotes. Filter by quote_id to get only records for the current quote, then
select the three mode records (air, sea, road).
How to identify the correct three records:
- Match on
quote_idfor the current quote - The correct records typically have IDs following the pattern
FR-<prefix>-<MODE>(e.g.FR-WC-AIR,FR-WC-SEA,FR-WC-ROAD) - Distractor records share the same
quote_idbut have any of:status: "stale"withvalid_untilbefore the quote date AND wrong shipment dimensions- Wrong
shipment_cbmorshipment_weight_kgthat doesn't match the quote's scale - Different destination or forwarder that is clearly a benchmark/test record
- IDs with
DISorOLDmarkers
Freight option fields from the API:
| Output field | API source | Notes |
|---|---|---|
freight_id |
id |
As-is |
mode |
mode |
Uppercase: air→AIR, sea→SEA, road→ROAD |
freight_cost_usd |
cost_usd |
As-is |
transit_days |
transit_days_text |
The pre-formatted string, e.g. "4-6 days" |
valid_until |
valid_until |
ISO date as-is |
risk_level |
route_risk |
Uppercase: low→LOW, medium→MEDIUM, high→HIGH |
risk_flag / customs_border_risk |
route_risk |
Uppercase; use NONE for low-risk routes; for medium road risk use the appropriate flag |
grand_total_usd |
Calculated | exw_total_usd + freight_cost_usd |
Validity checks:
- A freight quote is stale when
valid_until < quote_dateor whenstatusis"stale"in the API. source_is_stale: truewhen the record'svalid_untilfalls before the quote date.all_freight_options_valid_on_quote_date: trueonly when every freight option'svalid_until >= quote_dateAND none havestatus: "stale".
4. Recommended transport mode
Pick the lowest-risk, viable mode. Decision priority:
- Eliminate any stale/invalid option.
- Among remaining, prefer the lowest
route_risk. - When risk is equal, prefer lower cost.
- For cold-chain products (
cold_chain_required: true): shorter transit matters; the faster low-risk mode may be preferred even when more expensive. Balance cost against shelf-life headroom.
5. Policy flags
Fetch /api/policies and apply the relevant policies:
| Policy ID | When it applies | Effect |
|---|---|---|
POL-FREIGHT-RECONFIRM |
All freight quotes | freight_reconfirmation_required: true |
POL-QUOTE-VALIDITY |
All catalog quotes | offer_validity_days: 30 |
POL-INDICATIVE-EXW |
Quotes without confirmed destination | quote_basis: "EXW_ONLY", freight_excluded: true |
POL-MODULE-GRANULARITY |
Module RFQs | Quote at module level; do NOT split into components |
POL-NEW-CLIENT-PAYMENT |
New NGO / prospect clients | Payment terms = PREPAY_100 |
POL-RECURRING-NGO-PAYMENT |
Recurring NGO customers | Payment terms = NET_30_AFTER_PO |
6. Payment terms
Derive from the customer record combined with policy rules:
| Customer segment | payment_profile |
Payment terms |
|---|---|---|
new_ngo / prospect |
NEW_CLIENT_REVIEW |
PREPAY_100 |
recurring_ngo |
NET_30_AFTER_PO |
NET_30_AFTER_PO |
recurring_commercial |
NET_30_AFTER_PO |
NET_30_AFTER_PO |
implementation_services |
MILESTONE_BILLING |
Depends on milestone context — use the invoice/payment state |
Important: When the task prompt names a customer differently from the API's
name field (e.g. the prompt says "GreenHarvest Labs" but the API says "Global
Health Laboratories"), use the API name as the authoritative value. The
prompt narrative is for orientation; the API is the system of record.
Module RFQ workflow (EXW only, no freight)
When the request type is indicative_module_quote or the RFQ explicitly states
no destination:
- Quote each requested module as a separate line item.
- Use the module product code and article number — do NOT descend into
componentsarrays. The policyPOL-MODULE-GRANULARITYforbids component-level splits unless the customer requests them. quote_basis: "EXW_ONLY",freight_excluded: true.- Payment terms follow the customer's segment (see table above).
offer_validity_days: 30(POL-QUOTE-VALIDITY).
Opportunity & engagement reconciliation
1. Opportunity header
- Map stage values:
closed_won→WON,open→OPEN,closed_lost→LOST. won_amount=won_amount_usdfrom the opportunity.phase_total_amount= sum of all phaseamount_usdvalues.opportunity_matches_phase_total=won_amount == phase_total_amount.outstanding_balance= sum ofoutstanding_amount_usdacross invoices, OR the opportunity'soutstanding_amount_usdfield (they should agree).total_paid_amount= sum ofpaid_amount_usdacross all invoices.
2. Milestone naming
Use canonical milestone identifiers from the invoice or phase data. When the
template expects MS1/MS2/MS3-style sequential IDs, order phases by
their natural sequence (phase number or completion date) and assign MS1 for
phase 1, MS2 for phase 2, MS3 for phase 3.
When the template uses phase IDs directly (e.g. HEL-P1), use the
phase_id from the opportunity's phase array.
When in doubt about the ID format, check the template's enum constraint.
If the template declares "enum: MS1 | MS2 | MS3", use that sequential
format regardless of what the API calls the phases.
3. Invoice state mapping
API status |
Output invoice_state |
|---|---|
paid |
PAID |
unpaid |
OPEN |
void |
VOID |
4. Payment state mapping
Derive from paid_amount_usd vs amount_usd on the invoice:
- Fully paid (
paid_amount == amount):PAID - Partially paid (
0 < paid_amount < amount):PARTIAL - No payment (
paid_amount == 0):UNPAID
5. Revenue recognition status (per milestone)
Look up the /api/revenue-journals collection and match on invoice_id or
phase_id. A posted revenue journal entry means the milestone is recognized.
| Condition | recognition_status |
|---|---|
| Invoice paid AND revenue journal posted | RECOGNIZED |
| Invoice paid BUT no revenue journal | MISSING_REVENUE_JOURNAL |
| Invoice unpaid | NOT_REQUIRED_UNPAID |
6. Overall revenue recognition summary
COMPLETE_FOR_PAID_MILESTONES— every paid milestone has a revenue journal.MISSING_FOR_PAID_MILESTONES— at least one paid milestone lacks a journal.recognized_milestones— list of milestone IDs with journals.missing_required_milestones— list of paid milestone IDs without journals.recognized_amount— sum of amounts for recognized milestones only.
7. Accounting actions
When a paid milestone is missing its revenue journal (MISSING_REVENUE_JOURNAL):
- Primary action:
RECORD_REVENUE_MS<N>(where N is the phase number). - Debit:
DEFERRED_REVENUE - Credit:
IMPLEMENTATION_SERVICES_REVENUE - Owner queue:
ACCOUNTING
When all paid milestones are recognized, use VERIFY_REVENUE_ONLY.
8. Collection actions
Check unpaid invoice due dates against the current business date:
| Condition | Action |
|---|---|
Unpaid AND due_date < current_date (overdue) |
SEND_COLLECTION_NOTICE |
Unpaid AND due_date >= current_date (future) |
MONITOR_UNPAID_NOT_DUE |
| Nothing unpaid or due | NO_COLLECTION_ACTION |
Collection task owner: ACCOUNT_MANAGEMENT.
9. Events and vouchers
- Event status mapping:
scheduled→SCHEDULED,confirmed→SCHEDULED,active→ACTIVE,completed→COMPLETED,cancelled→CANCELLED. - Voucher status mapping:
active→ACTIVE,draft→DRAFT,expired→EXPIRED,disabled→DISABLED. discount_amount(USD) =discount_percentfrom the voucher record (used directly as the numeric value).max_uses=max_redemptionsfrom the voucher record.- Invite action:
SEND_BRIEFING_INVITEwhen the event is scheduled and hasn't passed; useVERIFY_INVITE_SENTwhen invitation was already sent;NO_INVITE_ACTIONfor past or cancelled events. - Invite task owner:
ACCOUNT_MANAGEMENT(matches the event'sfollow_up_ownerfield).
10. Follow-up task routing
Every follow-up task must include:
linked_customer_idandlinked_opportunity_idfrom the parent recordscontact_namefrom the opportunity'scontactfield or the customer's primary contactowner_queuematching the responsible department
Two standard follow-up types:
- Collection — for unpaid milestones. Includes
milestone_idandamount_due;event_idandvoucher_codearenull. - Event invitation — for upcoming client events. Includes
event_idandvoucher_code;milestone_idandamount_duearenull.
Source precedence
- API records are authoritative for all IDs, codes, names, dates, and amounts. Task narrative text is orientation only — it may use shorthand names or approximate descriptions.
- Product price tiers override any
prior_unit_price_usdon the quote. - Customer
payment_profile+ policy rules together determine payment terms — not the task narrative's description of the customer. - Invoice and payment records determine paid/unpaid state, not the opportunity's summary fields alone (though they should agree).
- Revenue journal presence (matching on invoice/phase) determines recognition status — not assumptions from the invoice state.
Common pitfalls
- Using the wrong price tier. Always match
confirmed_quantityagainst tier boundaries. A quantity of 360 lands in[300, 499], not[150, 299]. - Including distractor freight records. The
/api/freight-quoteslist includes stale benchmarks, wrong-size shipments, and unrelated quotes. Filter strictly byquote_idand then by mode/size plausibility. - Stale freight validity. Check
valid_untilagainstquote_date. A freight record that expired before the quote date is not valid and must be flagged. - Splitting module RFQs into components. The policy is explicit: keep
module-level granularity unless the customer asks otherwise. Ignore
the
componentsarray on the product. - Confusing customer names. Use the API's
namefield, not the task's informal customer label. - Incorrect milestone ID format. Check the answer template's enum
constraints. When the template says
MS1 | MS2 | MS3, use that sequential form — don't pass through rawphase_idvalues. - Missing revenue recognition for paid milestones. A paid invoice without
a corresponding revenue journal entry is an actionable gap — it drives
RECORD_REVENUE_MS<N>accounting actions, notVERIFY_REVENUE_ONLY. - Miscalculating grand totals.
exw_total + freight_cost = grand_total. Do not add tax, insurance, or duties unless the freight record includes them. - Forgetting freight reconfirmation.
POL-FREIGHT-RECONFIRMapplies to every quote with freight options — always setfreight_reconfirmation_required: true. - Event date vs task due date. The invite should be sent before the event.
The task due date for an invitation is typically well ahead of the event date.
Use the event's
event_dateand schedule the task reasonably before it.