Integrations · Invoicing & accounting
A REST API and signed webhooks for your invoices and the payments against them, so QuickBooks, NetSuite, Xero, an ERP or your own system stays in step with PhonePick.
On Operations and Growth, with invoices. · PhonePick starts at $75 a month. No contract, no setup fee.
What it does
Invoices come in and payments go out. Your system sends each invoice once, by its own id, and sending it again updates it rather than making a copy; PhonePick chases it by text, the customer pays on the invoice's own page, and the payment — by card, or an e-transfer or cheque your office records — is pushed back to you the moment it lands.
It is the same set of rules the PhonePick app uses, not a side door. A payment recorded through the API moves the balance, stops the reminders and shows in the invoice's history exactly as one entered by hand does, and money is always whole cents, so a $450.00 invoice can never be read as $4.50.
Setting it up
PUT /api/v1/invoices/external/{your id}, and payments with POST /api/v1/invoices/{id}/payments.GET /api/v1/events.Reference
Make a key in PhonePick under Settings → API & MCP (an owner or admin, on Operations or Growth). Send it on every request:
Authorization: Bearer pp_live_…
A read-only key can call every GET; a read and write key can also create, change and pay. Keys are shown once, stored only as a hash, and stop working the moment they are revoked. The base address is https://www.phonepick.ai/api/v1. There is no browser access (no CORS): a key belongs on a server.
_cents: "total_cents": 45000 is $450.00. A decimal such as "450.00" is refused rather than guessed at. The currency is the account's (USD or CAD) and is on every object.YYYY-MM-DD; times are ISO 8601 in UTC. "Overdue" is worked out on the business's own calendar date.external_id (your invoice or payment id). Sending the same one twice returns the record already there instead of making a duplicate, so a retry after a timeout is always safe.{"error": {"type", "message", "errors"?}} with 401 (key), 403 (plan or read-only key), 404, 409 (e.g. voiding an invoice that has payments), 422 (invalid — errors names every problem) and 429 (over 300 requests a minute).{"object":"list","data":[…],"has_more","next_cursor"}; pass starting_after=<next_cursor> for the next page. limit is up to 100.{
"object": "invoice", "id": "Xq3…", "number": "INV-1042", "external_id": "QB-1042",
"status": "partially_paid", // open | partially_paid | paid | void
"overdue": true, "days_overdue": 13, "currency": "CAD",
"total_cents": 45000, "paid_cents": 20000, "balance_cents": 25000,
"customer": { "name": "Ana Ruiz", "phone": "+15195550101", "email": "ana@example.com" },
"issue_date": "2026-09-15", "due_date": "2026-10-01",
"items": [{ "description": "Furnace service", "quantity": 1, "unit_cents": 45000, "amount_cents": 45000 }],
"notes": null, "pay_url": "https://www.phonepick.ai/i/…",
"reminders_sent": 1, "last_reminded_at": "…", "source": "api",
"created_at": "…", "updated_at": "…", "paid_at": null, "voided_at": null
}
pay_url is the invoice's own page: the customer sees it and, if Stripe is connected, pays what's left by card. It never expires. A mobile number is needed only for text reminders; an invoice without one is still tracked and payable.
{ "object": "payment", "id": "…", "invoice_id": "Xq3…", "kind": "payment", // or "refund"
"amount_cents": 20000, "currency": "CAD", "paid_on": "2026-10-10",
"method": "etransfer", // card | cash | cheque | etransfer | bank | other
"reference": "CA-778", "note": null, "external_id": "PMT-88",
"source": "api", // manual | api | stripe
"removed": false, "created_at": "…" }
A payment can't be more than the balance (a refund can't be more than what was paid). When the balance reaches zero the invoice is paid and reminders stop. Card payments made on the invoice's page arrive with source: "stripe", and a refund made in the business's Stripe dashboard arrives as a refund.
| Request | What it does |
|---|---|
GET /me | The account and key: name, currency, time zone, plan. |
GET /summary | Outstanding, overdue, collected this month, and the aging buckets. |
GET /invoices | Newest first. Filters: status (open, partially_paid, overdue, paid, void), q (search), number, external_id, updated_since (ISO time or epoch ms — oldest change first). |
POST /invoices | Create. customer.name, due_date, and total_cents or items. 201 created; 200 if its external_id was already here. |
PUT /invoices/external/{external_id} | Create or update by your id — the one call an ERP sync needs. |
GET /invoices/external/{external_id} | One invoice by your id. |
GET /invoices/{id} | One invoice, with its payments. |
PATCH /invoices/{id} | Change only the fields sent. The total can't drop below what's been paid. |
POST /invoices/{id}/void · /reopen | Cancel (no payments on it) or restore. |
POST /invoices/{id}/remind | Text the customer a reminder now: 9am–8pm business time, one a day, needs a mobile. |
DELETE /invoices/{id} | Gone for good, with its payments, history and page. |
GET /invoices/{id}/payments | Its payments and refunds. |
POST /invoices/{id}/payments | Record a payment or refund: amount_cents, method, paid_on, reference, external_id, kind. |
DELETE /invoices/{id}/payments/{payment_id} | Take off one entered by mistake (kept, marked removed). Card payments are refunded in Stripe instead. |
GET /payments | Every payment on every invoice, oldest first; since (ISO or epoch ms). |
GET /events | The change feed, in order; after=<next_cursor>. Kept 30 days. |
Invoices in: send each one with PUT /invoices/external/{your id} whenever it is raised or changed. Payments out: either take webhooks (below) or poll GET /events?after=… and store next_cursor each time. The feed holds back the last five seconds, so a cursor never skips an event that was still being written.
curl -X PUT https://www.phonepick.ai/api/v1/invoices/external/INV-1042 \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"number":"INV-1042","customer":{"name":"Ana Ruiz","phone":"+15195550101"},
"total_cents":45000,"due_date":"2026-11-15"}'
Add an https address under Settings → API & MCP and pick the events (all, or any of invoice.created, invoice.updated, invoice.paid, invoice.voided, invoice.deleted, invoice.reminded, payment.created, payment.refunded, payment.deleted). Each event is POSTed as JSON — {"id","object":"event","type","created_at","data":{"object":…}}, with the invoice too on payment events — usually within a minute. Answer with any 2xx. Anything else is retried after 1 minute, 5 minutes, 30 minutes, 2, 6, 12 and 24 hours; an address that has failed for three days is switched off, and the app says so.
Check every request. PhonePick-Signature: t=<unix time>,v1=<hex> is an HMAC-SHA256, with the address's signing secret, of t + "." + the raw body — the scheme Stripe uses:
// Node
const crypto = require("crypto");
function verified(header, rawBody, secret) {
const p = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
if (Math.abs(Date.now() / 1000 - Number(p.t)) > 300) return false; // replayed
const want = crypto.createHmac("sha256", secret).update(`${p.t}.${rawBody}`).digest("hex");
const a = Buffer.from(want), b = Buffer.from(p.v1 || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
# Python
import hmac, hashlib, time
def verified(header, raw_body: bytes, secret: str) -> bool:
p = dict(kv.split("=", 1) for kv in header.split(","))
if abs(time.time() - int(p["t"])) > 300: return False
want = hmac.new(secret.encode(), f'{p["t"]}.'.encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(want, p.get("v1", ""))
Use PhonePick-Event-Id to ignore a delivery you have already handled.
The MCP server is https://www.phonepick.ai/mcp (Streamable HTTP, the same key as a Bearer header). For a client that can only be given a link, https://www.phonepick.ai/mcp/<key> works too — treat that link as a password. Its tools: get_account, get_receivables_summary, list_invoices, get_invoice, list_changes, and with a write key create_invoice, update_invoice, record_payment, remove_payment, void_invoice and send_reminder. Tool inputs take dollars (450.00); results carry cents. Deleting an invoice is not offered to an assistant.
claude mcp add --transport http phonepick https://www.phonepick.ai/mcp \
--header "Authorization: Bearer pp_live_…"
Being straight with you
It covers invoices and payments only. Calls, leads, appointments and customers are not in the API yet, and it is not a live connection to any one accounting product on its own — something on your side (your developer, Zapier, Make, n8n) does the sending. There is no sandbox account: a key works on your real data, so try things with a read-only key first.
Invoicing & accounting
Export your invoice list from QuickBooks, import the CSV into PhonePick, and overdue invoices start chasing themselves.
How it works →CSV import, same as QuickBooks: export your invoices from Wave and import them under Money → Invoices. No live sync.
No page yet — ask us for itCSV import, same as QuickBooks: export your invoices and import them under Money → Invoices. No live sync.
No page yet — ask us for itGive Claude, ChatGPT or any assistant that speaks MCP your PhonePick invoices as tools: what is overdue, record a payment, text a reminder.
How it works →Tell us which one and what you would do with it. That is genuinely how the order gets decided — there is no roadmap vote more useful than a business saying it would switch for it.
Every product named on this page is a trademark of its owner. Naming one here says PhonePick works with it, or does not — it does not say they have endorsed, reviewed or heard of us.