FAQs

Answers to the questions developers ask us most often. For full details of every endpoint, see the API Documentation and the Authentication and OAuth 2.0 guide.

Getting started

How do I apply for API access?

Please complete our API application form. You can use the API on Crunch Free, Pro or Premium. The form asks whether your app is confidential (it runs on a secure server) or public (it’s a desktop, mobile or browser app); see Do I need a client secret? By using the API, you accept the Developer Terms of Service.

Can I register for the API with the same email as my Crunch login?

Yes, that’s fine. Using the same email address for both is expected, so you don’t need to change anything.

Is there a separate developer login or portal?

There isn’t a separate login: the API uses your normal Crunch login, and logging in at accounts.crunch.co.uk shows your usual accounting dashboard. The API has no user interface: you call it from code or from an API client such as Postman. This Developer Portal is documentation only. If you were sent a password during API setup, you can ignore it.

Is there a sandbox or demo account for testing?

There isn’t a sandbox or demo account, but you can create a free Crunch account and test against that.

What the API can and can’t do

Which features does the public API support?

Everything the API supports is listed in the API Documentation, where each service has its own page. If an endpoint isn’t documented there, it isn’t available.

I get a 403 error on an endpoint I found. Can I be given access?

Unfortunately not. Endpoints that return a 403 error to public API clients, such as /banking/statement-entries, serve the Crunch app only and aren’t part of the public API, so no elevated scope, partner tier or allow-list grants access to them.

How do I request a feature?

Use the Feedback tab on the right-hand side of the Crunch app.

Authentication and tokens

Do I need a client secret?

It depends on the client type you chose when you registered.

Client type Typical use Client secret PKCE
Confidential Server-side or backend integrations Yes, sent in your onboarding email Optional
Public Desktop, CLI, mobile or browser-only apps No secret is issued Required

If you received a client_id but no secret, you’re most likely registered as a public client and must use PKCE.

How do I exchange the authorisation code for an access token?

Send a POST to https://oauth.crunch.co.uk/token with a form-encoded body (application/x-www-form-urlencoded), not URL query parameters. In Postman, either use the Body tab and select x-www-form-urlencoded, or use the Authorization tab with type OAuth 2.0 to run the whole flow and fetch the token for you.

Exchange the code straight away, as it’s short-lived. Confidential clients should authenticate with HTTP Basic auth (client_id as the username, client_secret as the password) and then must not also send them in the body. Public clients send client_id and code_verifier instead. If you sent redirect_uri to /authorize, send the identical value here.

# Confidential client (Basic auth)
curl -X POST https://oauth.crunch.co.uk/token \
  -u YOUR_CLIENT_ID:YOUR_CLIENT_SECRET \
  -d grant_type=authorization_code \
  -d code=AUTH_CODE \
  -d redirect_uri=http://127.0.0.1:8080/callback

# Public client (PKCE)
curl -X POST https://oauth.crunch.co.uk/token \
  -d grant_type=authorization_code \
  -d code=AUTH_CODE \
  -d redirect_uri=http://127.0.0.1:8080/callback \
  -d client_id=YOUR_CLIENT_ID \
  -d code_verifier=YOUR_PKCE_CODE_VERIFIER

Then send the access token as a Bearer token on every API call:

curl https://public-api.crunch.co.uk/v1/expenses \
  -H "Authorization: Bearer ACCESS_TOKEN"

How long do tokens last? Can I get a permanent API key?

There are no permanent API keys or non-expiring tokens. Access tokens currently last 1 hour, but read the exact lifetime from expires_in in the token response rather than hard-coding it. Request the offline_access scope to receive a refresh token, which lasts 60 days, and refresh when the access token expires; your integration can then run unattended.

# Confidential client
curl -X POST https://oauth.crunch.co.uk/token \
  -u YOUR_CLIENT_ID:YOUR_CLIENT_SECRET \
  -d grant_type=refresh_token \
  -d refresh_token=YOUR_LATEST_REFRESH_TOKEN

# Public client
curl -X POST https://oauth.crunch.co.uk/token \
  -d grant_type=refresh_token \
  -d refresh_token=YOUR_LATEST_REFRESH_TOKEN \
  -d client_id=YOUR_CLIENT_ID

