Skip to content
FractionalPay

Split payment API reference

The split payment API reference for FractionalPay v1. Every endpoint below takes and returns JSON, uses amounts as integers in cents plus a currency, and works the same with test and live keys. New here? Start with the quickstart.

Base URL https://fractionalpay.com/api/v1

Authentication

Send your secret key in the Authorization header as a bearer token. Keys are created in Developers in the dashboard, shown once and stored only as a SHA-256 hash with a visible prefix. Keep them on your servers, never in a browser or mobile app.

curl https://fractionalpay.com/api/v1/beneficiaries \
  -H "Authorization: Bearer fp_test_9c2f..."

Test and live keys

PrefixDataAvailable
fp_test_Test data and the sandbox. Never touches your operator's live account.Every workspace, the sandbox included
fp_live_Live payments, splits and payouts on your connected account.Starter, Growth and Scale

Payments carry livemode: a test key records and reads test payments, a live key live payments. Beneficiaries, rules, balances, the ledger and payouts belong to the workspace. A live key on a workspace without a paid plan gets 402 plan_limit. Scale workspaces can restrict each key to an IP allowlist; a request from another address gets 403 with type authentication.

HTTP/1.1 403 Forbidden

{
  "error": {
    "type": "authentication",
    "message": "Requests from this IP address are not on the allowlist of this key."
  }
}

Idempotency

POST requests accept an Idempotency-Key header. The same key with the same body returns the first result for 24 hours, so a retried request never records a payment, refund or approval twice. A replayed answer has the same status and body as the first one and carries the header Idempotent-Replayed: true. The same key with a different body, or while the first request is still running, returns 409 conflict. Keys are scoped to the API key, up to 255 characters, and server errors are not stored, so they can be retried. Use your order or refund ID, or a UUID.

HTTP/1.1 201 Created
Idempotent-Replayed: true
Content-Type: application/json

{"id":20,"object":"beneficiary","name":"Northwind Studio",...}

Pagination

List endpoints return objects newest first in a list object. Pass limit (1 to 100, default 20) and, for the next page, starting_after with the integer ID of the last object you received. Stop when has_more is false. IDs are integers on every object.

curl "https://fractionalpay.com/api/v1/beneficiaries?limit=2&starting_after=21" \
  -H "Authorization: Bearer $FP_KEY"

{
  "object": "list",
  "data": [
    "..."
  ],
  "has_more": false
}

Limits

WorkspaceAPI requestsAbove the limit
Sandbox100 per day402 plan_limit
Starter50,000 per month402 plan_limit
Growth500,000 per month402 plan_limit
Scale5,000,000 per month402 plan_limit

Requests are counted as usage in the billing period. Each key is also limited to 100 requests per second, above which you get 429 rate_limited.

Errors

Errors use HTTP status codes and a JSON body with a type and a readable message. Validation errors add param with the name of the field.

HTTP/1.1 422 Unprocessable Content

{
  "error": {
    "type": "invalid_request",
    "message": "Line 0 is above 100 percent.",
    "param": "lines.0.value"
  }
}
StatusTypeMeaning
400invalid_requestAn Idempotency-Key longer than 255 characters (param Idempotency-Key).
422invalid_requestValidation failed, for example a missing field or a percent line above 100. param names the field, for example lines.0.value.
401authenticationMissing, malformed, revoked or unknown API key.
403authenticationThe key has an IP allowlist (Scale) and the request comes from an address outside it.
402plan_limitThe request quota, the beneficiary or rule limit of your plan is used, a live key is used without a paid plan, or the feature belongs to a higher plan.
404not_foundThe object does not exist in this workspace (and, for payments, in the mode of the key), or the endpoint does not exist.
409conflictAn external_id that already exists, an Idempotency-Key reused with a different body or still in progress, or a payout that cannot be approved in its state or by this key.
429rate_limitedMore than 100 requests per second with one key. The response carries Retry-After: 1.

Splits

Simulate a split

POST /v1/splits/simulate

Calculates a split without saving anything, with the same engine as the website simulator. Use it to check percentages, fixed lines, holds and refunds before you create a rule. Amounts and fixed values go in and come back in minor units. The ledger and payout lines of a simulation carry local labels (txn_1, txn_2), because nothing is stored.

Parameters of Simulate a split
ParameterTypeDescription
amountintegerRequired. Payment amount in minor units (cents), 1 or more.
currencystringRequired. Three-letter code. Returned in upper case.
partiesarrayRequired. 2 to 6 objects with role (string), kind (percent or fixed), value (a percent, or minor units for fixed), and optionally platform and remainder (booleans). The platform party receives the remainder unless another party has remainder set.
schedulestringinstant, weekly or monthly. Default weekly.
holdbooleanHolds part of every non-platform share. Default false.
hold_percentnumber0 to 50, default 10. Used when hold is true.
hold_daysinteger1 to 90, default 14. Used when hold is true.
refundinteger or nullOptional partial refund in minor units, reversed proportionally (refund_reversal per party).

Request

curl -X POST "https://fractionalpay.com/api/v1/splits/simulate" \
  -H "Authorization: Bearer $FP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount":100000,"currency":"USD","parties":[{"role":"Provider","kind":"percent","value":70},{"role":"Platform","kind":"percent","value":15,"platform":true},{"role":"Partner","kind":"percent","value":10},{"role":"Subcontractor","kind":"fixed","value":5000}],"schedule":"weekly","hold":false}'

