Refunds & Notifications · Article 6.4
Refund stages: issued → requested → succeeded (or manual)
The credit note moves through up to four states from creation to settled. Each state corresponds to a specific point in the refund lifecycle, with predictable UI badges and email triggers.
The five-state machine on CreditNote.refund_status is the single source of truth for refund progress. Both the freelancer UI and the client emails read from it. Understanding the meaning of each state — and which transitions are possible from which — is the fastest path to diagnosing "why is my refund stuck?" questions.
Step by step
Watch the proposal Refund banner.
It mirrors the credit note's status with human-readable copy: -
pending(Stripe-flow) → "Refund in progress — checking Stripe" -pending(SEPA-flow) → "Manual refund — confirm bank transfer" -requested→ "Refund in progress — expected in 3–5 business days. Check status." -failed→ "Refund failed: [reason]. Retry or mark manually." -succeeded→ "Refund completed on [date] via Stripe." -manual→ "Refund completed on [date] via manual transfer."Use the right action per state.
pendingStripe-flow: wait or retry.pendingSEPA-flow: issue transfer + mark refunded.requested: Refresh status (article 8.7) if banner stays static for >5 minutes.failed: Retry (article 8.8) or mark manually (article 8.9).Audit via the Timeline.
Every state transition emits a Timeline event (
refund_initiated,refund_completed, etc.) with metadata (amount, method). Useful for tax inspectors or if you ever need to dispute a transaction.
Why this works this way
State transitions:
| From | To | Trigger | Side-effects |
|---|---|---|---|
| (none) | pending | Credit note created in _build_credit_note post-sign | Documents (Storno + Berichtigung + Gutschrift) created; client sees "Refund initiated" email |
pending | requested | _trigger_stripe_refund succeeded (Refund.create accepted) | stripe_refund_id and refund_initiated_at populated; banner reads "Refund in progress, expected in 3–5 business days" |
pending | failed | _trigger_stripe_refund raised an exception or charge wasn't confirmed | refund_failure_reason populated; banner offers Retry + Mark refunded manually |
pending | manual | Freelancer clicked Mark refunded (SEPA flow) | refund_completed_at and manual_refund_reason populated; client gets "refund completed" email; proposal closes if applicable |
requested | succeeded | Webhook refund.updated with status=succeeded OR pull-based sync returned succeeded | refund_completed_at populated; "refund completed" email; proposal closes |
requested | failed | Webhook with status=failed/canceled | refund_failure_reason populated; banner offers Retry + Mark refunded manually |
failed | requested | Freelancer clicked Retry Stripe refund (article 8.8) | Failure reason cleared; back to Stripe |
failed | manual | Freelancer clicked Mark refunded (after issuing SEPA out-of-band) | Same as pending → manual |
succeeded and manual are terminal — both indicate the cash leg has settled, just via different channels. The audit trail preserves which channel; the user-visible behaviour is identical (close email, proposal Paid if applicable, credit note PDF accessible).
Troubleshooting
Keep reading
Refunds & Notifications
When refunds happen: the Δ_REFUND amendment branches
A refund in Clozo is always tied to a Δ_REFUND amendment branch — partial (revised total smaller than deposit paid) or full (revised total at zero). The amendment is the legal trigger; the refund is the cash consequence.
Refunds & Notifications
Stripe automatic refund (Direct Charges via Connect)
When the original deposit was paid via Stripe, the refund is automatic. Clozo issues a Refund on the freelancer's connected account, watches for the `refund.updated` webhook, and flips the credit note to `succeeded` once Stripe confirms — typically within 3–5 business days for the cash to reach the client.
Refunds & Notifications
SEPA / out-of-band refund: when automation can't help
When the original deposit was paid via SEPA bank transfer (or any non-Stripe channel), Clozo can't refund automatically — Stripe API has nothing to refund. You issue the SEPA transfer manually from your bank, then click `Mark refunded` to update the credit note and notify the client.
Refunds & Notifications
Refresh status — pull-based sync when the Stripe webhook didn't land
Webhooks aren't perfect. When a credit note appears stuck in `requested` despite Stripe having processed the refund, click `Refresh status` to pull the current state from Stripe directly. Same transition logic as the webhook; no risk of double-firing side-effects.