Penny Docs

Overview

Base URL, authentication, household scoping, pagination, and errors.

The Penny API gives scripts and integrations access to household financial data. Connect bank accounts and sync Apple financial accounts through the Penny app.

Download OpenAPI schema for this reference. You can import it into API tools or use it to generate a client.

Base URL

https://api.penny.finance

Every path is versioned under /v1.

Authentication

Use a personal API token to authenticate scripts and integrations. In the app, open Settings → API Tokens and create a token. Give it a name, choose its access level and expiration, then copy the token.

The full token is shown only once. Save it somewhere secure, such as a password manager, before closing the screen.

Send the token in the Authorization header on each request:

Authorization: Bearer penny_pat_YOUR_TOKEN

For example, with your token stored in the PENNY_API_TOKEN environment variable:

curl https://api.penny.finance/v1/households \
  -H "Authorization: Bearer $PENNY_API_TOKEN"

Token permissions

  • Read only (read) permits read requests. Writes return 403 read_only_token.
  • Read and write (read_write) permits actions your account is allowed to perform.

A token never has more access than you do. A household viewer stays read-only even with a read/write token. If your role changes or you are removed from a household, existing tokens follow your new permissions on subsequent requests. Household subscription requirements also apply.

Expiry and revocation

The app defaults to a 90-day expiration. You can choose 30 days, 90 days, 1 year, or Never when creating a token. Existing tokens keep their original expiration dates.

To revoke a token, open Settings → API Tokens, select it, and confirm Revoke. Expired or revoked tokens return 401 not_authenticated; create a replacement to continue using the API. Changing or resetting your password also invalidates existing tokens.

To rotate a token, create a replacement, update your integration to use it, then revoke the old token.

Treat tokens like passwords: keep them out of source control, shared scripts, and application logs. Use a read-only token when your integration only needs to retrieve data.

Household scope

Most financial data belongs to a household, whose ID is included in the path:

/v1/households/{household_id}/transactions

Call GET /v1/households first to find the IDs you have access to. What you can do inside one depends on your role: viewers get read-only, and owner-only routes return 403 for everyone else.

How households work →

IDs

Resource IDs are opaque strings. Don't parse them, and don't assume ordering or length.

Pagination

Paginated list endpoints return a page of results and a cursor for the next page:

GET /v1/households/{household_id}/transactions
{
  "transactions": [],
  "next_cursor": "eyJkIjoiMjAyNi0wNy0xNCIsImkiOiJ0eG5fOWYz...",
  "has_more": true
}

Transaction objects are omitted here to highlight the pagination fields.

Pass next_cursor back as the cursor query parameter for the next page, and stop when has_more is false. Use min_limit to hint at page size.

Treat cursors as opaque strings. Keep the same filters while paging; if you change a filter, start again without a cursor. Transaction pages keep complete days together, so a page can contain more than min_limit transactions.

Errors

Failures return a JSON body with a stable machine-readable code and a message meant for humans:

401 response
{ "code": "not_authenticated", "message": "Authentication required" }

Use code for error handling. Message wording and status text may change.

StatusMeaning
400The request was malformed or the operation isn't valid in this state
401Missing, expired, revoked, or invalid token
402The household requires an active subscription
403The token's access level or your current household membership or role doesn't allow it
404No such resource, or it isn't in a household you can see
415A JSON request body was sent without Content-Type: application/json
422Validation failed. The body carries an errors array describing each field error
500Internal server error

A 422 looks like this:

422 response
{
  "code": "validation_error",
  "message": "Some fields are invalid.",
  "errors": [
    {
      "field": "amount",
      "code": "decimal_parsing",
      "message": "Input should be a valid decimal",
      "context": null
    }
  ]
}

For validation_error, read errors[] to handle each failure. Each entry has a field containing the public field path (for example, amount or splits.0.amount), a machine-readable code, and a human-readable message. Field paths omit the request source (body, query, or path). The context field contains additional validator details as a string-valued object, or null when there are none.

Money and dates

Monetary amounts use decimal strings to avoid binary floating-point rounding. Each amount has a currency, which can differ from the household currency. For details, see multiple currencies.

Dates are YYYY-MM-DD. Timestamps are ISO 8601 in UTC.

Expenses are negative and income is positive, on every account type. More on signs →

Trying it out

Each endpoint page has a playground. Select PersonalApiToken and paste the token itself, without Bearer; the playground adds that prefix to the Authorization header. Send issues a real production request against your data. Write requests change your data; the playground has no sandbox.

Compatibility

We maintain backward compatibility for the public /v1 API. Existing paths, field names, field types, and error codes remain compatible as the API evolves.

Clients should tolerate new endpoints, response fields, and enum values:

  • Ignore unrecognized response fields.
  • Preserve unrecognized enum values as strings or decode them into an unknown case.

Apply these settings to generated clients too.

Last updated on

On this page