Tips & Fees
An order's total is often more than the sum of its line items. A delivery run costs something. A booking carries a service fee. A happy customer rounds up for the crew. Flint models this extra money as two first-class objects on the order:
- A tip is money the buyer chooses to add. You request it (or the hosted checkout page offers it), and it settles when the order is paid.
- A charge is money you add: a delivery fee, setup fee, service fee, or surcharge. You control it; the buyer sees it in the total.
Both participate correctly in the order's discount, tax, refund, and reconciliation math, which is exactly what breaks when fees are faked with line items.
Don't model a fee or a tip as a line item. Line items are products: coupons discount them, catalog reporting counts them, and inventory logic watches them. A $5.00 "Delivery" line item silently becomes $4.50 the day a 10% coupon lands. Charges are immune to discounts, carry their own tax treatment, and track their own refunds.
Which One Do You Need?#
| Tip | Charge | |
|---|---|---|
| Who decides | The buyer (you suggest) | You |
| Typical use | Gratuity for service or delivery | Delivery, service, setup, and booking fees; surcharges |
| Amount | Fixed, or percent from 1 to 100 | Fixed, or percent with an explicit basis |
| Percent computed on | Post-discount subtotal | Pre- or post-discount subtotal (you choose) |
| Taxed | Never | Optional, per charge |
| Per order | One requested tip | As many as you need |
Every endpoint in this guide mutates one order and returns the full updated order, so you never need a follow-up fetch to see the new totals.
Tips#
The life of a tip#
A tip moves through explicit states, so you can always tell intent from money:
requested on the order and included in the total to collect
└─ settled the payment succeeded; the tip is real money
└─ partially_refunded / refunded
canceled the requested tip was cleared before payment
A tip gets requested one of two ways:
- The buyer picks one at hosted checkout. Checkout sessions and payment links render tipping when you enable it; the buyer's choice is written to the order automatically.
- Your code sets one. Your own ordering UI, a POS flow, or an agent collects the buyer's choice and sets it on the order directly.
Either way it is the same object: an entry in the order's tips[] array, counted in pricing_amounts.requested_tip_money.
Offer tips at hosted checkout#
Pass a tip section when creating a checkout session or payment link:
{
"tip": {
"enabled": true,
"tip_percentages": [15, 18, 20],
"default_tip_percentage": 18,
"is_custom_tip_enabled": true
}
}
Percent presets fit service orders. For small totals where percentages feel stingy, offer fixed-amount presets instead: set is_smart_tips_enabled: true with smart_tip_money_options (for example $1, $2, $5) and default_smart_tip_money. Omit the tip section entirely to inherit the merchant's dashboard tipping settings.
The buyer's selection becomes the order's requested tip and settles with their payment. There is nothing else to build.
Set a requested tip from your code#
POST /v1/orders/{order_id}/requested-tip takes a fixed amount or a percent:
curl -X POST https://api.withflintpay.com/v1/orders/ord_1kmn0aExample/requested-tip \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{"requested_tip": {"percent": 18}}'
The response is the full updated order. On an order with a $40.00 subtotal and a $4.00 discount, the tip lands like this:
{
"data": {
"order_id": "ord_1kmn0aExample",
"tips": [{
"order_tip_id": "tip_1kmn0aExample",
"status": "requested",
"percent": 18,
"effective_amount_money": {"amount": 648, "currency": "USD"},
"settled_amount_money": {"amount": 0, "currency": "USD"},
"refunded_money": {"amount": 0, "currency": "USD"}
}],
"pricing_amounts": {
"subtotal_money": {"amount": 4000, "currency": "USD"},
"discount_money": {"amount": 400, "currency": "USD"},
"charge_money": {"amount": 0, "currency": "USD"},
"tax_money": {"amount": 0, "currency": "USD"},
"requested_tip_money": {"amount": 648, "currency": "USD"},
"total_money": {"amount": 4248, "currency": "USD"}
}
}
}
The rules:
- Pass
amount_moneyorpercent, never both. Amounts are integers in the currency's minor unit. percentis a whole number from 1 to 100 (18means 18%;12.5means 12.5%).- A percent tip computes on the post-discount subtotal, before charges and tax. The resolved amount always comes back as
effective_amount_money; read it instead of recomputing. - An order has one requested tip. Setting it again replaces the value in place (same
order_tip_id), so re-sending the buyer's latest choice is always safe. name,description, andmetadataare optional and travel with the tip for your own reporting.
Clear the requested tip#
curl -X POST https://api.withflintpay.com/v1/orders/ord_1kmn0aExample/requested-tip/clear \
-H "Authorization: Bearer YOUR_API_KEY"
The tip's status flips to canceled and pricing_amounts.requested_tip_money returns to zero. The canceled entry stays in tips[] as history rather than disappearing, so audit trails survive a buyer changing their mind.
When the tip settles#
A requested tip is intent, not money. It rides on the order's payment: hosted checkout and the pay endpoint collect the total including the tip, and when payment succeeds the tip's status becomes settled, with settled_amount_money and the collecting payment_intent_id filled in. The order's settlement_amounts.settled_tip_money is the tip money actually collected, which is the number to reconcile against.
If you split an order across multiple payment intents, the tip rides on exactly one of them; choose which with tip_payment_intent_id on the pay call. Integrations that manage payment intents directly can also set tip_money on the payment intent itself instead of using a requested tip.
Fees & Service Charges#
A charge is a named amount you add on top of line items: POST /v1/orders/{order_id}/charges.
Add a charge#
curl -X POST https://api.withflintpay.com/v1/orders/ord_1kmn0aExample/charges \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: delivery-fee-ord-1kmn0a-001" \
-d '{
"charge": {
"name": "Delivery",
"type": "delivery_fee",
"amount_money": {"amount": 500, "currency": "USD"}
}
}'
The order comes back with the charge in charges[] and pricing_amounts recalculated:
{
"order_charge_id": "och_1kmn0aExample",
"name": "Delivery",
"type": "delivery_fee",
"amount_money": {"amount": 500, "currency": "USD"},
"applied_money": {"amount": 500, "currency": "USD"},
"tax_money": {"amount": 0, "currency": "USD"},
"total_money": {"amount": 500, "currency": "USD"},
"refunded_money": {"amount": 0, "currency": "USD"}
}
What each money field means:
amount_money(orpercent) is the charge's definition, echoing what you passed.applied_moneyis the computed pre-tax amount. For a fixed charge it equalsamount_money; for a percent charge it is the resolved value.tax_moneyis this charge's own tax, andtotal_moneyisapplied_moneyplustax_money.refunded_moneytracks how much of this specific charge has been refunded.
name is what the buyer sees at checkout and on receipts, so write it for them ("Delivery", not "DLVRY_FEE_2"). type classifies the charge for reporting and must be one of: service_fee, delivery_fee, shipping_fee, handling_fee, packaging_fee, small_order_fee, service_area_fee, setup_fee, installation_fee, cleaning_fee, booking_fee, reservation_fee, ticket_fee, fulfillment_fee, restocking_fee, rush_fee, or other.
Two smaller fields worth knowing: metadata carries your own tags on the charge, and fulfillment_id ties the charge to one of the order's fulfillments, so a shipping fee can travel with its shipment.
The order needs at least one line item before you add a charge (the order's currency comes from its items), and the Idempotency-Key header matters more here than on most calls: adding a charge is not naturally idempotent, and a retried request without a key would stack a second fee. See Idempotency.
Percent charges declare their basis#
A percent charge must say what it is a percent of:
curl -X POST https://api.withflintpay.com/v1/orders/ord_1kmn0aExample/charges \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: service-fee-ord-1kmn0a-001" \
-d '{
"charge": {
"name": "Service fee",
"type": "service_fee",
"percent": 10,
"calculation_basis": "subtotal_post_discount"
}
}'
subtotal_pre_discountcomputes on the line-item subtotal before discounts.subtotal_post_discountcomputes on the subtotal after discounts.
On a $40.00 order with a $4.00 discount, a 10% charge is $4.00 pre-discount or $3.60 post-discount. calculation_basis is required on every percent charge and rejected on fixed-amount ones, so the math is always explicit rather than defaulted.
percent is a whole number: 10 means 10%. Tips reject fractions below 1, but charges accept them, because sub-1% fees are legitimate (a 0.5% surcharge is {"percent": 0.5}). That means a charge of {"percent": 0.15} intended as 15% is silently created as 0.15%. If a fee computes to almost nothing, you sent a fraction where the API expects a whole number.
Taxing charges#
Whether a charge is taxed is decided per charge, with its own tax section:
{
"charge": {
"name": "Delivery",
"type": "delivery_fee",
"amount_money": {"amount": 1000, "currency": "USD"},
"tax": {"taxable": true, "tax_category": "delivery"}
}
}
- A charge never inherits taxability from the order or its line items. If tax is enabled on the order, every charge you add must state
tax.taxableexplicitly, or the request is rejected withORDER_CHARGE_TAX_INPUT_REQUIRED. tax_categoryrefines the rate applied and takes charge-specific values:service_fee,shipping,delivery,handling, orsurcharge. (Line items use product categories likeprepared_food; charges have their own list.)- Computed tax lands on the charge as
tax_money, rolls intopricing_amounts.tax_money, and makes the charge'stotal_moneylarger than itsapplied_money.
Update or remove a charge#
Updates are a PATCH with the changed fields at the top level (no charge wrapper, unlike the create call):
curl -X PATCH https://api.withflintpay.com/v1/orders/ord_1kmn0aExample/charges/och_1kmn0aExample \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{"name": "Priority delivery", "amount_money": {"amount": 750, "currency": "USD"}}'
Omitted fields are unchanged, and totals recalculate in the response. metadata merges per key; send "metadata": null to clear it all, and "fulfillment_id": null to unlink the charge from its fulfillment.
Removing works one at a time or in bulk:
curl -X DELETE https://api.withflintpay.com/v1/orders/ord_1kmn0aExample/charges/och_1kmn0aExample \
-H "Authorization: Bearer YOUR_API_KEY"
curl -X POST https://api.withflintpay.com/v1/orders/ord_1kmn0aExample/charges/remove \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{"order_charge_ids": ["och_1kmn0aExample", "och_1kmn0bExample"]}'
The bulk call is atomic: the IDs must be distinct and every one must exist on the order, or nothing is removed. Removed charges disappear from charges[] entirely (unlike cleared tips, which remain as canceled history).
How the Total Composes#
The order computes its total in a fixed sequence:
subtotal_money Σ line items
- discount_money coupons and discounts, applied to line items only
+ charge_money fixed charges, plus percent charges on their chosen basis
+ tax_money taxable line items + taxable charges
+ requested_tip_money fixed, or percent of the post-discount subtotal
= total_money
A worked example: $40.00 of line items, a 10% coupon, a $5.00 delivery fee, a 3% service fee on subtotal_post_discount, and an 18% tip:
pricing_amounts field | Value | How |
|---|---|---|
subtotal_money | $40.00 | line items |
discount_money | $4.00 | 10% of $40.00 |
charge_money | $6.08 | $5.00 delivery + 3% of $36.00 |
tax_money | $0.00 | nothing taxable here |
requested_tip_money | $6.48 | 18% of $36.00 |
total_money | $48.56 | sum |
Three rules fall out of the sequence:
- Discounts never shrink charges or tips. A coupon applies to line items; the delivery fee stays $5.00.
- Tips are computed on the post-discount subtotal only. Buyers tip on the goods and services, not on your fees or the tax.
- Percent charges pick their basis explicitly, so a coupon changes a
subtotal_post_discountfee but not asubtotal_pre_discountone.
Read every value from pricing_amounts instead of re-deriving it client-side; percent math rounds to the nearest minor unit and discounts prorate, so recomputation invites penny drift. See Money & Currency.
When You Can Change Tips and Fees#
Tips and charges shape the amount a buyer is about to pay, so mutations are fenced by the order's lifecycle:
- While a checkout session is open, merchant-side changes to charges and the requested tip return
409 CHECKOUT_SESSION_ACTIVE; the buyer may be looking at the total right now. The buyer's own tip choice on the hosted page still works. Close the session, edit, then create a new session. See checkout session lifecycle rules. - Charges freeze at payment. Add charges while the order is open; update or remove them only before a successful payment (
ORDER_ALREADY_PAIDafterward). - A settled tip is not editable. Once paid, tip money is adjusted through refunds, not edits.
- After any refund, all financial mutation on the order is rejected with
ORDER_FINANCIAL_MUTATION_NOT_ALLOWED. - Closed orders reject everything with
ORDER_CLOSED.
No webhook fires specifically for tip or charge edits; they are pre-payment total shaping, and the payment events carry the final amounts. The order's activities[] records each change (charge_added, charge_updated, charge_removed, requested_tip_added, requested_tip_updated, requested_tip_removed) for auditing, and a fresh GET /v1/orders/{order_id} always shows current state.
Refunding Tips and Charges#
Both objects track their own refund history, so partial refunds stay precise:
- Charges can be targeted directly when creating a refund: pass
charges: [{"order_charge_id": "och_...", "amount_money": {...}}]to refund a specific fee, and that charge'srefunded_moneyrecords it. - Tips refund as part of the payment they settled on. The refund reports the tip portion as
refunded_tip_money, and the tip's status moves topartially_refundedorrefunded.
Errors You Will Encounter#
All errors use the standard error envelope, with error.param pointing at the offending field.
| Status | Code | Meaning |
|---|---|---|
400 | INVALID_TIP | Tip has both amount_money and percent, or neither |
400 | INVALID_TIP_PERCENT | Tip percent below 1 or above 100 (remember: 0.15 is 0.15%) |
400 | CHARGE_VALUE_REQUIRED | Charge has neither amount_money nor percent |
400 | INVALID_CHARGE_VALUE | Charge has both amount_money and percent |
400 | INVALID_CHARGE_PERCENT | Charge percent is zero, negative, or above 100 |
400 | CALCULATION_BASIS_REQUIRED | Percent charge without calculation_basis |
400 | CALCULATION_BASIS_FORBIDDEN | Fixed-amount charge with calculation_basis |
400 | INVALID_CHARGE_TYPE | type is not one of the documented values |
400 | ORDER_CHARGE_TAX_INPUT_REQUIRED | Order has tax enabled but the charge omitted tax.taxable |
400 | INVALID_TAX_CATEGORY | tax_category is not one of the five charge categories |
400 | CURRENCY_MISMATCH / TIP_CURRENCY_MISMATCH | Amount currency differs from the order's currency |
400 | ORDER_CURRENCY_REQUIRED | Charge added to an order with no line items yet |
409 | CHECKOUT_SESSION_ACTIVE | A hosted checkout is open for this order; close it first |
400 | ORDER_ALREADY_PAID | Charge edits after a successful payment |
400 | ORDER_CLOSED | The order is closed |
400 | ORDER_FINANCIAL_MUTATION_NOT_ALLOWED | The order has refunds; financial fields are frozen |
404 | NOT_FOUND | Unknown order or charge ID (charge IDs belong to one order) |
Related Docs#
- Orders API Reference: every field on charges, tips, and order totals.
- Checkout Sessions: tip presets on the hosted page and the session lock.
- Payment Links: the same tip configuration on reusable links.
- Money & Currency: minor units, percent conventions, and rounding.
- Refunds: full and partial refunds, including per-charge targeting.
- Idempotency: safe retries for charge mutations.