Response

{
  "object": "split_simulation",
  "amount": 100000,
  "currency": "USD",
  "refund": 0,
  "schedule": "weekly",
  "hold": false,
  "balanced": true,
  "allocations": [
    {
      "role": "Provider",
      "kind": "percent",
      "value": 70,
      "platform": false,
      "remainder": false,
      "amount": 70000,
      "share_percent": 70,
      "held": 0,
      "refund_reversal": 0,
      "net": 70000,
      "available_now": 70000
    },
    {
      "role": "Platform",
      "kind": "percent",
      "value": 15,
      "platform": true,
      "remainder": true,
      "amount": 15000,
      "share_percent": 15,
      "held": 0,
      "refund_reversal": 0,
      "net": 15000,
      "available_now": 15000
    },
    {
      "role": "Partner",
      "kind": "percent",
      "value": 10,
      "platform": false,
      "remainder": false,
      "amount": 10000,
      "share_percent": 10,
      "held": 0,
      "refund_reversal": 0,
      "net": 10000,
      "available_now": 10000
    },
    {
      "role": "Subcontractor",
      "kind": "fixed",
      "value": 5000,
      "platform": false,
      "remainder": false,
      "amount": 5000,
      "share_percent": 5,
      "held": 0,
      "refund_reversal": 0,
      "net": 5000,
      "available_now": 5000
    }
  ],
  "ledger": [
    {
      "id": "txn_1",
      "date": "2026-10-15",
      "description": "Payment captured and split by rule",
      "lines": [
        {
          "account": "Operator balance (your account)",
          "debit": 100000,
          "credit": 0
        },
        {
          "account": "Payable to Provider",
          "debit": 0,
          "credit": 70000
        },
        {
          "account": "Platform revenue",
          "debit": 0,
          "credit": 15000
        },
        {
          "account": "Payable to Partner",
          "debit": 0,
          "credit": 10000
        },
        {
          "account": "Payable to Subcontractor",
          "debit": 0,
          "credit": 5000
        }
      ],
      "balanced": true
    },
    {
      "id": "txn_2",
      "date": "2026-10-23",
      "description": "Payout run 1",
      "lines": [
        {
          "account": "Payable to Provider",
          "debit": 70000,
          "credit": 0
        },
        {
          "account": "Payable to Partner",
          "debit": 10000,
          "credit": 0
        },
        {
          "account": "Payable to Subcontractor",
          "debit": 5000,
          "credit": 0
        },
        {
          "account": "Operator balance (your account)",
          "debit": 0,
          "credit": 85000
        }
      ],
      "balanced": true
    }
  ],
  "payouts": [
    {
      "date": "2026-10-23",
      "party": "Provider",
      "amount": 70000,
      "note": "Scheduled payout"
    },
    {
      "date": "2026-10-23",
      "party": "Partner",
      "amount": 10000,
      "note": "Scheduled payout"
    },
    {
      "date": "2026-10-23",
      "party": "Subcontractor",
      "amount": 5000,
      "note": "Scheduled payout"
    }
  ]
}

Beneficiaries

List beneficiaries

GET /v1/beneficiaries

Returns beneficiaries, newest first, as a list object.

Parameters of List beneficiaries
ParameterTypeDescription
starting_afterintegerID of the last object from the previous page.
limitinteger1 to 100, default 20.

Request

curl "https://fractionalpay.com/api/v1/beneficiaries?limit=2" \
  -H "Authorization: Bearer $FP_KEY"

Response

{
  "object": "list",
  "data": [
    {
      "id": 22,
      "object": "beneficiary",
      "name": "Lumen Edit",
      "email": null,
      "type": "subcontractor",
      "external_ref": "sub-12",
      "country": "US",
      "currency": "USD",
      "provider_account_id": "acct_1Qx9TestLumen",
      "tax_status": "missing",
      "status": "active",
      "created": 1792062760
    },
    {
      "id": 21,
      "object": "beneficiary",
      "name": "Harbor Partners",
      "email": null,
      "type": "partner",
      "external_ref": "partner-77",
      "country": "US",
      "currency": "USD",
      "provider_account_id": "acct_1Qx9TestHarbor",
      "tax_status": "missing",
      "status": "active",
      "created": 1792062760
    }
  ],
  "has_more": true
}

Create a beneficiary

POST /v1/beneficiaries

Adds a party that receives shares and returns it with 201. In live mode, provider_account_id is the connected account ID the beneficiary has at your operator, where the operator verifies it. The create response returns status and tax_status as stored at that moment; a read returns status active and the current tax_status. Above the beneficiary limit of your plan the answer is 402 plan_limit.

Parameters of Create a beneficiary
ParameterTypeDescription
namestringRequired. Display name, up to 190 characters.
emailstringOptional. Payout notices and statements.
typestringseller, partner, affiliate, subcontractor, location or creator. Default seller.
external_refstringOptional. Your own ID for this party.
countrystringOptional. Two-letter country code, stored in upper case.
currencystringOptional. Three-letter payout currency. Default is the currency of your workspace.
provider_account_idstringOptional. Connected account ID at your operator, for example acct_...

