Skip to content
PaidFinch

API and webhooks

Create invoices from your own systems, collect the payments through PaidFinch, and get told as they are viewed, paid, refunded, or overdue. REST over HTTPS, JSON in and out, versioned at /v1. Part of the Pro plan.

Contract: openapi.yaml (OpenAPI 3.1). Keys and webhook endpoints live in Settings, Developers.

The basics

  • Base URL: https://app.paidfinch.com/api/v1. Staging: https://staging-app.paidfinch.com/api/v1, where Stripe runs in test mode so you can build with no real money.
  • Authenticate with Authorization: Bearer pf_live_…. Keys start pf_live_ in production and pf_test_ on staging, and each environment refuses the other's. A key belongs to one business and carries scopes; a request outside them returns 403.
  • Money is integer cents. Dates are YYYY-MM-DD. Timestamps are UTC with a Z. Ids are prefixed and unguessable: biz_, cli_, inv_, ins_, pay_, ref_, evt_.
  • Every create accepts an Idempotency-Key header. The same key and body returns the original response for 24 hours; the same key with a different body returns 422.
  • Lists use cursor pagination: limit (default 25, max 100) and starting_after, answering { "data": [...], "has_more": true, "next_cursor": "..." }.
  • Rate limit: 120 requests a minute per key, with X-RateLimit-Limit, X-RateLimit-Remaining, and Retry-After on a 429.
  • Unknown fields return 422, so a typo never silently does nothing. Server to server only: CORS is off and a key must never reach a browser.

Errors

One shape, with a request id to quote to support:

{ "error": { "type": "invalid_request", "code": "parameter_invalid", "message": "unit_price_cents must be a positive integer", "param": "items.0.unit_price_cents", "request_id": "req_01J..." } }

Status codes: 400, 401, 403, 404, 409, 422, 429, 5xx. Types: invalid_request, authentication, forbidden, not_found, conflict, rate_limited, api_error.

Quickstart: a booking becomes an invoice

A customer accepts a quote in your booking system. Create the invoice with a deposit and a balance, let PaidFinch email it, and watch the webhooks.

POST /api/v1/invoices
Authorization: Bearer pf_test_...
Idempotency-Key: quote-1042-accepted

{
  "external_source": "flourcity-booking",
  "external_id": "quote_1042",
  "client": { "external_id": "customer_377", "name": "Taylor and Jordan Reed", "email": "taylor@example.com" },
  "issue_date": "2026-11-03",
  "items": [
    { "description": "The Signature, 5 hours (wedding package)", "quantity": 1, "unit_price_cents": 69900 },
    { "description": "Additional rental hour", "quantity": 1, "unit_price_cents": 9900 }
  ],
  "payment_plan": { "installments": [
    { "label": "Deposit to hold your date", "amount_cents": 20000, "due_date": "2026-11-03", "note": "Fully refundable until we confirm your booking." },
    { "label": "Balance", "amount_cents": null, "due_date": "2027-05-29" }
  ] },
  "return_url": "https://flourcityphotobooth.com/bookings/1042",
  "metadata": { "booking_id": "1042" },
  "send": { "email": true, "message": "Thanks for choosing us. Your invoice and deposit link are below." }
}

The response is the invoice object, with pay_url, the numbered installments, and version. Retry the same request with the same Idempotency-Key and you get the same invoice. Send it again with a new key and you get 409 duplicate_external_id with existing_invoice_id, so a quote is never billed twice.

When the client pays the deposit you receive payment.succeeded and installment.paid; when the balance is paid, invoice.paid. Never trust the return_url redirect for payment status: use webhooks or GET /invoices/{id}.

Webhooks

Add an HTTPS endpoint in Settings, Developers, choose the event types (or all), and copy its signing secret. Every event carries the full current invoice, so you can treat events as upserts.

{
  "id": "evt_01J...",
  "type": "payment.succeeded",
  "created": "2026-11-03T15:20:11Z",
  "business_id": "biz_01J...",
  "data": {
    "payment": { "id": "pay_01J...", "amount_cents": 20000, "method": "stripe", "installment_id": "ins_01J...", "paid_at": "2026-11-03T15:20:09Z" },
    "invoice": { "...the full invoice object..." }
  }
}

