Offer approval and brand payment

End-to-end path from a settled conversation to dual Approve and Send Payment

This is the complete product path for a deal that starts in chat: a formal one-page offer, two independent Approves, then brand pay-in. Stripe Connect (talent payouts) is a later, separate step — it is not part of Accept or of charging the brand.

Adaptive UI prompts:

  • Pay-in path: tests/manual/volt-hydration-maya-ortiz-offer-pay.md
  • Re-approve after a rewrite, and Offer-tab Counter as chat: tests/manual/zly-1932-offer-rewrite-requires-reapprove.md

End-to-end

1. Chat until a formal offer exists

A brand opens the talent's public Z-Link as a guest and talks the deal. The product may hold the thread behind a login gate; that is expected. After the brand signs up through that gate, the same conversation continues.

Until Offer-Gen writes a formal offer, the conversation Offer tab stays disabled. After generation it is usable on both sides (/dashboard/agent/conversations/:roomId?view=offer).

Chat agreement is not a stage. Distil records a principal's chat yes as an instruction; the deal's stage is the written offer (NO_WRITTEN_OFFER until Offer-Gen mints the row). Minting the document is a physical-layer effect when the negotiation goal flips to met. Chat is not where anyone signs or pays. See Deals move through explicit physical stages.

2. The one-page offer document

The Offer surface is a single document: payment, when, where, brief, content tiles, terms, Z recommends, and a footer. Status labels such as Waiting for your approval / Approved by both are product copy. In the database, APPROVED still means "submitted to the talent," not "the talent said yes."

The footer is role-specific. Approve writes the physical offer. Counter, Reject, and Add on a Z recommends item do not — they send a message to that side's agent (see below).

3. Dual Approve (order matters)

WhoClickWhat it doesAgent stageWhat it does not do
Brand firstApproveSets brand_approved_at. Footer becomes Approval sent / waiting for talent.BRAND_ACKED_AWAITING_TALENTDoes not charge a card. Does not open Send Payment. Does not move status off APPROVED.
TalentApproveAPPROVED → ACCEPTED. This is the booking.BOOKED_AWAITING_PAYMENTDoes not require Stripe Connect or KYC. Does not pay anyone out.
Brand after acceptView paymentOpens the Send Payment side panel.still BOOKED_AWAITING_PAYMENT until the cardIs not a second Approve.

Brand-first Approve is acknowledgement only. Reloading Offer still shows thanks / waiting for talent, not a pay control, until the talent has accepted.

Send Payment appears for the brand only while status is ACCEPTED. If the talent Approves first, the brand footer skips a second Approve and goes straight to View payment.

Talent Accept does not call Connect. Connect is required later to payout (and on some complete/release paths), not to accept or to take the brand's card. See Stripe Connect (Talent Payouts).

Talent Approve on an APPROVED row also requires a current brand stamp (canAcceptOfferAsTalent). A stamp from an earlier version does not count.

4. A rewrite cancels a one-sided brand yes

brand_approved_at is tied to the terms it was stamped on. Writers clear it (BRAND_APPROVAL_CLEARED) when those terms change while the deal is still waiting on talent:

  • Z rewriting the written terms after the actor asked in chat (including Add on a Z recommends item, Counter, or a typed rewrite)
  • Offer-Update or another material rewrite of fee, brief, usage, or the document body

Refreshing the recommend list, or tapping Add without Z rewriting, is not a rewrite.

After a clear:

  • Brand footer shows Approve again, not Approval sent.
  • The brand chip waits on the brand.
  • Talent Approve is gone. Counter and Reject remain.
  • Agents are woken on the rewrite path so both sides hear that both must approve this version.

The brand must Approve the new version before the talent can book.

5. Only Approve writes directly

On the conversation Offer tab, only Approve writes the written offer directly. Counter, Reject, and Add on a Z recommends item are messages to Z: the click or sheet switches to Conversation and sends the request as a chat line to that side's agent. Z then runs last-look: it asks whether there is anything else to add before the request goes to the other side, and only after the answer does it act on the written offer. The person can answer that question in the chat at their own pace.