Request

curl -X POST "https://fractionalpay.com/api/v1/beneficiaries" \
  -H "Authorization: Bearer $FP_KEY" \
  -H "Idempotency-Key: seller-2041" \
  -H "Content-Type: application/json" \
  -d '{"name":"Northwind Studio","email":"[email protected]","type":"seller","external_ref":"seller-2041","country":"US","currency":"USD","provider_account_id":"acct_1Qx9TestNorthwind"}'

Response

{
  "id": 20,
  "object": "beneficiary",
  "name": "Northwind Studio",
  "email": "[email protected]",
  "type": "seller",
  "external_ref": "seller-2041",
  "country": "US",
  "currency": "USD",
  "provider_account_id": "acct_1Qx9TestNorthwind",
  "tax_status": "missing",
  "status": "active",
  "created": 1792062760
}

Retrieve a beneficiary

GET /v1/beneficiaries/{id}

Returns one beneficiary with a balances object in the main currency and per currency in by_currency: owed (what your platform owes the beneficiary in the ledger), pending (payouts scheduled, awaiting approval or sent and not yet paid), available (owed minus pending, due at the next payout), held (reserves until their release date) and paid.

Parameters of Retrieve a beneficiary
ParameterTypeDescription
idintegerPath parameter. The beneficiary ID.

Request

curl "https://fractionalpay.com/api/v1/beneficiaries/20" \
  -H "Authorization: Bearer $FP_KEY"

Response

{
  "id": 20,
  "object": "beneficiary",
  "name": "Northwind Studio",
  "email": "[email protected]",
  "type": "seller",
  "external_ref": "seller-2041",
  "country": "US",
  "currency": "USD",
  "provider_account_id": "acct_1Qx9TestNorthwind",
  "tax_status": "missing",
  "status": "active",
  "created": 1792062760,
  "balances": {
    "currency": "USD",
    "pending": 1401500,
    "held": 149500,
    "available": 0,
    "paid": 0,
    "owed": 1401500,
    "by_currency": {
      "USD": {
        "pending": 1401500,
        "held": 149500,
        "available": 0,
        "paid": 0,
        "owed": 1401500
      }
    }
  }
}

Rules

List rules

GET /v1/rules

Returns split rules with their lines, newest first. Same pagination as other lists.

Parameters of List rules
ParameterTypeDescription
starting_afterintegerCursor.
limitinteger1 to 100, default 20.

Request

curl "https://fractionalpay.com/api/v1/rules?limit=1" \
  -H "Authorization: Bearer $FP_KEY"

Response

{
  "object": "list",
  "data": [
    {
      "id": 5,
      "object": "split_rule",
      "name": "Marketplace default",
      "match_type": "all",
      "match_key": null,
      "match_value": null,
      "priority": 10,
      "remainder_to_beneficiary_id": 20,
      "active": true,
      "version": 1,
      "lines": [
        {
          "id": 14,
          "beneficiary_id": 20,
          "role_key": null,
          "label": "Provider",
          "kind": "percent",
          "value": 70,
          "hold_percent": 10,
          "hold_days": 14,
          "order": 0
        },
        {
          "id": 15,
          "beneficiary_id": 21,
          "role_key": null,
          "label": "Partner",
          "kind": "percent",
          "value": 10,
          "hold_percent": 0,
          "hold_days": 0,
          "order": 1
        },
        {
          "id": 16,
          "beneficiary_id": 22,
          "role_key": null,
          "label": "Subcontractor",
          "kind": "fixed",
          "value": 5000,
          "hold_percent": 0,
          "hold_days": 0,
          "order": 2
        },
        {
          "id": 17,
          "beneficiary_id": null,
          "role_key": "platform",
          "label": "Platform",
          "kind": "percent",
          "value": 15,
          "hold_percent": 0,
          "hold_days": 0,
          "order": 3
        }
      ],
      "created": 1792062769
    }
  ],
  "has_more": false
}

Create a rule

POST /v1/rules

Creates a split rule (object split_rule, version 1) and returns it with 201. Each line points to a beneficiary_id or to a role_key, for example platform for the share you keep. A percent line above 100 is rejected with 422. Lines that together exceed the payment are not rejected here: such a payment is recorded with split.status queued and queued_reason shares_exceed_amount. The cents left after rounding go to remainder_to_beneficiary_id.

Parameters of Create a rule
ParameterTypeDescription
namestringRequired.
match_typestringall, product or metadata. Default all.
match_keystringMetadata key. Required when match_type is metadata.
match_valuestringProduct reference or metadata value. Required when match_type is product or metadata.
priorityinteger0 to 100000, default 100. Higher priority rules are checked first.
linesarrayRequired. 1 to 50 objects with beneficiary_id (integer) or role_key, an optional label, kind (percent or fixed), value (a percent, or minor units for fixed), hold_percent (0 to 100) and hold_days (0 to 365).
remainder_to_beneficiary_idintegerOptional. Receives the cents left after rounding.

Request

