Use Postwyre CRM from your agent
Connect an AI agent to Postwyre CRM over MCP or the REST API: keys, tools, retries with idempotency keys, and rate limits. Read this page as markdown
What your agent can do
Postwyre CRM has an MCP server and a REST API. An agent signs in to the MCP server with OAuth, in your browser; the REST API takes an API key. An agent can read contacts, the pipeline, bookings, proposals, invoices and the help inbox, and it can write notes, tasks, deals, drafts, bookings, proposals and invoice drafts.
After a customer approves an option on a proposal, the agreement for it is made on the same link. Your agent reads it with get_agreement or GET /api/proposals/{id}/agreement, and every agreement with list_agreements or GET /api/agreements: the terms, whether and when it was signed, its Stripe billing while billing is on, and the onboarding tasks made when the customer paid. draft_agreement_copy_email puts the email with the signed copy in Today for a person. An owner's or admin's agent can enter a month's revenue share figure with report_share_gross (or POST /api/proposals/{id}/agreement/share/reports); it is recorded as the agent's, never as the client's. list_share_invoices (or GET /api/proposals/{id}/agreement/share/invoices) lists each month's share invoice: percent x the reported figure, rounded to the cent, within any cap. Only the workspace's owner, or an agent signed in as the owner, can approve one with approve_share_invoice; it is then finalized in Stripe test mode for the business to send. No tool or key approves, signs, starts billing or pays for a customer. list_terms_ending lists the deals whose committed term ends within 14 days, or already ended with nobody deciding, as Today shows them; draft_renewal drafts the next term's proposal on the same deal for a person to read and share; ending is a person's call.
Refunds are read the same way the app shows them. While billing is on, get_agreement (and GET /api/proposals/{id}/agreement) returns the deal's money under billing: paidCents, refundedCents (what the business gave back in Stripe, setup and monthly, as setupRefundedCents and monthlyRefundedCents), keptCents, owedCents (what is left on the open setup invoice; a refund never makes a paid invoice owed again), refund (none, part_refunded or refunded), setupRefund, and monthlyPastDue when Stripe has a monthly invoice past due that owedCents leaves out. list_agreements returns the same figures per agreement under money. For invoices, list_invoices and get_invoice return refundedCents, and paidCents net of refunds; a refund on an invoice reopens its balance. Refunds are made in Stripe by a person; no tool or key refunds. Reading twice gives the same answer: a refund Stripe reports twice is counted once.
A person stays in charge of anything that reaches a customer or costs money. No key can approve a message, send one a person has not approved, change who may be contacted, make an offer, share a proposal, record a payment, change billing or manage keys. Drafts an agent writes wait in Today until a person approves them.
In a brand marked as healthcare, a key gets ids and counts only, over MCP and REST alike, and is refused anything that would hand patient details to an agent. A person signed in to the app still sees everything.
Connect over MCP
- In your agent's MCP settings, add a remote server with the address
https://crm.postwyre.com/api/mcpand OAuth sign-in. For example:hermes mcp add crmwyre --url https://crm.postwyre.com/api/mcp --auth oauth. - Your browser opens Postwyre CRM. Sign in as usual, pick one brand and the role the agent gets there, and choose Allow. The agent never gets a higher role than yours on that brand; Member is enough for every tool today.
- Your agent calls
list_workspacesfirst. It lists the one brand you picked, and every other tool takes itsworkspace_id. - To cut an agent off, open Settings, find it under Connected agents and choose Disconnect. It loses access on its next call.
The server speaks Streamable HTTP, protocol versions 2025-11-25, 2025-06-18, 2025-03-26. Sign-in follows the MCP authorization spec: an unauthenticated call gets 401 with a WWW-Authenticate header that points to https://crm.postwyre.com/.well-known/oauth-protected-resource/api/mcp; the authorization server's metadata is at https://crm.postwyre.com/.well-known/oauth-authorization-server; clients register themselves (dynamic client registration) and must use PKCE with S256.
An access token lasts 60 minutes and is renewed with a refresh token, which changes on every use and lapses after 30 days without one. Membership and your role are checked on every call: removing you from the brand cuts the agent off there at once, and lowering your role lowers the agent's.
API keys also work on the MCP server until November 18, 2026, and then stop; each answer to a key carries a Sunset header with that date. A key reaches every brand you belong to. Keys keep working with the REST API.
Use the REST API
Owners and admins create API keys in Team, under Agent access; a key is shown once, and only a hash of it is stored. Every route under https://crm.postwyre.com/api takes the key in the Authorization: Bearer <your key> header. If you belong to more than one brand, name it with the x-workspace-id header; GET /api/workspaces lists them.
Errors always have this shape, with a stable code your agent can match on:
{ "error": { "code": "validation_error", "message": "..." } }Retry safely with idempotency keys
A call that times out may still have worked. Send an idempotency key with every create, and send the same key when you retry, so nothing is made twice.
- REST: an
Idempotency-Keyheader on create routes (deals, tasks, timeline entries, pipeline stages, bookings, proposals, invoices, help inbox notes, replies and saved replies, and new brands). - MCP: an
idempotency_keyargument on every tool that creates something. The tool list below marks them. - A key is 1 to 255 visible ASCII characters. A UUID works well.
- A retry with the same key and the same request gets the first answer back. Over REST it also carries the
Idempotent-Replayed: trueheader. - The same key with a different request is refused (422 over REST). A retry while the first call is still running is refused with 409; wait and send it again.
- Only a successful answer is kept, for 24 hours. If the first call failed, fix the request and send it again with the same key.
Rate limits
Calls made with your API keys and your agents' tokens, REST and MCP together, are limited to 300 a minute for each person. Past that, the answer is 429 with the code rate_limited and a Retry-After header that says how many seconds to wait. Wait that long, then go on.
The public booking page and help form have their own limits per connection and per brand, and send Retry-After the same way.
MCP tools
Each tool in tools/list carries the standard MCP hints: readOnlyHint, destructiveHint (it changes, replaces or cancels something that already exists), idempotentHint (the same call twice does nothing more) and openWorldHint (it reaches the connected calendar or the email provider). Your agent can use them to ask you before a destructive call. They are hints only: the server checks every call on its own.
list_workspaces- List the workspaces you belong to, with your role in each. Call this first: every other tool needs a workspace_id from this list.
search_contacts- Find past customers by name, email, phone digits or company, 50 to a page, most recently in touch first. due_only keeps people due for service now.
get_contact- One contact: jobs, when each is due, whether each channel (email, text, mail, call) may be used and why, the live offers they qualify for, notes, tags and recent activity.
update_contact- Change a contact's notes, or add and remove tags. Nothing else can change here: consent, do-not-contact and contact details are changed by a person in the app. The referrer tag is set by a person too.
reconnect_queue- Past customers with no job and no message from you in 12 months or more (or the months you give), most paid work first. People with a message already waiting for approval are left out.
due_for_service- Services coming due now, from each job type's interval in Settings, soonest first, that no reminder has been drafted for yet. Reads only: dueItemId is null for an item the due list has not stored yet (generate_due_drafts stores it).
check_consent- Whether a contact may be reached on a channel now, and why. Read from the consent records and the do-not-contact list; draft_message and approval check it again on their own.
list_eligible_offers- The workspace's live offers and whether this contact qualifies for each, with the reasons. CRMwyre decides who qualifies; offers are made by a person in the app.
draft_messagetakes idempotency_key- Write a message to one contact. It is saved as your draft, out of the owner's inbox, until you call submit_for_approval. Refused if the contact may not be reached on that channel. Nothing is ever sent without a person's OK.
attach_offer- Attach a live offer to your draft. CRMwyre checks that the contact qualifies, and adds the offer's line with its real end date as the message's last paragraph, replacing any offer attached before.
submit_for_approval- Put your draft in the owner's Today inbox to approve, edit or skip. Consent and the offer are checked again first. You cannot approve or send it.
draft_follow_up_sequencetakes idempotency_key- Write a first email and up to 4 follow-ups to one contact, each sent a number of days after the one before it went out. Every email waits for the owner's OK in Today, like any draft. Call submit_for_approval with the first draft id; each follow-up reaches Today when its day has come and the outbox runs (POST /api/outbox/run, or the owner's "Find who's due" button in Today), and the rest stop as soon as the contact replies, unsubscribes or the owner skips one. Email only.
log_activitytakes idempotency_key- Add to a contact's timeline: a note, a call, or a reply they sent by email or text. Give external_id (for example the email's Message-ID) and the same reply is recorded once however often you send it. A reply puts them under Needs a reply in Today and stops any follow-up sequence to them.
get_timeline- A contact's timeline, newest first: emails, replies, notes, calls, tasks and deal changes. group narrows it to one of email, notes, tasks, deals.
create_tasktakes idempotency_key- Add a task for the owner, optionally about a contact or a deal, optionally due on a day. Overdue tasks show in Today.
due_tasks- Open tasks due today and overdue, in the workspace's time zone.
pipeline- The pipeline board: each stage with its deals, how many and their total value in cents, summed from the deals themselves. Reads only: a brand with no stages yet gets an empty list and a next step.
create_dealtakes idempotency_key- Add a deal for a contact: what they might buy and roughly what it is worth. It starts in the first open stage unless you name one.
move_deal- Move a deal to another stage, including won or lost. Name the stage you saw it in so a move made meanwhile by someone else is not undone.
create_stagetakes idempotency_key- Add a pipeline stage to this brand (one brand per client, so each client has its own stages). outcome is open (default), won or lost. An open stage goes just before the first won or lost stage unless you give a position (0 is first). At most 20 stages.
update_stage- Rename a pipeline stage, or move it to another position on the board (0 is first). Its deals stay in it. Removing a stage is left to a person in the app.
import_leadstakes idempotency_key- Import a list of leads (for example a calling list from MastroCC) into an open pipeline stage, up to 200 rows a call. Each row has any of: name, firstName, lastName, email, phone, address, company, notes, and needs an email, a phone, or a name with an address. Each person gets one open deal, named after their company or their name; anyone who already has an open deal is left alone, so sending the same rows twice adds nothing. Someone already here gets the deal only: their contact details and company are not changed. Without commit it only previews. Leads carry no past jobs, so importing them never makes anyone emailable.
weekly_results- The client report for this brand, the same numbers its read-only client link shows: open deals by stage with totals, and the last 8 weeks (newest first, the first is the week in progress) of new deals, won, lost, meetings booked, replies, calls logged and emails sent. Each count's rule is in definitions.
revenue_summary- Paid work that came back in the last 90 days under the counting rule, against the 90 days before, by rule, by offer and by month, with the holdout comparison.
import_rows- Import past customers and their jobs, up to 200 rows a call. Each row has any of: name, firstName, lastName, email, phone, address, company, jobType, jobDate, jobValue, paidDate, jobRef, notes. Dates as YYYY-MM-DD, amounts in dollars. Previews who is new, who matches someone already here, and which rows are skipped. Only a person can write an import, in the app, because past jobs decide who may be emailed.
list_booking_services- The brand's booking page: whether it is on, its public address, its time zone and the services a customer can book, each with its length and the free time kept after it (buffer). Reads only: a brand with no page yet gets pageOn false, a null url and the next step.
find_open_times- Open start times for one service, per day, between two dates in the booking page's time zone (at most 42 days). Working hours, notice, buffers, other bookings and the connected calendar are already taken into account. Pass a time from here to book_appointment or reschedule_booking.
list_bookings- Bookings, newest start first, 50 a page. Filter by a date range in the page's time zone, by status, or by contact. Leave the dates out for all of them.
book_appointmenttakes idempotency_key- Book a contact into an open time. The contact needs an email address. The booking is made at once and added to their timeline and the connected calendar; the confirmation email waits in the owner's Today inbox until a person approves it.
reschedule_booking- Move a confirmed booking to another open time (find it with find_open_times and booking_id). The new time is checked again before it is saved. The email telling the customer waits in Today for a person's OK.
cancel_booking- Cancel a confirmed booking that has not ended. The time opens up at once and the calendar event is removed. This cannot be undone; book again to restore it. The email telling the customer waits in Today for a person's OK.
list_proposals- Proposals in this workspace, 50 to a page, most recently changed first, with who each is for, the total, and when it was shared, first opened and signed. Filter by status or contact.
get_proposal- One proposal: its sections and prices, status, total, and audit trail (shared, first opened, approved, signed). Once the customer approves an option it includes which one, who approved it and when; once signed, the signer's name, email and time. Both carry the SHA-256 of the document read.
create_proposaltakes idempotency_key- Write a proposal for one contact from sections: intro, scope, priced line items, options to choose from, and terms. Leave out blocks to start from a template's empty sections. Link it to one of the contact's deals with deal_id so the deal shows how far the customer has got. It stays a draft that only your workspace sees until a person shares it from CRMwyre.
update_proposal- Change a proposal's title or replace all its sections. Only a draft or one waiting for approval can change; a shared or signed proposal is locked so the customer signs exactly what they read.
submit_proposal_for_approval- Ask a person to read your proposal and share it with the customer. It shows as waiting for their OK in Proposals. You cannot share it, send it or sign it.
list_agreements- Agreements in this workspace, 50 to a page, newest first. An agreement is made when a customer approves an option on a proposal, and the customer signs it on their link. Each row has who it is for, the option, whether it is signed, and, while billing is on, money: paid, refunded in Stripe, kept and owed, in cents. Filter by status.
get_agreement- The agreement for one proposal: its terms (the option the customer approved), whether and when it was signed and by whom, its billing in Stripe while billing is turned on (setup invoice, paid or not, months paid, end date, and paidCents, refundedCents, keptCents, owedCents and whether it was part refunded or refunded), the revenue share's signed terms (percent, what it is of, first month, months, cap), and the onboarding tasks made when the customer paid. You can read it; only the customer signs, and only a person starts billing.
draft_agreement_copy_emailtakes idempotency_key- Put the email with the customer's signed copy of an agreement in Today, for a person to approve and send. Use it when signing could not draft it (the contact had another message waiting). Nothing is sent by this tool. Safe to repeat: one draft per agreement.
report_share_gross- Enter one month's figure for an agreement's revenue share (the number the share is a percent of, such as monthly revenue), when the client gave it to you some other way than their report link. Only for a person who is an owner or admin of the workspace. Month 1 is the month the agreement was signed; a month can be entered once it has ended, and only within the share's months. It is recorded as your agent's, by name, never as the client's. Sending a different figure for the same month records a correction (refused once the month's invoice is approved); sending the same one again changes nothing. The month's share invoice is drafted for an owner to approve; nothing is finalized or sent by this tool.
list_share_invoices- The revenue share invoices of one agreement, oldest month first: the month, the figure it bills, the amount (percent x figure, rounded to the cent, within any cap), and its status: awaiting_approval, approved (finalized in Stripe for the business to send), paid, or nothing_due (a zero month, or the cap reached). Reads only. It never returns the invoice's pay link.
approve_share_invoice- Approve one month's revenue share invoice so it is finalized in Stripe (test mode) for the business to send. Only for a person who is the workspace's owner; an admin or member is refused. It is checked again first: the share against the signed agreement, the figure against the month's newest report, the amount against percent x figure and the cap. Approving twice finalizes once. Stripe emails nobody; the business sends the invoice.
list_terms_ending- Deals whose committed term ends within within_days (default 14) of today, and those whose term already ended with nobody renewing or ending it, soonest first. The end is the signed agreement's start plus its committed months, or the day Stripe stops billing; days are counted on the workspace's own calendar. Each row has the contact, the option, ends_on (YYYY-MM-DD) and days_left (negative once past). Reads only; the same call twice gives the same answer.
draft_renewal- Draft the renewal of a deal's current committed term as a new proposal on the same deal, for a person to read, change and share. Pass the proposal_id of the signed agreement (from list_terms_ending). Left out, option_index renews the option the customer signed, at the signed monthly fee, with no setup fee. To change plan, pass option_index: another option from that proposal as the customer saw it, at its offered prices. Nothing is sent to the customer and nothing is billed. Safe to repeat: the renewal already drafted for that term comes back (created: false), never a second one.
list_support_conversations- Help requests from customers (by email or the help form), newest activity first, 50 to a page. Filter by status (open: needs an answer; waiting: answered, waiting on the customer; closed), by assignee, by a date range on the last message, or by words in the subject or the customer's name. Each row says how many drafted replies wait for a person's approval.
get_support_conversation- One help request in full: the customer (contact id, name and email, so get_contact can show their jobs and history), status, assignee, and every message in order. kind is inbound (from the customer), reply (to the customer; reply_status pending means it waits for a person's approval, approved means a person said yes) or note (internal, never shown to the customer).
draft_support_replytakes idempotency_key- Write a reply to the customer in a conversation. It waits for a person to approve, edit or discard it in CRMwyre; nothing reaches the customer without that. Up to 3 drafts can wait in one conversation; change one with revise_support_reply instead of adding more. Sign it as the business, and do not promise prices, dates or refunds the contact's records do not support.
revise_support_reply- Change the words of a drafted reply that still waits for approval. Approved or discarded replies cannot change.
update_support_conversation- Set a conversation's status (open, waiting or closed) or who on the team handles it. assignee_id null leaves it unassigned. The customer is not told.
add_support_notetakes idempotency_key- Add an internal note to a conversation for the team: what you checked, what you suggest. The customer never sees notes.
list_support_team- The people on this brand's team who can be assigned a conversation: id and name.
list_saved_replies- The team's saved replies (common answers), by name. Use one as the start of draft_support_reply and fit it to the customer.
save_replytakes idempotency_key- Add a saved reply, or change one by passing saved_reply_id. Saving sends nothing. Only a person can delete a saved reply.
get_support_setup- Where help requests come from: the forwarding address to send the business's support email to (null until the deployment has a support email domain), the help form link, and whether the form is on. Only an admin can change these, in the app. Reads only: a brand whose support was never set up gets nulls and the next step.
list_invoices- List invoices, 50 to a page, newest first. status is all, draft, open (not yet due), overdue (open and past due), paid or void. from and to (YYYY-MM-DD) filter on the issue date. paidCents is net of refunds; refundedCents is what was given back in Stripe; balanceCents is still owed (a refund reopens the balance). Amounts are in cents.
get_invoice- One invoice: its lines, tax, total, what was paid and when, what was refunded in Stripe (refundedCents, and each refund as a negative payment), what is still owed, and whether it is overdue. paidCents is net of refunds. Amounts are in cents.
create_invoicetakes idempotency_key- Write a draft invoice for a contact. CRMwyre computes every amount from the lines and the tax rate. A draft has no number and reaches no one; call issue_invoice when the owner says it is right.
update_invoice- Change a draft invoice. Send only what changes; lines, when sent, replace all the lines. An issued invoice can't change.
issue_invoice- Issue a draft invoice: it gets the next invoice number and its pay page opens. Sends nothing. After this, its lines can't change; only a person can void it.
draft_invoice_emailtakes idempotency_key- Put the email for an issued invoice in Today for the owner's OK: kind "invoice" for the first email with the pay link, "reminder" for a nudge (at most one a day per invoice). CRMwyre writes the wording. Nothing is sent until a person approves it, and it is refused if the contact already has a message waiting or must not be emailed.
update_deal- Rename a deal or change what it is worth (value_cents, or null to clear it). To move a deal between stages use move_deal.
list_contact_tasks- Every task for one contact, open and done. For what is due across the workspace use due_tasks.
update_task- Mark a task done (done: true) or open again (done: false), change its due date (due_on, or null for none), or rename it. Deleting a task is a person's call in the app.
list_drafts- Drafted messages by status: pending (waiting in Today for a person, the default), queued (approved, waiting for quiet hours or the daily cap to pass), held (stopped by a rule, with the reason) or skipped. Up to 100, oldest first. Your own drafts that you have not submitted yet are not listed.
revise_draft- Change the subject or the body of a draft that waits in Today (status pending). It still needs a person's approval. To change your own draft before you submit it, call draft_message again.
generate_due_drafts- Writes one draft for each service that has come due and has none yet, moves follow-ups whose day has come into Today, and hands emails a person already approved to the email provider. Safe to call again: nothing is drafted or handed over twice. Every new draft waits in Today for a person.
run_outbox- Moves follow-ups whose day has come into Today and hands emails a person already approved to the email provider. Safe to call on a schedule. It never approves anything, and nothing is handed over twice.
list_occasions- Holidays and seasons that fit this business in the coming weeks, plus this week's birthdays and anniversaries as counts. Each says whether drafts can be written for it now (draftable), from when (draftsFrom), and how many already wait in Today.
draft_occasion_messages- Writes the drafts for one occasion from list_occasions (its key, such as thanksgiving or birthday) for the customers it fits. Safe to call again: nobody gets the same occasion twice. Every draft waits in Today for a person.
list_offers- Every offer in this workspace, live or not, with its terms and how many times it was given and booked. To see which offers one contact qualifies for use list_eligible_offers. Only a person creates or changes an offer.
get_settings- How this workspace sends: time zone, quiet hours, daily send cap, whether Autopilot is paused and why, holiday sending, the gap between occasion messages, text message registration, and whether it holds patient data. Only an owner or admin changes these, in the app.
get_brand- The brand's name, its kind of business, and whether its first-run setup is done.
list_do_not_contact- The do-not-contact list: emails, phone numbers and domains nobody here may contact, with the reason for each. Read only: no agent adds or removes an entry. A healthcare workspace returns the count only.