ClickWhat it doesWhen the offer changes
CounterSwitches to Conversation and sends a chat line (amount + why). No success message.After the answer: the offer shows as Countered, the counter is kept as the pending proposal, the other side is notified, and any earlier brand approval is cleared. The fee is untouched until a side accepts.
RejectSwitches to Conversation and sends a decline line to that side's agent. No success message.Only if the negotiation is over does the written offer close.
Add (Z recommend)Switches to Conversation and sends a chat line (title + why). No success message.Only when Z rewrites the document. The click alone changes nothing.
ApproveWrites the written offer directly.On the click.

Z treats those lines as if they had been typed in chat. Last-look still applies to every one of them.

A counter never lands on the written offer from the click alone. Because the change waits for Z's last-look, the Countered label, the deal stage, the conversation list, and the other side's notification all update at the same moment, after the person has confirmed.

Z-link forms and the automatic below-threshold counter still counter straight away. Only the conversation Offer tab goes through the chat first.

6. Send Payment

showSendPayment is true only for the brand while the offer is ACCEPTED (packages/offers/client/.../offer-document/footer-state.ts). The footer button is labeled View payment; the panel title is Send Payment.

The panel charges the due phase, not always the full deal:

  • Talent slice due now (example: 50% on approval = half of $4,500 → $2,250)
  • Zooly fee = 20% of that due talent slice ($450)
  • Charged today = slice + fee ($2,700)
  • Later phases are listed as not due yet

Helpers: offerDueCharge, zoolyFeeMinorUnit, brandChargeMinorUnit in @zooly/types. Checkout: POST /api/payments/create-intent with payFor: "OFFER" requires status ACCEPTED.

The card form is the shared StripePaymentForm (name, number, expiry, CVC, Pay). Paying does not walk hosted Connect.

7. After a successful card

Offer pay-in is authorize-then-capture:

  1. Stripe holds the authorization.
  2. completePayment / webhook moves the offer to PENDING_PAY_CAPTURE (awaitingAdminCapture: true). That is successful payment-in (BOOKED_PAYMENT_HELD to the agents).
  3. Admin capture (or auto) later moves it to PAID (BOOKED_PAID).
  4. A declined or voided auth (including the 7-day expiry cron) returns the offer to ACCEPTED (BOOKED_AWAITING_PAYMENT).

While the offer is PENDING_PAY_CAPTURE or later, View payment is gone. The brand cannot start a second charge for the same due phase.

Share tracking is still deferred until delivery is accepted (releaseOfferEscrowForOffer). See Payment Flows — Offer marketplace payments.

8. How the talent gets the money

The brand is charged as described above. What changes on the way out is when the talent receives it.

  • An agreed deal can carry a schedule: for example half on signing and half when the post is live, or a deposit now and the rest on delivery. The team sets it from the deal's payment wording; it is not added on its own.
  • Each part is released when its trigger happens and the proof the agreement names is attached and approved. A part can also simply wait for the brand's payment.
  • Offers with no schedule pay out exactly as they did before: once, after the brand accepts delivery.
  • If the talent (or their agent, when the agent earns a share) cannot receive money yet, the due part waits, they are asked once to finish setting up payouts, and the money goes out when they do. The brand is never blocked on it.
  • Every release, and every failed release, appears on the deal timeline.

See Staged payouts.

9. A booked offer is frozen in this conversation

After talent Approve (BOOKED_*, including payment held or paid), chat cannot change this written offer. Offer-Update still only touches DRAFT / APPROVED. If someone asks to change a booked or paid deal, Z refuses and asks whether they want a new offer. Yes opens a second conversation for the same two sides; the original Offer tab stays as booked. See Sibling rooms for a new offer.

One-sided rewrite before talent Approve is a different rule: the brand stamp is cleared and they must approve again.