curl -X POST "https://fractionalpay.com/api/v1/rules" \
  -H "Authorization: Bearer $FP_KEY" \
  -H "Idempotency-Key: rule-marketplace-default" \
  -H "Content-Type: application/json" \
  -d '{"name":"Marketplace default","match_type":"all","priority":10,"lines":[{"beneficiary_id":20,"label":"Provider","kind":"percent","value":70,"hold_percent":10,"hold_days":14},{"beneficiary_id":21,"label":"Partner","kind":"percent","value":10},{"beneficiary_id":22,"label":"Subcontractor","kind":"fixed","value":5000},{"role_key":"platform","label":"Platform","kind":"percent","value":15}],"remainder_to_beneficiary_id":20}'

Response

{
  "id": 5,
  "object": "split_rule",
  "name": "Marketplace default",
  "match_type": "all",
  "match_key": null,
  "match_value": null,
  "priority": 10,
  "remainder_to_beneficiary_id": 20,
  "active": true,
  "version": 1,
  "lines": [
    {
      "id": 14,
      "beneficiary_id": 20,
      "role_key": null,
      "label": "Provider",
      "kind": "percent",
      "value": 70,
      "hold_percent": 10,
      "hold_days": 14,
      "order": 0
    },
    {
      "id": 15,
      "beneficiary_id": 21,
      "role_key": null,
      "label": "Partner",
      "kind": "percent",
      "value": 10,
      "hold_percent": 0,
      "hold_days": 0,
      "order": 1
    },
    {
      "id": 16,
      "beneficiary_id": 22,
      "role_key": null,
      "label": "Subcontractor",
      "kind": "fixed",
      "value": 5000,
      "hold_percent": 0,
      "hold_days": 0,
      "order": 2
    },
    {
      "id": 17,
      "beneficiary_id": null,
      "role_key": "platform",
      "label": "Platform",
      "kind": "percent",
      "value": 15,
      "hold_percent": 0,
      "hold_days": 0,
      "order": 3
    }
  ],
  "created": 1792062769
}

Retrieve a rule

GET /v1/rules/{id}

Returns one rule with its lines and current version. Each split stores the rule_id and rule_version it used.

Parameters of Retrieve a rule
ParameterTypeDescription
idintegerPath parameter. The rule ID.

Request

curl "https://fractionalpay.com/api/v1/rules/5" \
  -H "Authorization: Bearer $FP_KEY"

Response

{
  "id": 5,
  "object": "split_rule",
  "name": "Marketplace default",
  "match_type": "all",
  "match_key": null,
  "match_value": null,
  "priority": 10,
  "remainder_to_beneficiary_id": 20,
  "active": true,
  "version": 1,
  "lines": [
    {
      "id": 14,
      "beneficiary_id": 20,
      "role_key": null,
      "label": "Provider",
      "kind": "percent",
      "value": 70,
      "hold_percent": 10,
      "hold_days": 14,
      "order": 0
    },
    {
      "id": 15,
      "beneficiary_id": 21,
      "role_key": null,
      "label": "Partner",
      "kind": "percent",
      "value": 10,
      "hold_percent": 0,
      "hold_days": 0,
      "order": 1
    },
    {
      "id": 16,
      "beneficiary_id": 22,
      "role_key": null,
      "label": "Subcontractor",
      "kind": "fixed",
      "value": 5000,
      "hold_percent": 0,
      "hold_days": 0,
      "order": 2
    },
    {
      "id": 17,
      "beneficiary_id": null,
      "role_key": "platform",
      "label": "Platform",
      "kind": "percent",
      "value": 15,
      "hold_percent": 0,
      "hold_days": 0,
      "order": 3
    }
  ],
  "created": 1792062769
}

Payments and refunds

Record a payment

POST /v1/payments

Records a payment and splits it now, then returns it with 201 and the split with its allocations. With a test key it records test data (livemode false). Live payments arrive from your connected operator account automatically, so with a live key this endpoint is only for payments taken outside the operator. A payment that cannot be split is still recorded: split.status is queued and split.queued_reason is no_matching_rule, shares_exceed_amount or flow_limit (second example). An external_id that already exists in the workspace is 409 conflict.

Parameters of Record a payment
ParameterTypeDescription
external_idstringRequired. Your order or invoice ID, unique in the workspace.
amountintegerRequired. Minor units, 1 or more.
currencystringRequired. USD, EUR or GBP.
customer_refstringOptional. Your customer ID.
product_refstringOptional. Used by product rules.
metadataobjectOptional. Up to 50 keys with string values of up to 500 characters, used by metadata rules.
occurred_atintegerOptional. Unix timestamp of the payment. Default now.

Request

curl -X POST "https://fractionalpay.com/api/v1/payments" \
  -H "Authorization: Bearer $FP_KEY" \
  -H "Idempotency-Key: order-10482" \
  -H "Content-Type: application/json" \
  -d '{"external_id":"order-10482","amount":100000,"currency":"USD","customer_ref":"cus_771","product_ref":"plan-pro","metadata":{"channel":"web"}}'

Response

