Home › Help › Wave invoicing integration
Wave invoicing integration
If you send invoices through Wave (waveapps.com), 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.
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. 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.
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.
It also works on any Wave account, including the free tier, because the connection uses a personal access token rather than requiring Wave's paid plan. Today this runs as a single-account setup: the token is configured on the ShootCal server (the "Phase 1" worker described below), not pasted in through the app yet. A per-user in-app "Connect with Wave" flow is scaffolded but not active.
The end to end flow
A background worker runs on a schedule (roughly every 10 minutes). Each run does this:
1. It loads its configuration and exits immediately unless the integration is enabled and a token, a business id, and a target ShootCal user are all present. 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.
Where the connection, token, and state actually live
There are two storage paths, matching the two phases of the design.
Phase 1 (the current dogfood path, and the only active one) keeps everything in the server configuration. The relevant settings are: whether the integration is enabled (off by default), whether it is in dry-run mode (on by default), the Wave token, the Wave business id, and the target ShootCal web user whose calendar these invoices map to. This is a single full-access token for one account.
Phase 2 (the per-user in-app "Connect with Wave" flow) is provisioned by a per-user connections table. One row per connected web user, storing the connection method, the encrypted access and refresh tokens, the token expiry, the business id and name, and timestamps. Tokens are encrypted at rest. The method is either OAuth (the Connect-with-Wave button, which Wave requires the business to be on Wave Pro for) or token (a pasted personal full-access token, which works on free accounts). The OAuth client settings also exist in configuration. Note: this table and the OAuth settings exist, but there is no live screen or route wiring them up yet, so Phase 2 is provisioned but not active.
Processing state is recorded per invoice: one record per Wave invoice already acted on, keyed by the invoice id, alongside the target user, the customer email, the matched event id, and when it was processed. This is the idempotency record.
The one and only write target is the matched Google Calendar event. The deposit value is stored as a private extended property on that event, set to paid, which is the exact same flag the web and native apps read and write, so the paid state is consistent everywhere.
Why it works on free Wave accounts
Wave's OAuth Connect-with-Wave button requires the connecting business to be on Wave Pro. To avoid locking free-tier users out, the design supports a second connection method: a personal full-access token that you generate in Wave. The connection record notes which method was used, and the personal-token path works on FREE Wave accounts. The in-app paste form for that token belongs to Phase 2, which is scaffolded but not active yet. The Phase 1 worker that runs today already uses a personal token set in the server config, so the free-account path is the same underlying mechanism that powers the current sync. Since all of Wave's role here is reading invoices and payments, a read-capable personal token is all that is needed.
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, or does a free Wave account work?
A free account works. The OAuth Connect-with-Wave button requires Wave Pro, but the integration also supports a personal full-access token, which works on free accounts. The Phase 1 worker that runs today already uses such a token, set in the ShootCal server config rather than pasted in through the app (the in-app connect screen is Phase 2 and not active yet).
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.