Errors

The error response format, how to handle each kind of failure, and every error code the API returns.

Every failed request returns a JSON body with a stable, machine-readable code and a message written for humans:

404 response
{ "code": "transaction_not_found", "message": "Transaction not found." }

Branch on code. Message wording can change at any time, so don't match on it or show it to end users as-is. Treat a code you don't recognize as a generic failure of its HTTP status. While Penny is in pre-launch development, codes can change; see the compatibility policy.

Handling errors

Authentication: 401

not_authenticated means the Authorization header is missing or the token is malformed, expired, or revoked. Retrying won't help. Create a new token in Settings → API Tokens and update your integration.

Permissions: 403

  • read_only_token: a read-only token made a write request. Use a read/write token.
  • not_household_member: you don't belong to the household in the path, or you were removed from it. Call GET /v1/households to see the households you can access.
  • insufficient_household_role: your role in the household doesn't allow the action. Viewers can only read, and some routes are limited to the owner.

A token never has more access than the person who created it, so a read/write token held by a viewer still gets insufficient_household_role on writes.

Subscription: 402

subscription_required means the household's subscription has lapsed. The body also carries the household_id it applies to:

402 response
{
  "code": "subscription_required",
  "message": "This household's subscription has lapsed",
  "household_id": "Hh4vK8pQ2sLm"
}

GET /v1/households reports these households with an access_status of subscription_required, so you can check before requesting a household's data.

Validation: 422

validation_error means the request was understood but one or more values were rejected. The errors array lists every field that failed, so you can fix them in one pass:

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

field is the path to the value, without the request source (body, query, or path). context holds extra validator detail as a string-valued object, or null.

Not found: 404

A *_not_found code means the resource doesn't exist, was deleted, or belongs to a different household. The API doesn't say which, so it never confirms that someone else's ID exists.

Bulk transaction endpoints skip transaction IDs that are missing or ineligible and return the transactions they changed. They return a 404 only when nothing matched, so compare the returned IDs with the ones you sent to find what was skipped.

Conflicts and invalid operations: 400 and 409

These mean the request was well formed but can't be applied. A 409 conflicts with the resource's current state: a name that's already taken (duplicate_name), an export that's still being prepared (export_in_progress), or a transaction that can't be split because it's pending. A 400 is a request that can never succeed as sent, such as split amounts that don't add up to the original (sum_does_not_match_total). Change the request or the resource before retrying.

Pagination: 400

invalid_cursor means the cursor is malformed, and filters_changed means it was sent with different filters from the request that produced it. In both cases, start again without a cursor.

Uploads: 400 and 413

Images must be JPEG, PNG, or WebP (invalid_file_type) and no larger than 5 MiB (file_too_large).

Server errors: 500 and 503

internal_error means something went wrong on our side. Read requests are safe to retry. The API has no idempotency keys, so after a failed write, read the resource back before sending the request again.

service_unavailable means the request waited too long for another change to the same household, such as a bank sync, and nothing it asked for was applied. Retry it after a short wait.

Requests that don't reach an endpoint

A request body that isn't valid JSON returns 400 with the code bad_request. A path that doesn't exist returns 404 with not_found, and an HTTP method an endpoint doesn't support returns 405 with method_not_allowed, and a JSON body sent without Content-Type: application/json returns 415 with unsupported_media_type. All four use the same code and message body, with the code named after the HTTP status.

Error codes

Every code the API returns. Each endpoint's page lists the ones that apply to it.

CodeStatusExample messageReturned by
bad_request400Bad Request39 endpoints
child_cannot_unsplit400Cannot unsplit a child of a split; pass the parent transaction id.Unsplit Transaction
filters_changed400Filters have changed since the cursor was created. Please start a new query.List Connection Status, List Institutions, Dry Run, List Transactions
invalid_cursor400Invalid pagination cursor. Please start a new query.8 endpoints
invalid_file_type400File must be JPEG, PNG, or WebP5 endpoints
invalid_frequency_fields400Invalid frequency fields: twice-monthly frequencies must omit start_date; all other frequencies require start_dateCreate Recurring Transaction, Update Recurring Transaction
invalid_month_format400month must be in YYYY-MM formatGet Summary
invalid_rule_ids400Rule IDs must match exactly all rules in the householdReorder Rules
split_sign_mismatch400Each split's sign must match the original transaction.Split Transaction
sum_does_not_match_total400The sum of the split amounts must equal the original transaction amount.Split Transaction
too_few_splits400A split must contain at least two parts.Split Transaction
zero_amount_split400Split amounts must not be zero.Split Transaction
not_authenticated401Authentication required100 endpoints
subscription_required402This household's subscription has lapsed89 endpoints
insufficient_household_role403Insufficient household role62 endpoints
not_household_member403Not a member of this household94 endpoints
read_only_token403This API token only permits read requests63 endpoints
account_not_found404Account not found11 endpoints
category_group_not_found404Category group not foundCreate Category, Update Category, Update Category Group, Delete Category Group
category_not_found404Category not found11 endpoints
export_not_found404Export not foundGet Export
image_not_found404Image not found.Bulk Delete Transaction Images, Delete Transaction Image
merchant_not_found404Merchant not found15 endpoints
no_image404No image to deleteDelete User Image, Delete Household Image, Delete Merchant Image, Delete Account Image
not_found404Not FoundA path that no endpoint serves
recurring_transaction_not_found404Recurring transaction not foundGet Recurring Transaction Occurrences, Get Recurring Transaction, Update Recurring Transaction, Delete Recurring Transaction
rule_not_found404Rule not found5 endpoints
security_not_found404Security not foundGet Security Prices
suggestion_not_found404Dismissed suggestion not foundRestore Suggestion
tag_not_found404Tag not found13 endpoints
transaction_not_found404Transaction not found24 endpoints
method_not_allowed405Method Not AllowedAn endpoint, for a method it doesn't support
account_not_manual409Transactions can only be created for manually-added accounts.Create Transaction
child_cannot_split409Cannot split a transaction that is already a child of another split.Split Transaction
duplicate_emoji409Category with this emoji already existsCreate Category, Update Category
duplicate_name409This name is already in use9 endpoints
export_in_progress409An export is already being prepared for this householdRequest Export
transaction_deleted409Cannot split a deleted transaction.Split Transaction
transaction_not_split409Transaction is not currently split.List Splits, Unsplit Transaction
transaction_pending409Cannot split a pending transaction.Split Transaction
file_too_large413File exceeds maximum size of 5MB5 endpoints
unsupported_media_type415Unsupported Media Type38 endpoints
validation_error422Some fields are invalid.63 endpoints
internal_error500Internal server errorAny endpoint
service_unavailable503Service UnavailableAny endpoint that changes data

Last updated on

On this page