{
  "id": 122,
  "object": "payment",
  "external_id": "order-10482",
  "amount": 100000,
  "currency": "USD",
  "fee": 0,
  "net": 100000,
  "customer_ref": "cus_771",
  "product_ref": "plan-pro",
  "metadata": {
    "channel": "web"
  },
  "livemode": false,
  "status": "succeeded",
  "occurred_at": 1792062776,
  "created": 1792062776,
  "split": {
    "id": 122,
    "object": "split",
    "status": "calculated",
    "rule_id": 5,
    "rule_version": 1,
    "gross": 100000,
    "currency": "USD",
    "queued_reason": null,
    "calculated_at": 1792062776,
    "allocations": [
      {
        "id": 602,
        "beneficiary_id": 20,
        "role_key": null,
        "amount": 70000,
        "currency": "USD",
        "held_amount": 7000,
        "held_until": 1793272376,
        "status": "available",
        "payout_id": null
      },
      {
        "id": 603,
        "beneficiary_id": 21,
        "role_key": null,
        "amount": 10000,
        "currency": "USD",
        "held_amount": 0,
        "held_until": null,
        "status": "available",
        "payout_id": null
      },
      {
        "id": 604,
        "beneficiary_id": 22,
        "role_key": null,
        "amount": 5000,
        "currency": "USD",
        "held_amount": 0,
        "held_until": null,
        "status": "available",
        "payout_id": null
      },
      {
        "id": 605,
        "beneficiary_id": null,
        "role_key": "platform",
        "amount": 15000,
        "currency": "USD",
        "held_amount": 0,
        "held_until": null,
        "status": "available",
        "payout_id": null
      }
    ]
  },
  "refunds": []
}

Request

curl -X POST "https://fractionalpay.com/api/v1/payments" \
  -H "Authorization: Bearer $FP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"external_id":"order-10483","amount":4000,"currency":"USD","customer_ref":"cus_772"}'

Response when the payment cannot be split yet

{
  "id": 123,
  "object": "payment",
  "external_id": "order-10483",
  "amount": 4000,
  "currency": "USD",
  "fee": 0,
  "net": 4000,
  "customer_ref": "cus_772",
  "product_ref": null,
  "metadata": {},
  "livemode": false,
  "status": "succeeded",
  "occurred_at": 1792062780,
  "created": 1792062780,
  "split": {
    "id": 123,
    "object": "split",
    "status": "queued",
    "rule_id": 5,
    "rule_version": 1,
    "gross": 4000,
    "currency": "USD",
    "queued_reason": "shares_exceed_amount",
    "calculated_at": null,
    "allocations": []
  },
  "refunds": []
}

Retrieve a payment

GET /v1/payments/{id}

Returns the payment with its latest split, allocations and refunds. A test key reads test payments and a live key reads live payments. The example is the payment above after a partial refund.

Parameters of Retrieve a payment
ParameterTypeDescription
idintegerPath parameter. The payment ID.

Request

curl "https://fractionalpay.com/api/v1/payments/122" \
  -H "Authorization: Bearer $FP_KEY"

Response

{
  "id": 122,
  "object": "payment",
  "external_id": "order-10482",
  "amount": 100000,
  "currency": "USD",
  "fee": 0,
  "net": 100000,
  "customer_ref": "cus_771",
  "product_ref": "plan-pro",
  "metadata": {
    "channel": "web"
  },
  "livemode": false,
  "status": "partially_refunded",
  "occurred_at": 1792062776,
  "created": 1792062776,
  "split": {
    "id": 122,
    "object": "split",
    "status": "calculated",
    "rule_id": 5,
    "rule_version": 1,
    "gross": 100000,
    "currency": "USD",
    "queued_reason": null,
    "calculated_at": 1792062776,
    "allocations": [
      {
        "id": 602,
        "beneficiary_id": 20,
        "role_key": null,
        "amount": 56000,
        "currency": "USD",
        "held_amount": 0,
        "held_until": null,
        "status": "available",
        "payout_id": 37
      },
      {
        "id": 603,
        "beneficiary_id": 21,
        "role_key": null,
        "amount": 8000,
        "currency": "USD",
        "held_amount": 0,
        "held_until": null,
        "status": "available",
        "payout_id": 38
      },
      {
        "id": 604,
        "beneficiary_id": 22,
        "role_key": null,
        "amount": 4000,
        "currency": "USD",
        "held_amount": 0,
        "held_until": null,
        "status": "available",
        "payout_id": 39
      },
      {
        "id": 605,
        "beneficiary_id": null,
        "role_key": "platform",
        "amount": 12000,
        "currency": "USD",
        "held_amount": 0,
        "held_until": null,
        "status": "available",
        "payout_id": null
      }
    ]
  },
  "refunds": [
    {
      "id": 7,
      "object": "refund",
      "payment_id": 122,
      "amount": 20000,
      "currency": "USD",
      "kind": "refund",
      "reversal_strategy": "proportional",
      "status": "processed",
      "processed_at": 1792062780
    }
  ]
}

Refund a payment

POST /v1/payments/{id}/refunds

Reverses the split proportionally across every allocation, takes held amounts back first and writes a balanced ledger transaction. Returns the refund with 201 and the updated payment. Shares already paid out become a negative balance netted against the next payout.

Parameters of Refund a payment
ParameterTypeDescription
amountintegerRequired. Minor units, up to the amount not yet refunded.
reasonstringOptional. dispute records the reversal as kind dispute, any other value as kind refund.

Request