Always save and use the newest refresh token. Applications created since 14 January 2026 have refresh token rotation switched on: each refresh returns a new refresh token and invalidates the old one. Reusing an old refresh token fails and revokes the whole grant, so the user has to authorise again.

Which scopes should I request?

Scopes are all with an optional CRUD prefix: create:all, read:all, update:all and delete:all, or plain all for everything. Add offline_access to get a refresh token, and separate scopes with spaces. Each endpoint’s required scope is listed under Security in the API Documentation; for example, creating an expense needs create:all and deleting one needs delete:all.

Can one application access more than one Crunch account (for example, a sole trader and a limited company)?

Yes, because your client_id belongs to your application, not to a Crunch account. Run the authorisation flow once per account, logging in with that account’s credentials, and store each account’s tokens separately.

Redirect URIs

Do I need to ask you to register my redirect URI?

No, you don’t need to. Pass the redirect URI you need in the redirect_uri parameter of your authorisation request, and send the same value in the token request.

Include it unless a redirect URI is already set on your API account; otherwise, leaving it out causes an invalid_redirect_uri error. If a redirect URI is set on your account, any value you send must match it. You can use different URIs for development and production, as long as no redirect URI is set on your account.

Are localhost or loopback redirect URIs allowed?

Yes, but only over http, not https. For example, http://localhost:3090/callback or http://127.0.0.1:8080/callback.

After I click Allow, the browser just spins. What’s wrong?

The browser is redirecting to your callback, but nothing is listening there. Make sure your local app is running on the redirect port before you start the flow. Authorisation codes are single-use, so clicking Allow a second time shows an error; restart the flow instead.

Making requests

An endpoint path in the API Documentation is blank or just /. What’s the full URL?

Paths are relative to each service’s base URL, shown under Access at the top of its documentation page. Add the path to the base URL. For example, Create Expense is listed as post / under the base URL https://public-api.crunch.co.uk/v1/expenses, so the full request is:

POST https://public-api.crunch.co.uk/v1/expenses

The same applies to Clients (/v1/clients), Invoices (/v1/invoices), Banking (/v1/banking) and the other services.

How do I paginate through long lists?

List endpoints accept page (starting at 0), size (default 20, maximum 200) and sort (field,asc or field,desc). Keep requesting the next page until the response has "last": true.

GET https://public-api.crunch.co.uk/v1/expenses?page=0&size=200&sort=date,desc

Is there a rate limit?

Yes, the limit is 100 requests per second.

Where do I find valid IDs and values for my payloads?

Look them up from the API rather than guessing; invalid values are the most common cause of validation errors.

You need Endpoint
Expense types (expenseType) GET /v1/expenses/expense-types
VAT types (vatType) GET /v1/expenses/vat-types
Payment methods (paymentMethodName) GET /v1/expenses/payment-methods
Director IDs for DIRECTORS_EXPENSES GET /v1/expenses/persons?date=YYYY-MM-DD
An existing supplier by name GET /v1/expenses/suppliers?supplierName={supplier_name}
Open accounting periods (closed years can’t be edited) GET /v1/expenses/ledgers
Bank account IDs GET /v1/banking/bank-accounts
Customer IDs for invoices GET /v1/clients/list
Country codes GET /v1/immutable-data/countries

Expenses, suppliers and attachments

How do I create a supplier?

Send a POST to /v1/expenses/suppliers. Only companyName is required; expenseType, country and vatRegistrationNumber are optional.

{
  "companyName": "Example Supplier Ltd",
  "expenseType": { "expenseTypeName": "TELEPHONE" }
}

How do I create an expense?

Send a POST to /v1/expenses. This needs the create:all scope. Use the id of an existing supplier, or create the supplier first.

{
  "supplier": { "id": SUPPLIER_ID },
  "postingDate": "2026-09-14",
  "expenseLineItems": [
    {
      "expenseType": "ACCOUNTANCY",
      "description": "Monthly bookkeeping",
      "vatType": "Standard",
      "amount": { "grossAmount": "120.00" }
    }
  ],
  "supplierPayments": [
    {
      "date": "2026-09-14",
      "amount": 120,
      "paymentTarget": {
        "paymentMethod": { "paymentMethodName": "EFT" },
        "bankAccount": { "id": BANK_ACCOUNT_ID }
      }
    }
  ]
}

