# 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.

## What your agent can do

Postwyre CRM has an MCP server and a REST API. Both take the same 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.

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, the MCP tools return ids and counts only, and refuse anything that would hand patient details to an agent. Use MCP rather than REST for such a brand.

## Connect over MCP

- Sign in to Postwyre CRM and open Team. Owners and admins see it.
- Under Agent access, create a key and copy it when it is shown.
- In your agent's MCP settings, add a remote server with the address `https://postwyre-crm.thatfellowinc.workers.dev/api/mcp` and the header `Authorization: Bearer <your key>`.
- Your agent calls `list_workspaces` first. Every other tool takes the `workspace_id` of one of those brands.

The server speaks Streamable HTTP, protocol versions 2025-11-25, 2025-06-18, 2025-03-26. One key reaches every brand you belong to. Membership is checked on every call, so removing you from a brand cuts the key off there at once.

## Use the REST API

Every route under `https://postwyre-crm.thatfellowinc.workers.dev/api` takes the same 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:

```json
{ "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-Key` header 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_key` argument 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: true` header.
- 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, REST and MCP together, are limited to 300 a minute for each person who owns keys. 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

- `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.
- `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_message` (takes `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_sequence` (takes `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_activity` (takes `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_task` (takes `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.
- `create_deal` (takes `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_stage` (takes `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_leads` (takes `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).
- `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_appointment` (takes `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, signed). Once signed it includes the signer's name, email and time, and the SHA-256 of the document they signed.
- `create_proposal` (takes `idempotency_key`): Write a proposal for one contact from sections: intro, scope, priced line items and terms. Leave out blocks to start from those four empty sections. 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_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_reply` (takes `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_note` (takes `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_reply` (takes `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.
- `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. Amounts are in cents.
- `get_invoice`: One invoice: its lines, tax, total, what was paid and when, what is still owed, and whether it is overdue. Amounts are in cents.
- `create_invoice` (takes `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_email` (takes `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.

The same page for people: https://postwyre-crm.thatfellowinc.workers.dev/docs/agents