curl -X POST "https://fractionalpay.com/api/v1/payments/122/refunds" \
  -H "Authorization: Bearer $FP_KEY" \
  -H "Idempotency-Key: refund-order-10482-1" \
  -H "Content-Type: application/json" \
  -d '{"amount":20000,"reason":"refund"}'

Response

{
  "id": 7,
  "object": "refund",
  "payment_id": 122,
  "amount": 20000,
  "currency": "USD",
  "kind": "refund",
  "reversal_strategy": "proportional",
  "status": "processed",
  "processed_at": 1792062780,
  "payment": {
    "id": 122,
    "object": "payment",
    "external_id": "order-10482",
    "amount": 100000,
    "currency": "USD",
    "fee": 0,
    "net": 100000,
    "customer_ref": "cus_771",
    "product_ref": "plan-pro",
    "metadata": {
      "channel": "web"
    },
    "livemode": false,
    "status": "partially_refunded",
    "occurred_at": 1792062776,
    "created": 1792062776,
    "split": {
      "id": 122,
      "object": "split",
      "status": "calculated",
      "rule_id": 5,
      "rule_version": 1,
      "gross": 100000,
      "currency": "USD",
      "queued_reason": null,
      "calculated_at": 1792062776,
      "allocations": [
        {
          "id": 602,
          "beneficiary_id": 20,
          "role_key": null,
          "amount": 56000,
          "currency": "USD",
          "held_amount": 0,
          "held_until": null,
          "status": "available",
          "payout_id": null
        },
        {
          "id": 603,
          "beneficiary_id": 21,
          "role_key": null,
          "amount": 8000,
          "currency": "USD",
          "held_amount": 0,
          "held_until": null,
          "status": "available",
          "payout_id": null
        },
        {
          "id": 604,
          "beneficiary_id": 22,
          "role_key": null,
          "amount": 4000,
          "currency": "USD",
          "held_amount": 0,
          "held_until": null,
          "status": "available",
          "payout_id": null
        },
        {
          "id": 605,
          "beneficiary_id": null,
          "role_key": "platform",
          "amount": 12000,
          "currency": "USD",
          "held_amount": 0,
          "held_until": null,
          "status": "available",
          "payout_id": null
        }
      ]
    },
    "refunds": [
      {
        "id": 7,
        "object": "refund",
        "payment_id": 122,
        "amount": 20000,
        "currency": "USD",
        "kind": "refund",
        "reversal_strategy": "proportional",
        "status": "processed",
        "processed_at": 1792062780
      }
    ]
  }
}

Balances and ledger

List balances

GET /v1/balances

One balance object per beneficiary, newest beneficiary first, read from the ledger: pending (payouts scheduled, awaiting approval or sent and not yet paid), held (reserves), available (owed minus pending, due at the next payout) and paid, in the main currency, plus by_currency with owed per currency.

Parameters of List balances
ParameterTypeDescription
starting_afterintegerBeneficiary ID cursor.
limitinteger1 to 100, default 100.

Request

curl "https://fractionalpay.com/api/v1/balances?limit=1" \
  -H "Authorization: Bearer $FP_KEY"

Response

{
  "object": "list",
  "data": [
    {
      "object": "balance",
      "beneficiary_id": 22,
      "name": "Lumen Edit",
      "currency": "USD",
      "pending": 9000,
      "held": 0,
      "available": 0,
      "paid": 0,
      "by_currency": {
        "USD": {
          "pending": 9000,
          "held": 0,
          "available": 0,
          "paid": 0,
          "owed": 9000
        }
      }
    }
  ],
  "has_more": true
}

List ledger transactions

GET /v1/ledger/transactions

The journal, newest first. Every transaction balances to zero: the sum of debits equals the sum of credits, and balanced says so.

Parameters of List ledger transactions
ParameterTypeDescription
fromstring or integerDate (2026-10-01) or unix timestamp, inclusive.
tostring or integerDate or unix timestamp, inclusive. A date alone covers that whole day.
source_typestringpayment, refund, payout or split_allocation (a released hold).
starting_afterintegerCursor.
limitinteger1 to 100, default 20.

Request

curl "https://fractionalpay.com/api/v1/ledger/transactions?source_type=payment&limit=1" \
  -H "Authorization: Bearer $FP_KEY"

Response

{
  "object": "list",
  "data": [
    {
      "id": 214,
      "object": "ledger_transaction",
      "source_type": "payment",
      "source_id": 124,
      "description": "Payment order-10490 split by rule \"Marketplace default\" v1",
      "posted_at": 1792062790,
      "entries": [
        {
          "id": 1305,
          "account": "Operator balance (your account)",
          "account_id": 48,
          "account_kind": "clearing",
          "direction": "debit",
          "amount": 2000000,
          "currency": "USD"
        },
        {
          "id": 1306,
          "account": "Payable to Northwind Studio",
          "account_id": 49,
          "account_kind": "payable",
          "direction": "credit",
          "amount": 1345500,
          "currency": "USD"
        },
        {
          "id": 1307,
          "account": "Reserve for Northwind Studio",
          "account_id": 50,
          "account_kind": "reserve",
          "direction": "credit",
          "amount": 149500,
          "currency": "USD"
        },
        {
          "id": 1308,
          "account": "Payable to Harbor Partners",
          "account_id": 51,
          "account_kind": "payable",
          "direction": "credit",
          "amount": 200000,
          "currency": "USD"
        },
        {
          "id": 1309,
          "account": "Payable to Lumen Edit",
          "account_id": 53,
          "account_kind": "payable",
          "direction": "credit",
          "amount": 5000,
          "currency": "USD"
        },
        {
          "id": 1310,
          "account": "Platform revenue",
          "account_id": 55,
          "account_kind": "platform_revenue",
          "direction": "credit",
          "amount": 300000,
          "currency": "USD"
        }
      ],
      "balanced": true
    }
  ],
  "has_more": true
}