vatType takes display names such as Standard, Exempt and Outside the Scope, not code-style values. For the DIRECTORS_EXPENSES payment method, add the director as person: { "id": PERSON_ID } inside paymentTarget.

How do I attach a receipt to an expense?

Include the file in the attachments array as a full data URI, with the MIME type prefix. Raw base64 without the prefix is accepted but saves an empty file.

"attachments": [
  {
    "id": 0,
    "fileName": "receipt.jpeg",
    "fileContents": "data:image/jpeg;base64,YOUR_BASE64_STRING"
  }
]

id numbers the attachments within the expense: 0, 1, 2 and so on.

Why is fileContents null when I fetch an expense?

This is by design, to keep responses small. Download the file separately:

GET /v1/expenses/attachments?expenseId={expense_id}&attachmentId={attachment_id}&fileName={file_name}

How do I delete an expense or remove its payment?

To delete the whole expense, send a DELETE to /v1/expenses/{expense_id}. To remove only the payment, send a PUT to the same endpoint with the supplierPayments section removed. You can’t delete a reconciled payment or an expense in a closed year.

Invoices

How do I create the customer I’m invoicing?

In the API, customers are called clients. Send a POST to /v1/clients, then use the returned id as customer.id on the invoice. To find existing customers, use GET /v1/clients/list.

{
  "name": "Example Customer Ltd",
  "email": "accounts@example.com",
  "paymentTermsDays": 30,
  "billingAddress": {
    "line1": "1 Example Street",
    "line2": "Example Town",
    "postCode": "AB1 2CD",
    "country": "GB"
  }
}

How do I create an invoice?

Send a POST to /v1/invoices. A 500 error usually means a required field is missing, so use this example body as a starting point.

{
  "customer": { "id": CUSTOMER_ID },
  "currency": "POUND_STERLING",
  "issuedDate": "2026-09-05",
  "lineItemType": "SIMPLE",
  "items": [
    {
      "description": "Consulting services",
      "priceExclVat": "100.00",
      "quantity": 1,
      "vatRate": "Outside the Scope",
      "lineType": "S"
    }
  ],
  "bankAccount": { "id": BANK_ACCOUNT_ID },
  "payment": { "paymentTerms": { "days": 28 } },
  "attachments": []
}

How do I record a customer payment against an invoice?

The invoice must be issued. Fetch it with GET /v1/invoices/{invoice_id}, add your new entry to the payments array, then send the whole invoice back with PUT /v1/invoices/{invoice_id}. This records the payment but doesn’t reconcile the matching bank entry.

Don’t send only the new payment. Any existing payment missing from payments is deleted, and any attachment missing from attachments is removed. Keep the existing entries exactly as the GET request returned them, and add your new payment without an id.

A new payment entry looks like this:

{
  "amount": "120.00",
  "date": "2026-09-30",
  "paymentTarget": {
    "paymentMethod": { "paymentMethodName": "EFT" },
    "bankAccount": { "id": BANK_ACCOUNT_ID }
  }
}

Can I create invoices in USD, AUD or other currencies?

Not yet. The API supports GBP only. Invoices in other currencies are planned, with no date set.

Can I create classic invoices?

Not through the API, which only works with new (Beta) invoices. You can still create classic invoices in the Crunch app.

Troubleshooting common errors

Symptom Likely cause Fix
404 on /token Parameters sent as URL query parameters Send them in a form-encoded body
401 invalid_client on /token Client secret missing, wrong or not matching your client type Confidential clients: send client_secret. Public clients: omit it and send code_verifier
invalid_redirect_uri redirect_uri missing from the authorisation request, and no redirect URI set on your API account Include redirect_uri
Browser spins after clicking Allow Nothing listening on your callback Start your local listener first, then restart the flow
404 on /v1/bank-accounts Wrong path Use /v1/banking/bank-accounts; banking endpoints sit under /v1/banking/
403 on a /banking/... endpoint Internal endpoint, not part of the public API Use only documented endpoints
500 when creating an invoice Missing required fields Compare your body with the example invoice
Attachment downloads as a zero-byte file fileContents sent without the data URI prefix Use data:<mime-type>;base64,...
404 on /v1/attachments Wrong path Use /v1/expenses/attachments