Integration details
Description
Underboss brings your business operations and accounting into ChatGPT, helping you stay organized, keep customers happy, and understand what’s happening — and what to do next. Connect your data to track clients, services, billing, payments, and financial records in one place, then ask questions in plain English to check status, spot issues, and keep everything aligned. Built for service businesses, Underboss reduces admin work while keeping operations and accounting in sync as your business grows.
- Integration type
- Plugin
- Verification status
- Not applicable
- Platform
- ChatGPT
- Primary Subcategory
- Accounting & Bookkeeping
- Secondary Subcategories
- None listed
- Brand
- Underboss
- Access
- Account required
- First tracked
- 2026-09-16
- Tool count
- 35
- Geography
- US
The Primary Subcategory used for this profile’s headline score.
Other Subcategories where the Integration is listed.
ChatGPT Plugin Discovery Score
ChatGPT Plugin discovery is coming soon
ChatGPT can surface a Plugin when it matches a user's request.Your Plugin Discovery Score measures how often yours appears.
No spam. Unsubscribe any time.
What discovery looks like

Competing in ChatGPT Accounting & Bookkeeping
View Category35 tools agents can invoke
Commit a `Quote` to the ledger. Pass the `quote_id` of the `discover_price` / `price_quote_component` result the chat agent has confirmed (single high-confidence quote, or the provider's selection when multiple quotes were presented) — the id, not the Quote object; the server holds the quote. This tool reads the quote's `components` and emits the right ledger entries — today that means an invoice per account (one event per account, grouping multi-attendee classes into household invoices). In the future this is also where drawdown / package consumption / other non-AR ledger movements will dispatch from based on the quote's shape; the chat agent doesn't need to know which. ## Finality — committing is permanent Once a Quote is applied, every one of its components is on the ledger PERMANENTLY. There is no edit, no replace, no re-apply. The ONLY way to undo a committed charge is `create_reversal`. This is unconditional — it holds for every Quote, every time. Consequence for amendments: when the provider adds something to an event you have ALREADY applied ("also charge $15 for materials", "add a late fee"), you MUST price and apply ONLY the new item — a fresh Quote containing just the added component(s). NEVER re-`discover_price` the whole event and re-`apply_quote` it: the original components are already committed, so re-applying double-books them. The amendment is a new, separate charge. If the provider instead wants to CHANGE or CANCEL an already- applied charge, that is a reversal (`create_reversal`) of the original event, optionally followed by applying a corrected Quote — never an in-place edit. Don't pass a low- or none-confidence quote. Confidence lives on the Quote — the chat agent is responsible for the disambiguation step (auto-apply single high, confirm single medium, present alternatives for multi-quote) BEFORE calling this tool. `book_id` will be the default book if not specified; in general, there's no need to specify it. `posted_at` if not specified defaults to the event's occurrence date (the `posted_at` the quote was discovered with) — a past date anchors to end-of-day in the org timezone; today keeps the live time. Pass it only to override that. `due_date` if not specified is taken from the `due_date` emitted by the pricer (carried on each component; computed there from the pricelist rule's payment timing and the event's posted_at, timezone-anchored). All components must share one due_date. Pass an explicit `due_date` only for manual corrections. If the pricer left `due_date` null (pricelist was silent on payment timing for the matched rule), this tool raises a `missing_due_date` ToolError instead of silently defaulting. On that error: ask the provider when the invoice is due. IMPORTANT: the answer is almost always a repeating org-level billing policy ("end of the month", "net 30", "due immediately", "first Monday of next month", etc.) — not a one-off date. Default behavior on the provider's answer: 1. Call `update_pricelist` to record the timing in the pricelist prose ("…due at the end of the month"). Read the current pricelist via `get_pricelist`, append the timing language to the matching rule, and write back. 2. Re-run `discover_price` so the pricer extracts the new timing into a concrete `due_date` on the resulting Quote. 3. Call `apply_quote` again. Only pass `due_date` as an explicit arg here if the provider says something that clearly signals a one-off — e.g. "just for this one, charge it tomorrow" or "this client prepays this specific session." Repeating patterns belong in the pricelist; future invoices need to inherit them without re-asking the provider every time. If in doubt about whether the provider's answer is a rule or a one-off, ASK them: "Should I make this a standing rule for this service, or just for today's invoice?" Don't guess. Recording a one-off as a rule pollutes the pricelist; treating a rule as a one-off forces the provider to answer the same question on every future invoice. On return, you'll get the full event payload(s). One event per account is emitted when multiple customers belong to different households; one event total when they share an account.
How to create a contact. At a minimum, provide a name -- either first or last -- and one or more tags. Contact tags depend on the current user's role: - For a user-provider: tag as 'attendee'/'client' (the client/customer who receives the service) or 'payer' (who pays on the account, typically a parent). - For a user-customer: tag as 'provider' (the service provider). A contact is one person. An account is a grouping of contacts (e.g., siblings, family members). To associate with an existing account, pass in an `account_id` (discoverable via `get_contacts` or a previous `create_contact` call). If the account_id is None, a new account will be created. NEVER invent an account_id — only use one returned from a previous call, or None. When creating a new account and the contact is not a payer, offer to either make the contact a payer or to add a billing contact to the account. Example: adding two contacts to the same account: 1. create_contact(name='June', tags=['attendee'], account_id=None) # creates account 2. create_contact(name='Jerry', tags=['attendee'], account_id=<June's account_id>) Account naming: accounts are named after the attendee/client, not the payer. For example, if Jerry is the payer and June is the attendee, name the contact (and thus the account) after June. If there are multiple attendees, use the first attendee's name or a shared name if appropriate. In general, unless it's clear from context, we should ask what account the contact should be associated with. Prefer adding a contact to an existing account (as shown in the example above) rather than creating a new one and moving the contact later. We want to avoid gratuitously creating new accounts.
How to offer a new service. An offered service is added to the catalog of services the business provides to clients. Service names should be CATEGORY-LEVEL — e.g. "Consulting", "Tutoring", "Piano", "Ballet", "Soccer practice", "Art". DO NOT bake variants into the name (no "Piano - 30 minutes" or "Ballet - level 3" or "Friday soccer - older group"). Variants are a pricing concern; they live in the pricelist as rules ("Piano lessons are $55 for 30 min, $90 for 60 min"), not as separate offered services. Multi-quote disambiguation at pricing time handles the cases where the same service is priced differently depending on context. Don't include client names in the service name. The service is what's offered to the catalog, not a specific instance. Before creating a new service, check `get_offered_services` with `similar_name` to see if a category-level match already exists. If one does, use it. Do NOT create a new service just because the user mentioned a duration / level / format detail — pass those as event metadata when pricing. Services no longer carry price info, balance_type declarations, or payment terms. The pricing engine owns all of those — the pricer determines per-line price, balance_type, and due_date based on the org's pricelist. Services are just named handles for what's being offered. If the user mentions a price for this service while creating it (e.g. "create a piano class at $55/lesson"), you MUST also call `update_pricelist` to record the rule — `create_offered_service` only creates the name handle, not the price. Call `get_pricelist` first to read the current text, then pass the full new pricelist (existing rules + the new one) to `update_pricelist`.
Create a personal organization for the current user. The user becomes the administrator with full access. A default ledger book is created automatically. The new org is automatically selected as the current context. Use this when list_my_orgs returns empty (user has no orgs).
How to create a payment entry in the ledger. While most of these parameters have a straight-forward meaning, the payer field deserves some explanation. A payer is a contact, typically tagged as a payer. It may be the user wants to use a contact that isn't tagged as a payer. It's probably worth it to be reluctant to use such a contact (unless it has no tags) and then tag the contact as such if used. When creating a payment event, ensure we know the `method`. Don't just assume it's a particular method unless it's very clear from context. Internally, the ledger will use a waterfall mechanism to apply the payment against open invoices. That means there's no need to ask the user how they want the payment applied: specifically, don't ask about invoices. In general, you'll never need to specify the book_id. As with `apply_quote`, the full event payload is returned. When inspecting the result, the account is the payer's account and the subaccount is the actual payer.
How to reverse an entry in the ledger. The `references` field points to the event being reversed. Discover the `event_id` from the chat history, or by querying the ledger for the event to reverse. You normally do NOT pass `effective_at`. The reversal is POSTED now but is automatically back-dated to the original event's effective_at, so it nets out in the original's period rather than today's. Pass `effective_at` only to override that. If you can't find an event_id because it fell out the chat history and it hasn't yet been processed, let the user know to wait a bit for the event to process. Also, you can't reverse a reversal event. In that case, just re-create the event based on the event that was reversed. In general, you'll never need to specify the book_id. As with `apply_quote`, the full event payload is returned.
Determine the economic impact of a service event by consulting the org's pricelist. Reach for this as the FIRST step when the user reports a service event — don't interrogate the provider first. Resolve the inputs with tools, not questions: `get_contacts` to find the attendees, `get_offered_services` to find the `contract`. Do NOT ask whether a customer is on the list, what the service costs, or when it happened — the tools and the pricer surface every gap. Ask the provider only once something genuinely missing has been reported (e.g. an unpriced component). Describe the fact event: `contract` (the offered service / class), the list of attendees (`customers`), and optional event-level `metadata` (head_count, duration, qualifiers — anything the glossary flags as relevant). Do NOT enumerate activities or line items — the pricer decides those from the pricelist. Amending an already-applied event: if you have ALREADY called `apply_quote` for this event and the provider now wants to ADD a charge ("also add materials", "tack on a late fee"), describe ONLY the new thing here — not the whole event again. The original charges are already committed; re-discovering the full event and re-applying it double-books them. See `apply_quote` "Finality". `provider` defaults to the current user — DON'T ask who taught / ran the session. Only pass `provider` when the user explicitly says someone else ran it (e.g. a multi-instructor org where the user is recording on a colleague's behalf). `posted_at` is WHEN the event happened. One test decides whether to pass it: did the user's message contain a time cue? - YES — "today", "yesterday", "last Tuesday", "on the 3rd", an explicit date — you MUST pass `posted_at`, resolving their words to that date. A user who wrote "today" gave a cue: pass it. A stated date lets the pricer return a `high`-confidence quote that applies WITHOUT a separate date confirmation. - NO — the message said nothing about timing — OMIT `posted_at`. Do not ask the user for it, and do not pass a date you assumed (believing it was today is an assumption, not a cue the user gave). Omitting is deliberate signalling, not a fallback: the pricer then assumes today, caps the quote at `medium` confidence (never `high`), and notes the assumed date in the quote's `reasoning`. That `medium` is your signal to confirm the date with the provider before applying (see the Workflow section). Passing a date you assumed defeats this — the pricer cannot tell an assumed date from a stated one — and an applied charge is permanent, so a wrong date then needs a reversal to fix. Service-name lookup: when the user names a service (e.g. "piano session"), look it up in the catalog via `get_offered_services(similar_name=...)`. If exactly one service matches, USE IT — don't ask the user to pick from invented variants. Variants (30 vs. 60 min, group vs. private) are pricelist rules keyed off event-level `metadata`, not separate services. Event metadata: pass everything that might be priced — the activities present, duration, head_count, format, qualifiers. If you suspect the pricelist depends on a fact the user didn't mention, ask naturally and pass it through. Returns a list of `Quote` — each is a coherent interpretation of the event (a complete set of line item `components` + an overall `confidence` + `reasoning`). Components may be: - **Priced from the pricelist**: `amounts=[...]`, `source=pricelist` - **Unpriced** (`'amount' in requires`, no rule matched): `amounts=[]`, `source=pricelist`, `reasoning` describes what's missing — fill in via `price_quote_component` or `update_pricelist` per `prefer`. - **Ad-hoc** (filled by you from provider input via `price_quote_component`): `amounts=[...]`, `source=adhoc` - **No-amount** (`'amount' in proscribes`, bookkeeping activity like `makeup_session` that drains a prior hold): `amounts=[]`, `source=pricelist` — FINAL, not unpriced. Don't fill it in; the apply layer raises `proscribed_field` if you do. The unpriced / no-amount split keys off `requires` / `proscribes`, NOT the activity name — read the fields; don't pattern-match the name. Each component also carries a `prefer` field (`pricelist` or `adhoc`) — the activity's routing hint. Use it to decide how to resolve an unpriced component (see the Workflow section). It is platform-set; trust it over your own read of the activity name. A component is ONE charge with ONE amount and a `customers` list: `[]` = account-level, `[one]` = a single customer's charge, `[a, b, …]` = a shared charge (one amount the group shares — e.g. a single set of class materials for two siblings). When you fill an unpriced shared component with `price_quote_component`, you give it the ONE shared amount — not a per-customer figure. ## Workflow First read the chosen Quote's `confidence` — it decides whether you apply silently or confirm with the provider first: - **`high`** — apply it directly; no confirmation needed. - **`medium`** — do NOT apply yet. Restate what you're about to record and get an explicit OK from the provider first. If the Quote's `reasoning` says the event date was ASSUMED (the provider gave no `when`), your confirmation MUST state that date — e.g. "recording this for today, May 20 — that right?" — and wait for the answer before `apply_quote`. An applied charge is permanent; confirm BEFORE, not after. - **`low` / `none`** — no usable interpretation yet; resolve the gap (below), then re-discover. Confidence is an INTERNAL routing signal — it tells YOU what to do, not what to tell the provider. When there's a single high-confidence quote, do NOT narrate the confidence at all: just record it (or report the action in your own words if a response is expected) without phrases like "I'm highly confident" or "this is a high-confidence match." That's filler. Save any qualifying language for the cases where it actually helps the provider decide (medium-confidence confirmations, multi-quote disambiguations). **Fully resolved on first pass** (every `'amount' in requires` component has `amounts`; no-amount components — `'amount' in proscribes` — are already final): the Quote is ready — apply it per the `confidence` rule above. **Some components unpriced** (`'amount' in requires` AND `amounts=[]`): resolve each gap. The right route is keyed on the component's `prefer` field — the platform sets it from the activity; DON'T guess from the activity name yourself: - **`prefer=pricelist`** (session, registration, no_show, late_cancel, makeup_session, missed_session): pricing is consistent across occurrences and belongs in the pricelist. Ask the provider for the amount, then DEFAULT to `update_pricelist` to encode it as a rule, then re-call `discover_price(quote_id=current_quote_id)`. Future events inherit the rule. Only treat as a one-off (next bullet) if the provider EXPLICITLY says "just this once / don't make it a rule." - **`prefer=adhoc`** (materials, damage, adjustment): pricing varies per occurrence — it does NOT belong in the pricelist. Ask the provider for the amount, then DEFAULT to `price_quote_component(quote_id, index, amount, ...)` to fill it in as a one-off (`source=adhoc`). Only call `update_pricelist` for one of these if the provider EXPLICITLY states a standing policy ("materials are always $25", "I always charge a flat $15 supply fee"). Do NOT intern an ad-hoc activity into the pricelist by default. A materials charge is the canonical example: it's a one-off; `price_quote_component` it, don't `update_pricelist`. - Follow the provider when their stated intent contradicts the `prefer` default. Otherwise just follow `prefer`: `prefer=pricelist` is a strong signal to store the price as a pricelist entry, so unless there's real ambiguity, encode it with `update_pricelist` and move on — a rate the provider gives for a pricelist activity is the standing rate, no confirmation needed. Reserve "standing rule, or just this once?" for a genuinely ambiguous `prefer=adhoc` case with no intent signal. After filling an unpriced component via `price_quote_component`, either: - re-call `discover_price(quote_id=...)` (the augmented quote's id) to let the pricer check for dependent rules (bundle discounts, tier thresholds). Recommended when the pricelist mentions tiers / bundles. - or go straight to `apply_quote` when the augmentation is clearly independent of pricelist rules (pure damage charge). **Multiple quotes returned**: only present them as a choice when they MATERIALLY differ — different amounts, or charges the provider would actually decide between. If they resolve to the same charges (e.g. all $50 each), they are NOT a real choice: just take one per its `confidence` and proceed — do NOT ask the provider to confirm or pick between identical outcomes. When they genuinely differ, present and let the provider pick. Each quote carries its own `id` — carry the chosen quote's `id` into `price_quote_component` / `apply_quote` / a re-`discover_price`. **`confidence=none` quote** (no pricelist coverage): same as "some unpriced" — resolve via standing-rule or one-off. ## Re-entry Pass `quote_id=<previous quote's id>` to re-discover after augmentation. The pricer respects `source=adhoc` components as frozen, may refine pricelist-source components, and may add new components if dependent rules now apply. If the pricer returns the same Quote (no changes), the iteration has converged — proceed to `apply_quote`. Don't loop indefinitely: if you've called `discover_price` more than 3 times for the same fact event without the Quote settling, stop and surface the situation to the provider. Use this BEFORE every `apply_quote` call. Don't invent prices.
How to generate a payment link a customer can open to pay an account's outstanding balance. This is the full customer-portal link (the same one used in unpaid-balance SMS reminders, but not shortened), valid for two weeks. Pass either an account id or a contact id: - an account id resolves to the payer-preferred contact on it - a contact id resolves to that contact's primary account Returns the link plus the account and contact it resolved to. Present the url as plain text exactly as returned — do NOT wrap it in a markdown link or anchor, so the provider can copy-paste the raw URL into an email.
How to query account aging. Use this query to discover things about accounts like the outstanding balance or days past due. In the result set, you'll see fields like `balance_0_15` which indicate the day range for how long a balance has been on the books. As with the get_ledger query, you almost never need to specify the book_id (it's defaulted). `provider` restricts scope to particular members (providers) in your org. Two query styles, by what they produce: - Ordered by name similarity: `similar_name` ranks the results by how closely a contact name matches the given text. It orders, it does not restrict — every in-scope account is returned, just sorted by closeness. - Restricted to specific accounts or customers: `account` returns only the given account ids; `customer` returns only accounts associated with the given contact (customer) ids. Both take ids, not names. `prefer_balance` and `prefer_days` also order rather than restrict: `prefer_balance` ranks higher balances first, `prefer_days` ranks greater days past due first. Use `join_customers=True` to include customer information in the results. This will add a `customers` field to each result row containing an array of complete customer, i.e. contact, records. Each customer record includes id, data (names, phones, addresses), and tags. This ensures one row per account even when multiple customers are associated with it. Treat the default limit as a strong hint for how many items should be fetched at a time and depend more on pagination and/or query restrictions over returning everything.
How to get the current account notification state. About some of the returned fields: `last_at` indicates when the last notification was sent to that account `next_at` indicates when the next notification may be sent to that account `reasons` indicates the reasons why a notification will NOT be sent Note that these are account-level notifications. If at least one contact can receive a notification, then that account will get notified. Interpreting `reasons` can be confusing. Some reasons are in negative form like 'notify::not_today' and others are in positive form like 'contact:receiver:mobile' (meaning a contact doesn't have a mobile number); either way, they indicate why a notification will not be sent. If you are showing reasons, try to interpret them into a more natural language.
How to fetch connector info. Connector `status` is important to determine whether or not a connector can be updated or created: 'new' == connector has been created and can be updated 'inactive' == connector is inactive but can be moved to 'new' 'ready' == connector is running 'verifying' == connector is doing setup/authorization to move to 'ready' 'failed' == connector failed to verify (terminal state) 'released' == connector is explicitly marked dead (terminal state) Use `latest_mode` to fetch latest connector for service: 'available' == prefers connectors with 'new' status 'running' == prefers connectors with 'ready' status 'recent' == prefers the most recently updated Pay attention to the `usable` field to determine if connector is, in fact, usable. The `unready` field will provide reasons the connector isn't usable.
Returns a list of contacts with names similar to `similar_name`. Results are paginated — use `offset` to fetch additional pages if needed. Use `tags` to filter only those tagged with at least one `tags` item. Use `prefer_tags` to boost score by each tag that overlaps. Use `contact_ids` to restrict search to only the specified contact_ids; otherwise None. Contact ids are not user facing, use only the name field to identify the contact. Contact tags depend on the current user's role: - For a user-provider: contacts are tagged 'attendee'/'client' (the client/customer who receives the service) or 'payer' (who pays on the account, typically a parent). When searching for a client, use both 'attendee' and 'client'. - For a user-customer: contacts are tagged 'provider' (the service provider). In our parlance, 'client' and 'customer' are interchangeable. Tags are not user facing; call them 'clients', 'billing contacts', and 'providers' respectively. An account is a grouping of contacts — there can be multiple contacts per account. When a user refers to an "account", they typically mean the group (e.g. a family), not an individual contact. Each contact returned will include its associated account. This is the preferred query for getting contact/account information. The list returned is sorted from highest to lowest 'score'. By default the limit is set to 5; if limit is None, all will be returned. Unless explicitly specified, restrict status to 'active'.
Returns the current local time in form of <hour>:<minute> <AM or PM> If no timezone is specified, the default timezone is used
Return the org's glossary — both the platform-level layer (legal `balance_type` values and other ruleset-coupled vocabulary) and the per-org layer (custom contact attributes, custom metadata fields the org has defined). The platform layer is universal across all orgs and inherited automatically; it's exposed here as read-only reference data. The per-org layer starts empty and grows via `update_glossary`. Both are returned together so callers don't need a separate fetch to learn the platform vocabulary. Note: `get_pricelist` also returns this glossary as a nested field (so the common authoring flow needs only one read). Use this standalone tool when you just want the vocabulary without the pricelist context. Use when: - You need to look up what custom attributes / fields are defined. - You're about to call `update_glossary` and want to consolidate with existing definitions. - Displaying or auditing the org's vocabulary. Do NOT pass the `platform` field's content into `update_glossary` — it's frozen reference data, and writing it back would intern stale copies into the per-org row.
How to query the ledger. Always use the production ledger. Leave the book_id field empty (use the main book on the current org). Note that the current org is determined outside of this function interface. For current balance inquiries, prefer `get_account_aging`. Use `get_ledger` only when you need transaction-level detail or to compute a balance at a specific past date. Always `limit` your query to no more than 20 results. Use `offset` to paginate for more results. Don't just increase the limit. Be sure to inform the user that there may be more pages of data after returning results. When you're aggregating on a window field, you must also specify the field `filter` value in the `window` arg. To match any (not null), use '*'. When querying for a specific customer, find and specify the account id in the `account` field and always leave the `customer` field empty. Don't use any other window field, instead use the full account data to answer the specific question. Most of the information the user will be interested in is in the 'customers' journal, which is the main accounts receivable journal. Use this journal for most queries. You can read revenues from the 'revenues' journal. The 'payments' journal contains payments that were pulled from a feed but we don't yet know who made the payment. Regular payments will be credited to the 'customers' journal. The ledger consists of entries, which is a single debit or credit journal entry. The entries are grouped by transactions, represented by a single transaction_id. The ledger is double entry. A transaction will always balance, meaning that the sum of the debits and credits will always be zero. Generally, the user is interested in a single transaction so, you should almost always aggregate on 'transaction_id' and set transaction_id='*' in the window. If you really need to know the underlying entries, you can add 'entries' to the include list to get all the detail (e.g. how a payment was allocated to different invoices and/or customers). That said, you should show one line per payment and only describe the allocation detail when asked about a specific payment. For payments, the 'subaccount' field in the results contains the contact id of the payer. To maintain chronological order when aggregating, also include 'posted_at' in the aggregations as the first element. The date you should reference is always the `posted_at` date. Never use the `event_at` date. Use `posted_from` and `posted_to` to filter transactions by their posted date range. Note that you'll need to provide the correct range between `posted_from` and `posted_to`. Specifically, if asking about, for example, today, ensure that from and to are a day apart. Use `date_period` to group results by time periods ('day', 'week', 'month', 'quarter', 'year'). When `date_period` is specified, it automatically groups by the time period using 'posted_at'. You don't need to include 'posted_at' in the `aggregate` list - it will be added automatically. When displaying a date to the user, it should be in human-readable format (e.g., 'August 1, 2025'). Tutor, teacher, coach, employee, staff, subcontractor are all synonyms for provider. Class, service, program, course, lesson are all synonyms for contract. Session is a synonym for an 'invoice' transaction_type. Do not show id values in the response, unless absolutely necessary. When describing the results, try to show each item as a sentence with names for customer, payer, provider and contract (aka service offering), if possible. When aggregating (balances, totals, sums, counts, etc), you should almost always set reversal=False in the `window` arg. Reversals are corrective entries that cancel out previous transactions. Only include reversals when the user specifically asks about reversed or voided transactions. Identify whether the request is about transactions (sessions, payments, invoices, etc) or balances. * To get customer transactions, specify the filters in the `window` arg but don't use aggregations. * To get balances, specify the filters in the `window` arg and also include them in the aggregations. Examples: * Get an account balance (say, for account id 'b92d8226114c'): get_ledger(window=dict(journal='customers', account='b92d8226114c', reversal=False), aggregate=['account']) * Get all account balances: get_ledger(window=dict(journal='customers', reversal=False), aggregate=['account']) * Get all transactions on an account (say, for account id 'b92d8226114c'): get_ledger(window=dict(journal='customers', account='b92d8226114c', transaction_id='*', reversal=False), aggregate=['posted_at', 'transaction_id'], include=[]) * Get payment transactions: get_ledger(window=dict(journal='customers', transaction_type='payment', transaction_id='*', reversal=False), aggregate=['posted_at', 'transaction_id'], include=[]) * Get sessions/services rendered (i.e. total invoiced amounts) by provider for the month (say, for August 2025): get_ledger(window=dict(journal='customers', transaction_type='invoice', reversal=False), aggregate=['provider'], posted_from='2025-08-01', posted_to='2025-08-31') * Get monthly revenue breakdown by provider: get_ledger(window=dict(journal='revenues', transaction_type='invoice', reversal=False), aggregate=['provider'], date_period='month', posted_from='2025-01-01', posted_to='2025-12-31')
Returns current user's data + `profile` + `identities` (the first being primary). The `profile.name` field has the current user's name. The `tags` field indicates the user's role(s): a user-provider offers services (the typical user); a user-customer is a customer of one or more providers. A user can be both.
Get the list of members/providers associated with the current org. Typically, we only care about `active` members. The `who` fields are the memembers whereas the `which` fields are the org. Use `who_ids` to constrain to query to only those members. Results are paginated — use `offset` to fetch additional pages if needed. The `roles` field indicates what roles the org member has been provisioned for. The 'admin' role is an owner of the org. Do not show the provider id unless absolutely necessary.
How to get a list/catalog of services or classes currently being offered. The id (or service_id) is also the contract id. The price field in `data` is a default price. Unless explicitly specified, restrict status to 'active'.
Returns the current org and org setting (if available). An org and a business are synonymous. For a user-friendly org name, look in `setting` for a `setting.profile.name`. If that's unavailable, decode the org `name` field matching the form: 'classpay-<who_id>'. Then, fetch the member of the org for that `who_id`. This is the creator of the org. Use the creator's name or username as the user-friendly org name. The `setting` object has both the profile and customer configuration data.
Return the org's currently-active pricelist (the prose source of truth for pricing rules), bundled with the glossary needed to author rules. For a brand-new org that hasn't authored a pricelist yet, `data` is an empty string and `version` / `status` / `updated_at` are null — that's the normal initial state, not a failure. `glossary` is always populated (platform layer is universal; per-org layer may be empty). Pass `version` to read a specific prior version instead of the active one — e.g. to recover a rule that a later `update_pricelist` dropped, so you can re-author it back in. It accepts a relative offset too: `-1` is the previous version, `-2` two back. Omit for the current active version. The bundled `glossary` field is what makes this the one-stop fetch before authoring: when constructing a new pricelist via `update_pricelist`, you need to know what `balance_type` values are legal (from glossary.platform) and what custom terms the org has defined (from glossary.data). No separate `get_glossary` call needed. Use this whenever you need the current rules — before any `update_pricelist`, after `discover_price` returns confidence=low/none, when displaying the pricelist to the provider, when verifying a rule the user asks about. When showing the pricelist to the provider, present ONLY the `data` field (the prose) — optionally with a brief "last updated" note derived from `updated_at`. Do NOT mention `version`, `status`, or any field from `glossary` (neither `platform` nor `data`). Those are for YOUR reference when authoring or pricing, not for the provider to see. Mention version/status/glossary only when the user explicitly asks ("what version is this?", "what balance_types are available?", etc.). Do not surface internal field names (`balance_type`, `due_date`, etc.) when displaying the pricelist — the provider thinks in business terms ("a session", "billed after class"), not schema terms. If you need to explain a concept (the user asks "what happens when someone misses a class?"), use the natural-language meaning from the glossary, not the field-name vocabulary. Consolidation hint: over time the pricelist may accumulate redundant or contradictory rules. If it looks unwieldy (lots of rules, overlapping conditions, obsolete entries), surface that to the provider — they may want a consolidated rewrite. Apply by constructing a cleaned-up version and calling `update_pricelist` with the consolidated text. Consolidation is a deliberate, provider-initiated action, not automatic.
List this context's quotes, filtered by the given parameters. With no arguments, returns everything for the context. Plain filters (each None = no filter): - `status` — lifecycle: `complete`/`incomplete` (pending) or `applied` (committed). - `quote_ids` / `batch_ids` — by quote id, or by event batch. `include` surfaces normally-hidden quotes: - `stale` — quotes past their expiry or priced against a superseded pricelist are hidden by default; pass `include=[stale]` to see them. `anchored_only` filters by whether a quote's batch is decided (a sibling has been applied, settling the event): `True` keeps only quotes in decided batches; `False` applies no batch filter (all). Left unset it resolves from `status`: pending → open batches only (live options), `applied` → decided batches (the recorded decisions), all/mixed → no filter. So `status=[complete], anchored_only=True` lists the losing alternatives of already-decided events ("what we passed on"), while `anchored_only=False` widens a pending query to include them. A returned quote is not a guarantee it can be applied — `apply_quote` still rejects stale / re-billed / already-resolved quotes with a specific error; re-run `discover_price` to refresh when that happens. Rows whose stored payload no longer decodes are skipped.
Returns today's date and the day of week in the form of <day of week>, <month> <day>, <year> If no timezone is specified, the default timezone is used
List all organizations the current user belongs to. Returns a list of orgs with their IDs, names, and basic info. Results are sorted with non-personal orgs first, then personal orgs. May return a single org if user has an override org configured. ORG SELECTION FLOW: If you get an "org context required" error, follow this flow: 1. Call get_me - if profile.data.selected_org_id is set, call set_current_org with that ID to restore the selection 2. Otherwise, call this tool (list_my_orgs) to discover available orgs: - If 0 orgs: Use create_org (it auto-selects the new org) - If 1 org: Auto-select it with set_current_org - If multiple: Ask user which org, then set_current_org Once selected, it persists across sessions.
Move a contact from current account to account `to_account_id`. If `to_account_id` is None, a new account is created and used. `to_account_id` may also be a contact id associated with an account; in that case it is resolved to that contact's account.
Fill in an ad-hoc amount on a quote's unpriced component. Use when `discover_price` returned a Quote with a component that has `amounts=[]` (no pricelist rule matched) and it should be a ONE-OFF charge — not a standing pricelist rule. This is the DEFAULT resolution for unpriced components whose `prefer` field is `adhoc` (materials, damage, adjustment) — pricing for those varies per occurrence. It's also correct for a `prefer=pricelist` component when the provider has explicitly said this specific charge is a one-off ("just this once"). For a standing/repeating charge — the default for `prefer=pricelist` activities — use `update_pricelist` + re-`discover_price` instead, so future events inherit the rule. The targeted component's `amounts` is set to `[amount]`, its `source` becomes `adhoc`, and its `reasoning` is replaced with the provided text (typically a short description of why this one-off applies, e.g., "replacement music stand, broken in today's class"). Other components are passed through unchanged. `due_date`: leave OMITTED to keep the component's existing due_date. The pricer anchors an unpriced component's due_date to its sibling priced components when they share one cycle (e.g., a books charge inherits the session's end-of-month timing) — that inherited value is usually what you want. Pass `due_date` ONLY to override: - the provider wants different timing for this specific charge ("books are due now, even though the class is end-of-month") - the component's due_date is still null (the pricer couldn't anchor it — sibling charges had differing due_dates, or there were no priced siblings). In that case ask the provider when it's due and pass the answer here. If you leave the component's due_date null, `apply_quote` will reject the Quote with `missing_due_date`. Returns the augmented Quote — its id is unchanged. Pass that id to `apply_quote` to commit, OR back through `discover_price(quote_id=...)` first if you suspect the pricelist has dependent rules that might trigger now that this component has an amount (bundle discounts, tier thresholds, etc.).
Set the current organization context for subsequent operations. Most financial operations require an org context. This selection is persisted to your profile, so you only need to set it once. When to use: - After list_my_orgs returns exactly 1 org (auto-select it) - After the user chooses from multiple orgs - After create_org to set the newly created org as current
How to update a connector. The `unready` field in the results indicate the reasons for a connector to be unready for use. An empty `unready` means there's no reason it won't work.
How to update a contact. If arg is None, then no change will be made. Note that these fields will overwrite what's currently stored, so its important to fetch the contact first and patch the data that's changing. When updating `name`, be careful that you don't end up setting an empty string on the computed `full` name (composed of first, last, middle, title, suffix, & nick). In general, we want the first or last name to be non-empty. While you can never delete a contact, you can mark it inactive. This is the best way to hide a contact. Try not to make any other changes to the contact record automatically. `meta` is an org-defined freeform dict for organisation-specific annotations that don't fit the platform fields above. By convention, `meta.labels` (list[str]) is the canonical slot for tag-like terms the org defines for itself (e.g. "rmsb", "vip") — distinct from `tags`, which is the platform-controlled enum. Any term used in `meta` should be defined in the per-org glossary so its meaning and location are recorded; see `get_glossary` / `update_glossary`. Pass `meta` to overwrite the whole dict — fetch the contact first if you want to preserve existing keys.
Write a new version of the org's per-org glossary. Pass the FULL new glossary text — this replaces the current one wholesale. The glossary is mutable in place (not versioned, unlike the pricelist). Call `get_glossary` first to read what's currently in place, then construct the new content (existing definitions + your additions / modifications / removals) and submit it here. Use when: - Defining a new contact attribute that pricelist rules will match against (e.g. a `member` tag the org will use for member pricing). - Documenting an org-specific term, custom balance_type, or per-event metadata field the platform glossary doesn't cover. - Naming a label the org wants to attach to entities via their freeform `meta` field (e.g. "rmsb" stored at `contact.meta.labels`). - Refining or removing a previously-defined term. When defining a term that gets stored on an entity, include where it lives in prose (e.g. "RMSB: members of the RMSB program; stored on contact `meta.labels`") so future lookups can route to the right field. Don't redefine platform-layer terms here (standard balance_types) — those are inherited from code and orgs can't override them.
How to update the current user's profile. Note that these fields will overwrite what's currently stored in the user profile. When doing an update, it's important to fetch the current profile and the patch in the data that's changing.
How to update an offered service or class. Note: services no longer carry pricing-relevant info (prices, balance_types, payment terms). The pricing engine owns all of that. Updates here only modify metadata (name, status); existing `default_amount` / `payment_terms` values stored on legacy service records are left in place (backwards compat) but no longer read or written.
How to update customer notification in org settings. `from_` uses the org setting profile name if not set; otherwise customize here `unpaid_sms` configuration for sending unpaid sms notifications
How to update the org setting profile. To clear non-object fields, use an empty string. Returns None if there was no change or current user doesn't have permission to change. Note that these fields will overwrite what's currently there so it's important to fetch the org setting first and patch what's changing.
Create a new version of the org's pricelist. Versioned, not destructive: the new text becomes the active version and the prior one is retained (demoted to inactive, readable via `get_pricelist(version= ...)`); historical charges keep referencing whatever version they resolved against. So authoring pricing the provider just stated is a routine write — go ahead and submit, no confirmation needed. Pass the FULL new text — the new version is what becomes active, so include every existing rule you want to keep alongside your additions / modifications / removals. A rule you omit leaves the active version (and is recoverable from the prior version via `get_pricelist` if you drop one by mistake). Call `get_pricelist` first to read what's currently in place (the response also bundles the glossary you'll need to author against — legal `balance_type` values plus any custom per-org terms). Optional `glossary` arg: pass alongside `data` when the new rule references a custom term the per-org glossary doesn't yet define (e.g. introducing "member" pricing requires a glossary entry defining what "member" means). Both writes commit in the same transaction — atomically applied or atomically rolled back. The returned PricelistOut.glossary reflects the post-update state, so no follow-up `get_glossary` is needed. Pass ONLY the per-org part — do NOT include `glossary.platform` content from `get_pricelist`, that's frozen reference data. Use this whenever the pricelist changes: - Adding a new rule for a REPEATING charge — a charge that will apply consistently to future occurrences (a session price, a registration fee, a no-show policy, a member discount). - Modifying an existing rule (price change, condition refinement). - Removing an obsolete rule. - Consolidating redundant rules into more general ones. Do NOT use this to record a ONE-OFF charge. If `discover_price` returned an unpriced component whose `prefer` is `adhoc` (materials, damage, adjustment), the default is a one-off — fill it with `price_quote_component`, not a pricelist rule. Only encode an ad-hoc-prefer activity as a rule when the provider explicitly states a standing policy. Interning a one-off charge here pollutes the pricelist and mis-prices future events. Record only what the provider actually stated. In particular, do NOT append payment timing ("due at end of month", "due immediately", "net 30") the provider did not give — an invented due-date clause here silently becomes a standing billing policy that every future invoice inherits. If timing is unstated, leave it out of the prose entirely. Future `discover_price` calls see the new prose.
How to upsert a connector. If there's a connector with a 'new' status, it will be updated; otherwise it will be created. The `unready` field in the results indicate the reasons for a connector to be unready for use. An empty `unready` means there's no reason it won't work.
How do I improve a ChatGPT Plugin's discoverability?
The levers are the listing surface agents actually read: names, descriptions, keywords, tool metadata, and registry health. Which lever matters depends on where discovery breaks, which is what continuous measurement shows.
What are Underboss alternatives on ChatGPT?
As of 2026-09-16, Underboss competes with AgentCollect, Akaunting, AuntBird Practice Management, B2B.nu, Cryptoworth, Digits, Double, Ekohesap, ExpenseBot, Fattura24, Finn — AI Accountant, Finom, FinOpps, Fiscal Pro, Granatum Financeiro, HelpDol, inFakt, Inkle, Intuit QuickBooks, Jaz Accounting, Kick, MedFIN, MYOB, Never86'd Marketplace Audit, Norman, NP Ledger, QBO Connector by Meridian, SHVL, Taxorio, tugesto, Validis (US), Xero in ChatGPT Accounting & Bookkeeping, ranked by public Discoverability Score.
Where is this profile measured?
This profile uses the geography attached to the latest public registry snapshot: US. Locale tags are intentionally omitted.