Retrieve a ledger transaction

GET /v1/ledger/transactions/{id}

Returns one transaction with its entries: account name, account_id, account_kind (clearing, payable, reserve, platform_revenue, fees), direction debit or credit, amount and currency. The example is the refund above.

Parameters of Retrieve a ledger transaction
ParameterTypeDescription
idintegerPath parameter. The transaction ID.

Request

curl "https://fractionalpay.com/api/v1/ledger/transactions/213" \
  -H "Authorization: Bearer $FP_KEY"

Response

{
  "id": 213,
  "object": "ledger_transaction",
  "source_type": "refund",
  "source_id": 7,
  "description": "Refund of $200.00 on payment order-10482 reversed proportionally",
  "posted_at": 1792062780,
  "entries": [
    {
      "id": 1299,
      "account": "Reserve for Northwind Studio",
      "account_id": 50,
      "account_kind": "reserve",
      "direction": "debit",
      "amount": 7000,
      "currency": "USD"
    },
    {
      "id": 1300,
      "account": "Payable to Northwind Studio",
      "account_id": 49,
      "account_kind": "payable",
      "direction": "debit",
      "amount": 7000,
      "currency": "USD"
    },
    {
      "id": 1301,
      "account": "Payable to Harbor Partners",
      "account_id": 51,
      "account_kind": "payable",
      "direction": "debit",
      "amount": 2000,
      "currency": "USD"
    },
    {
      "id": 1302,
      "account": "Payable to Lumen Edit",
      "account_id": 53,
      "account_kind": "payable",
      "direction": "debit",
      "amount": 1000,
      "currency": "USD"
    },
    {
      "id": 1303,
      "account": "Platform revenue",
      "account_id": 55,
      "account_kind": "platform_revenue",
      "direction": "debit",
      "amount": 3000,
      "currency": "USD"
    },
    {
      "id": 1304,
      "account": "Operator balance (your account)",
      "account_id": 48,
      "account_kind": "clearing",
      "direction": "credit",
      "amount": 20000,
      "currency": "USD"
    }
  ],
  "balanced": true
}

Payouts

List payouts

GET /v1/payouts

Payouts, newest first. scheduled_for, sent_at and approved_at are unix timestamps.

Parameters of List payouts
ParameterTypeDescription
statusstringscheduled, held, awaiting_approval, sent, paid or failed.
beneficiary_idintegerOnly payouts of this beneficiary.
starting_afterintegerCursor.
limitinteger1 to 100, default 20.

Request

curl "https://fractionalpay.com/api/v1/payouts?status=scheduled&limit=1" \
  -H "Authorization: Bearer $FP_KEY"

Response

{
  "object": "list",
  "data": [
    {
      "id": 39,
      "object": "payout",
      "beneficiary_id": 22,
      "amount": 9000,
      "currency": "USD",
      "status": "scheduled",
      "scheduled_for": 1792141200,
      "sent_at": null,
      "external_transfer_id": null,
      "failure_reason": null,
      "requires_approval": false,
      "approved_by": null,
      "approved_at": null,
      "created": 1792062790
    }
  ],
  "has_more": true
}

Approve a payout

POST /v1/payouts/{id}/approve

Four-eyes approval on Scale. A payout above the approval threshold waits in awaiting_approval. The approver is the team member who created the API key, and it must be someone other than the person who scheduled the payout; otherwise the answer is 409 conflict. Outside Scale the answer is 402 plan_limit. No request body.

Parameters of Approve a payout
ParameterTypeDescription
idintegerPath parameter. The payout ID.

Request

curl -X POST "https://fractionalpay.com/api/v1/payouts/37/approve" \
  -H "Authorization: Bearer $FP_KEY" \
  -H "Idempotency-Key: approve-37"

Response

{
  "id": 37,
  "object": "payout",
  "beneficiary_id": 20,
  "amount": 1401500,
  "currency": "USD",
  "status": "scheduled",
  "scheduled_for": 1792141200,
  "sent_at": null,
  "external_transfer_id": null,
  "failure_reason": null,
  "requires_approval": true,
  "approved_by": 7,
  "approved_at": 1792062800,
  "created": 1792062790
}

Webhooks

Events

Add endpoints in Developers in the dashboard, each for test or live events and for all events or a chosen list. Each endpoint has its own signing secret (whsec_...) and receives events as a POST with a compact JSON body, the headers Content-Type: application/json, User-Agent: FractionalPay-Webhooks/1.0 and FractionalPay-Signature. Redirects are not followed and an attempt times out after 10 seconds. Every event has id (evt_ followed by 24 characters), type, created (unix), livemode and data; data holds the same object shapes as the API. Send test event in the dashboard delivers a split.created example with an id starting evt_test_ and data.test set to true.

