split-free.openapps.xyz

Source

This app’s code and revision history are public. Download a copy or explore the files below.

Public
The app’s code and revision history are public.
Owned app · rev 9ed962174401
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone 20ace674a35b849ffda63b30
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
← All files
public/openapi.json
{
  "openapi": "3.1.0",
  "info": {
    "title": "Split by OpenApps",
    "version": "1.1.0",
    "summary": "Free expense splitting for groups: who owes whom and the fewest payments to settle up.",
    "description": "Split is a free alternative to Splitwise. No key and no account: a group's id is its secret, exactly like its link, so anyone with it can read and change the group. Amounts are decimal strings in the group's currency (\"12.50\"). People can be named by name (case-insensitive) or by id. Every route that reads or changes a group answers with the whole group, including balances and settleUp. Errors are JSON with a plain error sentence that says what to change. Notes for assistants: https://split-free.openapps.xyz/llms.txt",
    "contact": {
      "name": "OpenApps",
      "url": "https://free-apps.openapps.xyz/"
    }
  },
  "externalDocs": {
    "description": "Notes for assistants (llms.txt)",
    "url": "https://split-free.openapps.xyz/llms.txt"
  },
  "servers": [
    {
      "url": "https://functions.bounded.sh/apps/20ace674a35b849ffda63b30/api"
    }
  ],
  "paths": {
    "/": {
      "get": {
        "operationId": "listRoutes",
        "summary": "List the routes",
        "responses": {
          "200": {
            "description": "The routes, one line each, and links to send people.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/groups": {
      "post": {
        "operationId": "createGroup",
        "summary": "Create a group",
        "description": "Give the person the returned group.url: it is how they and their friends open the group.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewGroup"
              },
              "example": {
                "name": "Lisbon trip",
                "currency": "EUR",
                "people": [
                  "Sam",
                  "Alex",
                  "Jo"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new group with its people and their ids.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Group"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "share": {
                          "type": "string"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/E400"
          }
        }
      }
    },
    "/groups/{groupId}": {
      "get": {
        "operationId": "getGroup",
        "summary": "Read a group: people, expenses, balances and who pays whom",
        "parameters": [
          {
            "name": "groupId",
            "in": "path",
            "required": true,
            "description": "The group's id, from its link. It is the group's secret.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9]{8,32}$"
            },
            "example": "RbNuxRv3xdHpoLWqqZoN"
          }
        ],
        "responses": {
          "200": {
            "description": "The whole group.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Group"
                },
                "example": {
                  "group": {
                    "id": "RbNuxRv3xdHpoLWqqZoN",
                    "name": "Lisbon trip",
                    "currency": "EUR",
                    "url": "https://split-free.openapps.xyz/g/RbNuxRv3xdHpoLWqqZoN"
                  },
                  "people": [
                    {
                      "id": "B28aAS7UQd",
                      "name": "Sam"
                    },
                    {
                      "id": "Cw7EiEUfo7",
                      "name": "Alex"
                    },
                    {
                      "id": "RmvFimjbA7",
                      "name": "Jo"
                    }
                  ],
                  "totalSpent": "84.60",
                  "balances": [
                    {
                      "personId": "B28aAS7UQd",
                      "name": "Sam",
                      "amount": "56.40",
                      "display": "€56.40",
                      "meaning": "gets money back",
                      "paid": "84.60",
                      "share": "28.20"
                    },
                    {
                      "personId": "Cw7EiEUfo7",
                      "name": "Alex",
                      "amount": "-28.20",
                      "display": "-€28.20",
                      "meaning": "owes",
                      "paid": "0.00",
                      "share": "28.20"
                    },
                    {
                      "personId": "RmvFimjbA7",
                      "name": "Jo",
                      "amount": "-28.20",
                      "display": "-€28.20",
                      "meaning": "owes",
                      "paid": "0.00",
                      "share": "28.20"
                    }
                  ],
                  "settleUp": [
                    {
                      "from": "Alex",
                      "fromId": "Cw7EiEUfo7",
                      "to": "Sam",
                      "toId": "B28aAS7UQd",
                      "amount": "28.20",
                      "display": "€28.20"
                    },
                    {
                      "from": "Jo",
                      "fromId": "RmvFimjbA7",
                      "to": "Sam",
                      "toId": "B28aAS7UQd",
                      "amount": "28.20",
                      "display": "€28.20"
                    }
                  ],
                  "expenses": [
                    {
                      "id": "F2AYkJz7VNW7",
                      "kind": "expense",
                      "title": "Dinner",
                      "amount": "84.60",
                      "paidBy": "Sam",
                      "date": "2026-10-18",
                      "splitMode": "equal",
                      "split": {
                        "Alex": "28.20",
                        "Jo": "28.20",
                        "Sam": "28.20"
                      }
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/E404"
          }
        }
      },
      "patch": {
        "operationId": "updateGroup",
        "summary": "Rename the group or change its currency",
        "description": "The currency can change only before the first expense.",
        "parameters": [
          {
            "name": "groupId",
            "in": "path",
            "required": true,
            "description": "The group's id, from its link. It is the group's secret.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9]{8,32}$"
            },
            "example": "RbNuxRv3xdHpoLWqqZoN"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "currency": {
                    "$ref": "#/components/schemas/Currency"
                  }
                }
              },
              "example": {
                "name": "Lisbon trip 2026"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The whole group.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Group"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/E400"
          },
          "404": {
            "$ref": "#/components/responses/E404"
          },
          "409": {
            "$ref": "#/components/responses/E409"
          }
        }
      }
    },
    "/groups/{groupId}/people": {
      "post": {
        "operationId": "addPerson",
        "summary": "Add a person",
        "parameters": [
          {
            "name": "groupId",
            "in": "path",
            "required": true,
            "description": "The group's id, from its link. It is the group's secret.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9]{8,32}$"
            },
            "example": "RbNuxRv3xdHpoLWqqZoN"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 40
                  }
                }
              },
              "example": {
                "name": "Riley"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The whole group.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Group"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/E400"
          },
          "404": {
            "$ref": "#/components/responses/E404"
          },
          "409": {
            "$ref": "#/components/responses/E409"
          }
        }
      }
    },
    "/groups/{groupId}/people/{personId}": {
      "patch": {
        "operationId": "renamePerson",
        "summary": "Rename a person",
        "parameters": [
          {
            "name": "groupId",
            "in": "path",
            "required": true,
            "description": "The group's id, from its link. It is the group's secret.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9]{8,32}$"
            },
            "example": "RbNuxRv3xdHpoLWqqZoN"
          },
          {
            "name": "personId",
            "in": "path",
            "required": true,
            "description": "The person's id, from GET /groups/{groupId}.",
            "schema": {
              "type": "string"
            },
            "example": "Cw7EiEUfo7"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 40
                  }
                }
              },
              "example": {
                "name": "Riley B"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The whole group.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Group"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/E400"
          },
          "404": {
            "$ref": "#/components/responses/E404"
          },
          "409": {
            "$ref": "#/components/responses/E409"
          }
        }
      },
      "delete": {
        "operationId": "removePerson",
        "summary": "Remove a person whose balance is zero",
        "description": "Their past expenses stay. Someone who still owes or is owed money cannot be removed (409).",
        "parameters": [
          {
            "name": "groupId",
            "in": "path",
            "required": true,
            "description": "The group's id, from its link. It is the group's secret.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9]{8,32}$"
            },
            "example": "RbNuxRv3xdHpoLWqqZoN"
          },
          {
            "name": "personId",
            "in": "path",
            "required": true,
            "description": "The person's id, from GET /groups/{groupId}.",
            "schema": {
              "type": "string"
            },
            "example": "Cw7EiEUfo7"
          }
        ],
        "responses": {
          "200": {
            "description": "The whole group.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Group"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/E404"
          },
          "409": {
            "$ref": "#/components/responses/E409"
          }
        }
      }
    },
    "/groups/{groupId}/expenses": {
      "post": {
        "operationId": "addExpense",
        "summary": "Add an expense",
        "parameters": [
          {
            "name": "groupId",
            "in": "path",
            "required": true,
            "description": "The group's id, from its link. It is the group's secret.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9]{8,32}$"
            },
            "example": "RbNuxRv3xdHpoLWqqZoN"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewExpense"
              },
              "example": {
                "title": "Dinner",
                "amount": "84.60",
                "paidBy": "Sam"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new expense id and the whole group.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Group"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "created": {
                          "type": "string"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/E400"
          },
          "404": {
            "$ref": "#/components/responses/E404"
          }
        }
      }
    },
    "/groups/{groupId}/expenses/{expenseId}": {
      "patch": {
        "operationId": "editExpense",
        "summary": "Edit an expense or a payment",
        "description": "Send only what changes. Without split or splitMode the old split stays; an exact split must still add up to the new amount. For a payment, send from, to, amount or date.",
        "parameters": [
          {
            "name": "groupId",
            "in": "path",
            "required": true,
            "description": "The group's id, from its link. It is the group's secret.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9]{8,32}$"
            },
            "example": "RbNuxRv3xdHpoLWqqZoN"
          },
          {
            "name": "expenseId",
            "in": "path",
            "required": true,
            "description": "The expense or payment id, from GET /groups/{groupId}.",
            "schema": {
              "type": "string"
            },
            "example": "F2AYkJz7VNW7"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExpenseChange"
              },
              "example": {
                "amount": "90.00"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The edited expense id and the whole group.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Group"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "updated": {
                          "type": "string"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/E400"
          },
          "404": {
            "$ref": "#/components/responses/E404"
          }
        }
      },
      "delete": {
        "operationId": "deleteExpense",
        "summary": "Delete an expense or a payment",
        "parameters": [
          {
            "name": "groupId",
            "in": "path",
            "required": true,
            "description": "The group's id, from its link. It is the group's secret.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9]{8,32}$"
            },
            "example": "RbNuxRv3xdHpoLWqqZoN"
          },
          {
            "name": "expenseId",
            "in": "path",
            "required": true,
            "description": "The expense or payment id, from GET /groups/{groupId}.",
            "schema": {
              "type": "string"
            },
            "example": "F2AYkJz7VNW7"
          }
        ],
        "responses": {
          "200": {
            "description": "The whole group.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Group"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/E404"
          }
        }
      }
    },
    "/groups/{groupId}/payments": {
      "post": {
        "operationId": "recordPayment",
        "summary": "Record a payment between two people",
        "description": "Record it after the money changes hands. Split never moves money.",
        "parameters": [
          {
            "name": "groupId",
            "in": "path",
            "required": true,
            "description": "The group's id, from its link. It is the group's secret.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9]{8,32}$"
            },
            "example": "RbNuxRv3xdHpoLWqqZoN"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewPayment"
              },
              "example": {
                "from": "Jo",
                "to": "Sam",
                "amount": "28.20"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new payment id and the whole group.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Group"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "created": {
                          "type": "string"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/E400"
          },
          "404": {
            "$ref": "#/components/responses/E404"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Amount": {
        "type": "string",
        "description": "A decimal amount in the group's currency. Numbers are accepted too.",
        "example": "12.50"
      },
      "Day": {
        "type": "string",
        "description": "A day, YYYY-MM-DD. Defaults to now.",
        "example": "2026-10-18"
      },
      "Currency": {
        "type": "string",
        "enum": [
          "USD",
          "EUR",
          "GBP",
          "CAD",
          "AUD",
          "NZD",
          "JPY",
          "CHF",
          "SEK",
          "NOK",
          "DKK",
          "PLN",
          "CZK",
          "HUF",
          "INR",
          "SGD",
          "HKD",
          "CNY",
          "KRW",
          "MXN",
          "BRL",
          "ARS",
          "CLP",
          "COP",
          "ZAR",
          "AED",
          "ILS",
          "TRY",
          "THB",
          "IDR",
          "PHP",
          "VND"
        ]
      },
      "PersonRef": {
        "type": "string",
        "description": "A person's name (case-insensitive) or id."
      },
      "Split": {
        "description": "Omit to split equally between everyone. A list of people splits equally between them. An object of person to weight splits by shares (percentages work as shares); with splitMode exact it is person to amount, and the amounts must add up to the total.",
        "oneOf": [
          {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PersonRef"
            }
          },
          {
            "type": "object",
            "additionalProperties": {
              "oneOf": [
                {
                  "type": "number"
                },
                {
                  "type": "string"
                }
              ]
            }
          }
        ]
      },
      "NewGroup": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 80
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "people": {
            "type": "array",
            "maxItems": 50,
            "items": {
              "type": "string",
              "maxLength": 40
            }
          }
        }
      },
      "NewExpense": {
        "type": "object",
        "required": [
          "title",
          "amount",
          "paidBy"
        ],
        "properties": {
          "title": {
            "type": "string",
            "maxLength": 120
          },
          "amount": {
            "$ref": "#/components/schemas/Amount"
          },
          "paidBy": {
            "$ref": "#/components/schemas/PersonRef"
          },
          "split": {
            "$ref": "#/components/schemas/Split"
          },
          "splitMode": {
            "type": "string",
            "enum": [
              "equal",
              "shares",
              "exact"
            ]
          },
          "date": {
            "$ref": "#/components/schemas/Day"
          }
        }
      },
      "ExpenseChange": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "maxLength": 120
          },
          "amount": {
            "$ref": "#/components/schemas/Amount"
          },
          "paidBy": {
            "$ref": "#/components/schemas/PersonRef"
          },
          "split": {
            "$ref": "#/components/schemas/Split"
          },
          "splitMode": {
            "type": "string",
            "enum": [
              "equal",
              "shares",
              "exact"
            ]
          },
          "date": {
            "$ref": "#/components/schemas/Day"
          },
          "from": {
            "$ref": "#/components/schemas/PersonRef"
          },
          "to": {
            "$ref": "#/components/schemas/PersonRef"
          }
        }
      },
      "NewPayment": {
        "type": "object",
        "required": [
          "from",
          "to",
          "amount"
        ],
        "properties": {
          "from": {
            "$ref": "#/components/schemas/PersonRef"
          },
          "to": {
            "$ref": "#/components/schemas/PersonRef"
          },
          "amount": {
            "$ref": "#/components/schemas/Amount"
          },
          "date": {
            "$ref": "#/components/schemas/Day"
          }
        }
      },
      "Group": {
        "type": "object",
        "properties": {
          "group": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "currency": {
                "type": "string"
              },
              "url": {
                "type": "string",
                "format": "uri"
              }
            }
          },
          "people": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                }
              }
            }
          },
          "totalSpent": {
            "$ref": "#/components/schemas/Amount"
          },
          "balances": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "personId": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "amount": {
                  "type": "string",
                  "description": "Above zero gets money back; below zero owes."
                },
                "display": {
                  "type": "string"
                },
                "meaning": {
                  "type": "string",
                  "enum": [
                    "gets money back",
                    "owes",
                    "settled"
                  ]
                },
                "paid": {
                  "type": "string"
                },
                "share": {
                  "type": "string"
                }
              }
            }
          },
          "settleUp": {
            "type": "array",
            "description": "The fewest payments that settle everything.",
            "items": {
              "type": "object",
              "properties": {
                "from": {
                  "type": "string"
                },
                "fromId": {
                  "type": "string"
                },
                "to": {
                  "type": "string"
                },
                "toId": {
                  "type": "string"
                },
                "amount": {
                  "type": "string"
                },
                "display": {
                  "type": "string"
                }
              }
            }
          },
          "expenses": {
            "type": "array",
            "description": "Newest first.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "kind": {
                  "type": "string",
                  "enum": [
                    "expense",
                    "payment"
                  ]
                },
                "title": {
                  "type": "string"
                },
                "amount": {
                  "type": "string"
                },
                "paidBy": {
                  "type": "string"
                },
                "to": {
                  "type": "string",
                  "description": "Payments only."
                },
                "date": {
                  "type": "string"
                },
                "splitMode": {
                  "type": "string"
                },
                "split": {
                  "type": "object",
                  "additionalProperties": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "A sentence that says what to change."
          }
        }
      }
    },
    "responses": {
      "E400": {
        "description": "Bad input. The error sentence says what to change.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "paidBy must name a person in the group."
            }
          }
        }
      },
      "E404": {
        "description": "No group, person or expense with that id.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "No group with that id."
            }
          }
        }
      },
      "E409": {
        "description": "A conflict: a duplicate name, a currency change after the first expense, or removing someone with a balance.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Riley is already in the group."
            }
          }
        }
      }
    }
  }
}

Change history

Publish quality pass from owner chat
9ed9621744 · 5 hours ago
Bind agent address in policy constants
7effc1cbbd · 5 hours ago
Quality pass: cleaner group page, phone polish, fuller API, sourced comparison
5345219781 · 5 hours ago
Bounded source sync
277fb9ad96 · 1 day ago
Bounded source sync
e944552325 · 1 day ago