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?#

TipCharge
Who decidesThe buyer (you suggest)You
Typical useGratuity for service or deliveryDelivery, service, setup, and booking fees; surcharges
AmountFixed, or percent from 1 to 100Fixed, or percent with an explicit basis
Percent computed onPost-discount subtotalPre- or post-discount subtotal (you choose)
TaxedNeverOptional, per charge
Per orderOne requested tipAs 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:

text
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:

  1. 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.
  2. 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:

JSON
{
  "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:

Bash
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:

JSON
{
  "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_money or percent, never both. Amounts are integers in the currency's minor unit.
  • percent is a whole number from 1 to 100 (18 means 18%; 12.5 means 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, and metadata are optional and travel with the tip for your own reporting.

Clear the requested tip#

Bash
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#

Bash
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:

JSON
{
  "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 (or percent) is the charge's definition, echoing what you passed.
  • applied_money is the computed pre-tax amount. For a fixed charge it equals amount_money; for a percent charge it is the resolved value.
  • tax_money is this charge's own tax, and total_money is applied_money plus tax_money.
  • refunded_money tracks 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:

Bash
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_discount computes on the line-item subtotal before discounts.
  • subtotal_post_discount computes 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:

JSON
{
  "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.taxable explicitly, or the request is rejected with ORDER_CHARGE_TAX_INPUT_REQUIRED.
  • tax_category refines the rate applied and takes charge-specific values: service_fee, shipping, delivery, handling, or surcharge. (Line items use product categories like prepared_food; charges have their own list.)
  • Computed tax lands on the charge as tax_money, rolls into pricing_amounts.tax_money, and makes the charge's total_money larger than its applied_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):

Bash
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:

Bash
curl -X DELETE https://api.withflintpay.com/v1/orders/ord_1kmn0aExample/charges/och_1kmn0aExample \
  -H "Authorization: Bearer YOUR_API_KEY"
Bash
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:

text
  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 fieldValueHow
subtotal_money$40.00line items
discount_money$4.0010% of $40.00
charge_money$6.08$5.00 delivery + 3% of $36.00
tax_money$0.00nothing taxable here
requested_tip_money$6.4818% of $36.00
total_money$48.56sum

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_discount fee but not a subtotal_pre_discount one.

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_PAID afterward).
  • 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's refunded_money records 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 to partially_refunded or refunded.

Errors You Will Encounter#

All errors use the standard error envelope, with error.param pointing at the offending field.

StatusCodeMeaning
400INVALID_TIPTip has both amount_money and percent, or neither
400INVALID_TIP_PERCENTTip percent below 1 or above 100 (remember: 0.15 is 0.15%)
400CHARGE_VALUE_REQUIREDCharge has neither amount_money nor percent
400INVALID_CHARGE_VALUECharge has both amount_money and percent
400INVALID_CHARGE_PERCENTCharge percent is zero, negative, or above 100
400CALCULATION_BASIS_REQUIREDPercent charge without calculation_basis
400CALCULATION_BASIS_FORBIDDENFixed-amount charge with calculation_basis
400INVALID_CHARGE_TYPEtype is not one of the documented values
400ORDER_CHARGE_TAX_INPUT_REQUIREDOrder has tax enabled but the charge omitted tax.taxable
400INVALID_TAX_CATEGORYtax_category is not one of the five charge categories
400CURRENCY_MISMATCH / TIP_CURRENCY_MISMATCHAmount currency differs from the order's currency
400ORDER_CURRENCY_REQUIREDCharge added to an order with no line items yet
409CHECKOUT_SESSION_ACTIVEA hosted checkout is open for this order; close it first
400ORDER_ALREADY_PAIDCharge edits after a successful payment
400ORDER_CLOSEDThe order is closed
400ORDER_FINANCIAL_MUTATION_NOT_ALLOWEDThe order has refunds; financial fields are frozen
404NOT_FOUNDUnknown order or charge ID (charge IDs belong to one order)
Rate this doc