EventSent when
split.createdA payment was split by a rule. data holds split (with allocations), payment_id and external_id.
split.reversedA refund or dispute reversed part of a split. data holds refund, the updated split and payment_id.
payout.scheduledA payout was created for a beneficiary. data holds payout.
payout.paidYour operator confirmed the payout. data holds payout.
payout.failedYour operator rejected the payout. data.payout.failure_reason explains why.
hold.releasedA held amount became available. data holds allocation (with released_amount) and payment_id.
reconciliation.mismatchReconciliation found a difference between the ledger and your operator. data holds run_id, period_start, period_end and item (id, kind, external_id, expected_amount, actual_amount, currency).

split.created payload, as delivered for the payment above (formatted here)

{
  "id": "evt_crrbpjzta1tdknx2ozgaymgd",
  "data": {
    "split": {
      "id": 122,
      "gross": 100000,
      "object": "split",
      "status": "calculated",
      "rule_id": 5,
      "currency": "USD",
      "allocations": [
        {
          "id": 602,
          "amount": 70000,
          "status": "available",
          "currency": "USD",
          "role_key": null,
          "payout_id": null,
          "held_until": 1793272376,
          "held_amount": 7000,
          "beneficiary_id": 20
        },
        {
          "id": 603,
          "amount": 10000,
          "status": "available",
          "currency": "USD",
          "role_key": null,
          "payout_id": null,
          "held_until": null,
          "held_amount": 0,
          "beneficiary_id": 21
        },
        {
          "id": 604,
          "amount": 5000,
          "status": "available",
          "currency": "USD",
          "role_key": null,
          "payout_id": null,
          "held_until": null,
          "held_amount": 0,
          "beneficiary_id": 22
        },
        {
          "id": 605,
          "amount": 15000,
          "status": "available",
          "currency": "USD",
          "role_key": "platform",
          "payout_id": null,
          "held_until": null,
          "held_amount": 0,
          "beneficiary_id": null
        }
      ],
      "rule_version": 1,
      "calculated_at": 1792062776,
      "queued_reason": null
    },
    "payment_id": 122,
    "external_id": "order-10482"
  },
  "type": "split.created",
  "created": 1792062776,
  "livemode": false
}

Verifying signatures

Every delivery carries FractionalPay-Signature: t=<unix>,v1=<hex>. v1 is the HMAC-SHA256 of the string <t>.<raw body> with your endpoint secret. Compute it over the raw body before parsing JSON, compare in constant time and reject timestamps more than five minutes away. The three samples below were checked against a real signed delivery.

# Header received: FractionalPay-Signature: t=1792062817,v1=dcb0a7daca5c...
# Recompute the signature from the raw body exactly as received (body.json).
t=1792062817
payload="$t.$(cat body.json)"
expected=$(printf '%s' "$payload" | openssl dgst -sha256 -hmac "$FP_WEBHOOK_SECRET" | sed 's/^.* //')
echo "$expected"   # compare with v1, and reject t older than 5 minutes

# Test your endpoint locally with a correctly signed body:
curl -X POST http://localhost:3000/webhooks/fractionalpay \
  -H "Content-Type: application/json" \
  -H "FractionalPay-Signature: t=$t,v1=$expected" \
  --data-binary @body.json
import crypto from 'node:crypto';
import express from 'express';

const app = express();
const secret = process.env.FP_WEBHOOK_SECRET;

app.post('/webhooks/fractionalpay', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.get('FractionalPay-Signature') || '';
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.${req.body}`)
    .digest('hex');

  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  const valid = parts.v1 && expected.length === parts.v1.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
  if (!fresh || !valid) return res.status(400).end();

  const event = JSON.parse(req.body);
  // Handle event.type once per event.id, then answer 2xx quickly.
  res.status(200).end();
});
<?php
$secret = getenv('FP_WEBHOOK_SECRET');
$body = file_get_contents('php://input');
$header = $_SERVER['HTTP_FRACTIONALPAY_SIGNATURE'] ?? '';

parse_str(str_replace(',', '&', $header), $parts);
$expected = hash_hmac('sha256', ($parts['t'] ?? '').'.'.$body, $secret);

$fresh = abs(time() - (int) ($parts['t'] ?? 0)) < 300;
if (! $fresh || ! hash_equals($expected, $parts['v1'] ?? '')) {
    http_response_code(400);
    exit;
}

$event = json_decode($body, true);
// Handle $event['type'] once per $event['id'], then answer 2xx quickly.
http_response_code(200);

Retries and replay

Answer with any 2xx status. Anything else, a timeout or a refused connection is retried with exponential backoff: six more attempts, 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours after the previous one, about 21 hours in total. After the last attempt the delivery stays failed. Every attempt is listed in the delivery log in the dashboard, with the status code and the start of your answer, where you can replay a delivery by hand with the same event id and payload. Events can arrive more than once and out of order, so process each id once and read the current state from the API when order matters.

Next steps

See how the same API runs marketplace payouts and the payouts API, compare plans on the pricing page, or write to [email protected].