Appearance
Home › Help › Wave & QuickBooks invoicing integration
Wave & QuickBooks invoicing integration
If you send invoices through Wave (waveapps.com) or QuickBooks Online, ShootCal can watch for them getting paid and automatically mark the matching session's deposit as received. You do not have to open ShootCal and tick the box yourself. Keep invoicing where you already do it, with no double entry and no manually tracking who has paid. The bulk of this article walks through the Wave sync in detail; QuickBooks Online works the same way and is covered at the end.
You connect Wave inside ShootCal. Open Settings and choose Connect Wave (this uses Wave's own connect button, which requires Wave Pro). Once connected, the sync runs automatically in the background.
Here is the idea in plain terms. Wave is where your client actually pays. ShootCal periodically checks your Wave account for invoices that have received any payment. When it sees one, it looks through your upcoming bookings for the session that belongs to that customer. If it finds a match that is still waiting on its deposit, it flips that booking's deposit to "received" for you, right on the booking, exactly as if you had ticked the box yourself. That same paid flag is what colors your availability and shows up across the web app and the Mac and iPhone apps, so everything stays in agreement.
It pairs with ShootCal's public availability calendar, where only deposit-paid days show as booked, so a day flips to booked the moment your client's payment lands.
Two things worth knowing up front. First, it only touches future bookings that are still waiting on a deposit. It never reaches back to mark old or completed shoots, and it never un-marks anything. Second, ShootCal only ever reads from Wave. The single change it makes lives entirely inside ShootCal (on the booking's Google Calendar event). It does not edit, send, or void anything in Wave.
The end to end flow
A background worker runs on a schedule (roughly every 10 minutes). Each run does this:
- Each run exits early only if neither the Wave nor the QuickBooks integration is turned on. Otherwise it processes every connected Wave account and every connected QuickBooks account independently: each connection you made in Settings supplies its own securely stored token and business, so your sync never depends on anyone else's setup. 2. It asks Wave for recent PAID invoices. "Paid" here means the invoice has received any payment at all, including a partial one. The look-back window is the last 60 days of modified invoices. 3. For each paid invoice, it first checks whether that invoice has already been recorded as processed. If so, it skips it (this is the idempotency guard, so an invoice is never acted on twice). 4. It tries to find the matching ShootCal booking among your upcoming Google Calendar events, using email first and then name (see the matching section). 5. If it finds a future event that is still pending its deposit, it writes the deposit flag onto that event via the Google Calendar API, marking it paid. 6. On a successful write it records the invoice as processed. On a failed write it deliberately does NOT record it, so the next run retries (as long as the invoice is still inside the 60-day window).
The process always exits cleanly so a transient failure never spams the scheduler, and dry-run mode logs exactly what WOULD be marked without writing anything.
The matching rule: email first, then a word-boundary name match
For each paid invoice the worker pulls the customer's name and email off the Wave invoice, then searches your primary Google Calendar from the start of today (UTC) out to about 400 days ahead. It uses Google's full-text search as a coarse filter and then re-verifies the match itself, because that search is fuzzy.
EMAIL match (preferred, because email is unique and reliable): the customer's email must literally appear in the event's description. ShootCal booking-page events carry the client email in the description, so this is the strong, structural match. The comparison is case-insensitive.
NAME match (fallback, only used when there is no email match): the whole customer name must appear in the event TITLE as a whole-word match, AND the event must already carry an explicit UNPAID deposit flag. Two guards make this safe. The whole-word match stops a short name like "Sam" from matching "Samantha". The unpaid-flag gate means the event has to be a real ShootCal booking that is genuinely awaiting its deposit, so a random calendar entry that happens to share a name will not get stamped paid.
Among all qualifying events the worker picks the SOONEST one. It only ever considers future events that are not already marked paid, so it never touches past or completed shoots and never re-marks an already-paid booking.
The GraphQL polling against Wave
ShootCal talks to Wave with a deliberately minimal, READ-ONLY GraphQL client. It calls Wave's public GraphQL endpoint and authenticates with a bearer token. Every call passes its values as GraphQL variables rather than inlining them, because Wave's API requires it.
The invoice query asks for your business's invoices, sorted newest-modified first, and pages through the results (by default up to 5 pages of 50). Because results are newest-modified first, it stops paging as soon as it sees an invoice modified before the cutoff timestamp, which keeps the poll cheap even on a 500-plus invoice account. For each invoice it reads the id, invoice number, status, last-modified time, amount paid (in cents), and the customer name and email. It keeps only invoices that have received some payment.
The request uses a 25-second timeout, logs any HTTP or GraphQL errors to the server log, and returns nothing on any transport or GraphQL failure so a bad poll degrades gracefully rather than throwing.
What ShootCal stores
Your Wave connection is stored encrypted, tied to your ShootCal account, and used only to read your invoices. ShootCal also keeps a small record of which invoices it has already acted on, so a payment is never counted twice. The one and only thing it ever writes is the deposit flag on the matched Google Calendar event, the exact same flag the web and native apps already use, so the paid state stays consistent everywhere.
Safety properties baked into the design
Read-only on Wave: the Wave client only ever issues read queries. The sole write in the entire flow is the deposit flag on one Google Calendar event.
Future-only: the event search starts at the beginning of today (UTC), so past or completed shoots are never marked.
Never un-marks and never double-marks: events already considered paid are skipped, and each invoice is recorded once so it is acted on at most once.
Fail-safe writes: an invoice is recorded as processed only after a successful Google write. A transient failure (a server error, a token expiring mid-run, a network blip) leaves it unrecorded so a later run retries it inside the 60-day window, rather than silently losing a paid deposit.
Dry-run and baseline modes: dry-run logs every match it would make without writing. A one-time baseline run records all currently-paid invoices as already processed so existing payments are not retroactively marked, meaning only deposits that become paid AFTER go-live trigger a mark.
FAQ
What exactly counts as "paid" enough to trigger a deposit mark?
Any payment at all. The worker pulls invoices where Wave reports any amount paid (read in cents). It does not require the invoice to be fully paid, and it does not compare the amount against your deposit figure. A partial payment on an invoice is enough to flip the matching booking's deposit to received.
What happens if a customer's name matches more than one upcoming event?
Among all qualifying events (future, not already paid, and passing the email or name guard) it marks only the SOONEST one. For a name-based match the event also has to already carry an explicit unpaid deposit flag, so unrelated calendar entries that share a name are not eligible.
How does it avoid marking the same invoice twice, or re-marking after I manually unmark a deposit?
Every successfully handled invoice is recorded once server-side, and each run skips any invoice already recorded. So once an invoice has been acted on, a later poll will not touch it again even if you later change the deposit state by hand. The invoice, not the event, is the idempotency key.
Will it ever change anything in my Wave account?
No. The Wave client only issues read queries against Wave's GraphQL API (invoices, payments, customer name and email). The only write in the whole flow is setting the deposit flag to paid on the matched Google Calendar event inside ShootCal.
Why does the name fallback require an existing unpaid deposit flag but the email path does not?
They have different structural guards. The email path is already strong because it requires the client's exact email to be present in the event description, which only real ShootCal booking events carry. The name path is weaker (a name in a title), so it adds two protections: a whole-word match (so "Sam" will not hit "Samantha") and a requirement that the event already be flagged as awaiting its deposit, which confirms it is a genuine pending ShootCal booking rather than a coincidental name collision.
Do I need Wave Pro to connect Wave?
Yes. You connect Wave from ShootCal Settings using Wave's own connect button, and Wave requires your business to be on Wave Pro to use it. Once connected, ShootCal only reads from Wave, so the connection's whole job is watching your invoices and payments.
Could it mark a deposit on a shoot that already happened?
No. The event search begins at the start of the current day in UTC and only looks forward (out to about 400 days), so past and completed shoots are excluded by design.
What happens if the deposit mark fails to write, for example my Google token expired mid-run?
The invoice is deliberately left unrecorded on any failed write, so the next scheduled run retries it as long as it is still within the 60-day look-back window. This prevents a transient blip from permanently losing a paid deposit.
How often does it check, and how far back does it look?
The worker runs on a schedule, roughly every 10 minutes. Each run re-checks Wave invoices modified in the last 60 days, and the Wave query pages newest-modified first and stops early once it passes that cutoff, so the poll stays cheap even on large invoice histories.
Where is the actual deposit state stored, and will the apps agree with it?
It is stored as a private extended property on the booking's Google Calendar event, set to paid. This is the identical flag the web app and the native Mac/iOS apps read and write, so a deposit marked by the Wave sync shows up consistently across every surface.
QuickBooks Online works the same way
If QuickBooks Online is where you invoice instead of Wave, ShootCal syncs deposits from QuickBooks too, and it is available today. It works exactly like the Wave sync: when a client pays their QuickBooks invoice, ShootCal automatically marks that booking's deposit as received, right on the booking. The same background worker runs on the same roughly-ten-minute schedule, uses the same email-first then whole-word-name matching against your upcoming bookings, only ever touches future events still waiting on a deposit, and writes nothing except that one deposit flag on the matched Google Calendar event.
Just like the Wave connection, the QuickBooks Online connection is read-only. ShootCal only reads your paid invoices, their customer name and email, and never changes, sends, or voids anything in QuickBooks. You connect it in ShootCal Settings with QuickBooks' own secure sign-in (nothing to copy or paste), and it works with any QuickBooks Online plan. It also pairs with ShootCal's public availability calendar, where only deposit-paid days show as booked, so a day flips to booked the moment your client's payment lands.
You connect one accounting tool at a time, either Wave or QuickBooks Online, since a booking only has one deposit to mark. If you ever switch, disconnect one in Settings and connect the other; the sync picks up from there. Both are live today. ← Back to Help Center