Event types

TypeWhen
invoice.createdAn invoice was created (through the API or in the app).
invoice.sentThe invoice email went out, or was resent.
invoice.viewedThe client opened the pay page for the first time.
invoice.updatedThe invoice changed after being finalized: an edit, a discount, a late fee, or credit applied.
invoice.partially_paidA payment landed but a balance remains.
invoice.paidThe balance reached zero.
invoice.overdueThe invoice first became overdue.
invoice.voidedThe invoice was voided, with a reason.
payment.succeededA payment was received online or recorded by hand. data.payment carries it.
payment.failedAn online or automatic payment failed. data.failure has the reason.
payment.refundedA payment was refunded (data.refund) or a manual payment reversed.
installment.paidOne installment of a payment plan was settled. data.installment carries it.
installment.overdueOne installment passed its due date unpaid.

Signature

Header PaidFinch-Signature: t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of "{t}.{raw request body}" with your endpoint secret. Verify against the raw body and reject timestamps older than 5 minutes. During a secret rotation the header carries two v1 values for 24 hours.

// PHP example. Verify the raw body before parsing it.
$body = file_get_contents('php://input');
$header = $_SERVER['HTTP_PAIDFINCH_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $parts);          // t=..., v1=...
$t = (int) ($parts['t'] ?? 0);
if (abs(time() - $t) > 300) { http_response_code(400); exit; }  // too old
$expected = hash_hmac('sha256', "{$t}.{$body}", $secret);
$ok = false;
foreach (explode(',', $header) as $part) {
    if (str_starts_with($part, 'v1=') && hash_equals($expected, substr($part, 3))) { $ok = true; }
}
if (! $ok) { http_response_code(400); exit; }
$event = json_decode($body, true);
// Dedupe on $event['id']; use $event['data']['invoice']['version'] to ignore stale events.
http_response_code(200);

Delivery

  • At least once. Dedupe on the evt_ id. Ordering is not guaranteed, so compare invoice.version.
  • A 2xx within 10 seconds is success. Anything else is retried: right away, then after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, and 24 hours.
  • An endpoint that fails every delivery for 3 days is switched off and you are emailed. Re-enabling offers to resend what it missed.
  • Redirects are not followed. Private and local addresses are refused. GET /events returns the same events for at least 30 days, for reconciliation.

Reference

Account

GET /me Who am I

The key's business, scopes, and environment. A quick connectivity check.

Responses: 200 Me 401 Error

Clients

GET /clients List clients scope clients:read

Parameters: limit (query), starting_after (query), external_id (query), external_source (query), email (query)

Responses: 200 list of Client

POST /clients Create or update a client scope clients:write

Matches an existing client by external_source and external_id, then by email; otherwise creates one.

Parameters: Idempotency-Key (header)

Body: ClientInput

Responses: 200 Client 201 Client 422 Error

GET /clients/{id} Get a client scope clients:read

Parameters: id (path, required)

Responses: 200 Client 404 Error

PATCH /clients/{id} Update a client scope clients:write

Parameters: id (path, required)

Body: ClientInput

Responses: 200 Client 404 Error 422 Error

Invoices

GET /invoices List invoices scope invoices:read

Parameters: limit (query), starting_after (query), status (query), external_id (query), external_source (query), client_id (query), created_after (query), updated_after (query)

Responses: 200 list of Invoice

POST /invoices Create an invoice scope invoices:write

The server computes every total. external_source plus external_id is unique per business: a second create for the same pair returns 409 duplicate_external_id with existing_invoice_id. Unknown fields return 422.

Parameters: Idempotency-Key (header)

Body: InvoiceInput

Responses: 201 Invoice 409 Error 422 Error

GET /invoices/{id} Get an invoice scope invoices:read

Parameters: id (path, required)

Responses: 200 Invoice 404 Error

PATCH /invoices/{id} Update an invoice scope invoices:write

A draft changes freely. An open invoice follows the editing rules: the total cannot drop below what has been paid, a revision is kept, and the payment plan is locked once a payment exists (409).

Parameters: id (path, required)

Body: InvoiceUpdate

Responses: 200 Invoice 404 Error 409 Error 422 Error

DELETE /invoices/{id} Delete a draft scope invoices:write

Parameters: id (path, required)

Responses: 200 object 409 Error

POST /invoices/{id}/send Send or resend the invoice email scope invoices:write

A draft is finalized and numbered first.

Parameters: id (path, required)

Body: object with message, subject, to

Responses: 200 Invoice 409 Error 422 Error

POST /invoices/{id}/void Void an invoice scope invoices:write

Parameters: id (path, required)

Body: object with reason

Responses: 200 Invoice 409 Error

GET /invoices/{id}/pdf Get a PDF link scope invoices:read

A signed URL that works for 15 minutes without a key.

Parameters: id (path, required)

Responses: 200 object

Payments

GET /invoices/{id}/payments List an invoice's payments scope payments:read

Parameters: id (path, required), limit (query), starting_after (query)

Responses: 200 list of Payment

POST /invoices/{id}/payments Record a manual payment scope payments:write

A check, cash, bank transfer, or similar. Never creates a Stripe charge.

Parameters: id (path, required), Idempotency-Key (header)

Body: ManualPaymentInput

Responses: 201 Payment 422 Error

GET /payments/{id} Get a payment scope payments:read

Parameters: id (path, required)

Responses: 200 Payment 404 Error

POST /payments/{id}/refunds Refund a payment scope refunds:write

For a Stripe payment, issues the refund on the connected account; the platform fee is refunded in proportion. A manual payment is reversed in full instead (object "reversal").

Parameters: id (path, required), Idempotency-Key (header)

Body: object with amount_cents, reason

Responses: 201 Refund or Reversal 409 Error 422 Error

Events

GET /events List events scope events:read

The same events webhooks deliver, kept for at least 30 days, so a consumer that missed webhooks can catch up.

Parameters: limit (query), starting_after (query), type (query), created_after (query)

Responses: 200 list of Event

GET /events/{id} Get an event scope events:read

Parameters: id (path, required)

Responses: 200 Event 404 Error

Objects

Error

FieldTypeNotes
error requiredobject

Me

FieldTypeNotes
object requiredstring
environment requiredstring: live, test
business requiredobject
api_key requiredobject

Address

FieldTypeNotes
line1string | null
line2string | null
citystring | null
statestring | null
zipstring | null

ClientInput

FieldTypeNotes
external_sourcestring
external_idstring
name requiredstring
company_namestring
emailstring (email)
cc_emailsarray of string
phonestring
addressAddress
notesstring
tax_exemptboolean

Client

FieldTypeNotes
id requiredstring
object requiredstring
name requiredstring
company_namestring | null
email requiredstring | null
cc_emailsarray of string
phonestring | null
addressAddress
notesstring | null
tax_exemptboolean
external_source requiredstring | null
external_id requiredstring | null
archivedboolean
created_at requiredstring | null (date-time)
updated_atstring | null (date-time)

Discount

FieldTypeNotes
type requiredstring: percent, flat
percent_bpsintegerBasis points: 1000 = 10.00%.
amount_centsintegerInteger cents.
labelstring

ItemInput

FieldTypeNotes
titlestringShown bold above the description.
descriptionstring
quantitynumber | stringUp to 4 decimal places. Default 1.
unit_price_cents requiredintegerInteger cents.
discountDiscount
tax_rate_idinteger

InstallmentInput

FieldTypeNotes
labelstring
amount_cents requiredinteger | nullnull on at most one installment means the remainder.
due_date requiredstring (date)
notestringPlain text shown to the client.
auto_chargebooleanPro; needs a saved card.

InvoiceInput

FieldTypeNotes
external_sourcestring
external_idstring
clientClientInput
client_idstringInstead of a client object.
issue_datestring (date)
due_datestring (date)Default from the business terms; the last installment's date with a payment plan.
referencestring
notesstring
terms_textstring
items requiredarray of ItemInput
discountDiscount
payment_planobjectAmounts must add up to the total to the cent.
allow_partial_paymentsboolean
accept_online_paymentsboolean
return_urlstringWhere "Return to {Business}" sends the client after paying. Must be on an allowed domain. Never trust the redirect for payment status.
metadataobjectUp to 50 keys (40 chars) with string values (500 chars). Returned unchanged, included in webhooks, never shown to the client.
sendobject
statestring: open, draftopen finalizes and numbers the invoice; draft leaves it for review.

InvoiceUpdate

FieldTypeNotes
client_idstring
issue_datestring (date)
due_datestring (date)
referencestring
notesstring
terms_textstring
itemsarray of ItemInput
discountDiscount
payment_planobject | null
allow_partial_paymentsboolean
accept_online_paymentsboolean
return_urlstring
metadataobject

Item

FieldTypeNotes
titlestring | null
description requiredstring
quantity requiredstring
unit_price_cents requiredintegerInteger cents.
discount_centsintegerInteger cents.
tax_centsintegerInteger cents.
amount_cents requiredintegerInteger cents.
kind requiredstring: line, late_fee, security_deposit

Installment

FieldTypeNotes
id requiredstring
sequence requiredinteger
label requiredstring
notestring | null
amount_cents requiredintegerInteger cents.
due_date requiredstring (date)
status requiredstring: scheduled, due, partially_paid, paid, overdue, canceled
paid_cents requiredintegerInteger cents.
paid_atstring | null (date-time)

Invoice

FieldTypeNotes
id requiredstring
object requiredstring
version requiredintegerIncreases on every change; ignore events with a lower version than one you have seen.
number requiredstring | nullnull until finalized.
status requiredstring: draft, open, paid, void, written_offThe stored lifecycle state.
display_status requiredstring: draft, scheduled, sent, viewed, partially_paid, overdue, paid, void, written_offThe derived label.
client_id requiredstring | null
clientobject | null
currency requiredstring
issue_date requiredstring | null (date)
due_date requiredstring | null (date)
subtotal_centsintegerInteger cents.
discount_total_centsintegerInteger cents.
tax_total_centsintegerInteger cents.
total_cents requiredintegerInteger cents.
paid_cents requiredintegerInteger cents.
balance_due_cents requiredintegerInteger cents.
items requiredarray of Item
installments requiredarray of Installment
allow_partial_paymentsboolean
accept_online_paymentsboolean
notesstring | null
referencestring | null
pay_url requiredstring
external_source requiredstring | null
external_id requiredstring | null
metadata requiredobject
return_urlstring | null
sent_atstring | null (date-time)
first_viewed_atstring | null (date-time)
paid_atstring | null (date-time)
voided_atstring | null (date-time)
void_reasonstring | null
created_at requiredstring | null (date-time)
updated_at requiredstring | null (date-time)

ManualPaymentInput

FieldTypeNotes
amount_cents requiredintegerInteger cents.
paid_onstring (date)Default today in the business time zone.
method requiredstring: check, cash, bank_transfer, zelle, venmo, other
referencestring
notestring
installment_idstringWhich installment this is for, if the invoice has a payment plan.
send_receiptboolean

Payment

FieldTypeNotes
id requiredstring
object requiredstring
invoice_id requiredstring | null
installment_idstring | null
method requiredstring: stripe, manual
manual_methodstring | null
status requiredstring: succeeded, failed, reversed, pending
amount_cents requiredintegerInteger cents.
platform_fee_centsintegerInteger cents.
refunded_centsintegerInteger cents.
referencestring | null
notestring | null
paid_at requiredstring | null (date-time)
created_atstring | null (date-time)

Refund

FieldTypeNotes
id requiredstring
object requiredstring
payment_id requiredstring | null
amount_cents requiredintegerInteger cents.
status requiredstring
reasonstring | null
created_atstring | null (date-time)

Reversal

FieldTypeNotes
object requiredstring: reversal
payment requiredPayment

Event

FieldTypeNotes
id requiredstring
objectstring
type requiredstring: invoice.created, invoice.sent, invoice.viewed, invoice.updated, invoice.partially_paid, invoice.paid, invoice.overdue, invoice.voided, payment.succeeded, payment.failed, payment.refunded, installment.paid, installment.overdue
created requiredstring (date-time)
business_id requiredstring | null
data requiredobject