Files
fiken-api/README.md
T
2026-06-23 14:52:08 +02:00

232 lines
7.2 KiB
Markdown

# Fiken
A resource-oriented Ruby client for the [Fiken API v2](https://api.fiken.no/api/v2/docs) —
the Norwegian accounting and invoicing platform.
- Personal API token **and** OAuth2 (authorization-code) authentication
- Resource-oriented surface that mirrors the API (`client.invoices(slug).drafts.create(...)`)
- Automatic pagination with `auto_paging_each`
- HTTP errors mapped to typed exceptions, with retry on rate limits (429) and server errors
> **API reference.** This gem is a thin transport over the Fiken API — it does not
> redefine the data model. Request bodies and response fields use exactly the names,
> types and nesting from the official OpenAPI (Swagger) specification. Keep it open
> while you build:
>
> - Interactive docs: <https://api.fiken.no/api/v2/docs>
> - OpenAPI spec (YAML): <https://api.fiken.no/api/v2/docs/swagger.yaml>
>
> Each resource below maps to a tag in that spec, and every `Fiken::Object` you get
> back mirrors the corresponding response schema. See
> [Response objects & data structures](#response-objects--data-structures).
## Installation
```ruby
# Gemfile
gem "fiken"
```
```sh
bundle install
```
## Authentication
### Personal API token
Generate a token in Fiken (under your account settings) and pass it in:
```ruby
client = Fiken::Client.new(token: ENV["FIKEN_TOKEN"])
client.user # => #<Fiken::Object name="..." email="...">
client.companies.list # => #<Fiken::Collection ...>
```
### OAuth2 (acting on behalf of other Fiken users)
```ruby
oauth = Fiken::OAuth.new(
client_id: ENV["FIKEN_CLIENT_ID"],
client_secret: ENV["FIKEN_CLIENT_SECRET"],
redirect_uri: "https://example.com/oauth/callback"
)
# 1. Send the user to authorize
redirect_to oauth.authorize_url(state: "csrf-token")
# 2. In your callback, exchange the code for tokens
token = oauth.exchange_code(params[:code])
client = Fiken::Client.new(access_token: token.access_token)
# 3. Later, refresh
token = oauth.refresh(token.refresh_token)
```
## Usage
Almost every resource is scoped to a company, identified by its **slug**:
```ruby
slug = client.companies.list.first.slug
```
### Reading
```ruby
client.contacts(slug).list(page: 0, pageSize: 50)
client.contacts(slug).find(123)
client.invoices(slug).find(456)
client.accounts(slug).find("1500:10001")
```
### Pagination
A list returns a `Fiken::Collection` that exposes the page metadata and can walk
every page lazily:
```ruby
contacts = client.contacts(slug).list
contacts.result_count # total across all pages
contacts.auto_paging_each do |contact|
puts contact.name
end
```
### Creating and updating
```ruby
# Create returns the new resource's id (parsed from the Location header)
created = client.contacts(slug).create(name: "Acme AS", organizationNumber: "123456789")
created.id
# Contacts/drafts update via PUT; finalized invoices/projects/etc. via PATCH
client.contacts(slug).update(123, email: "post@acme.no")
client.invoices(slug).update(456, sentManually: true)
client.contacts(slug).delete(123)
```
### Invoices, credit notes, offers, order confirmations
These share a draft → finalize lifecycle, plus counters and (where supported) sending:
```ruby
invoices = client.invoices(slug)
draft = invoices.drafts.create(
type: "invoice",
customerId: 123,
lines: [{ description: "Consulting", unitPrice: 100_000, vatType: "HIGH", quantity: 1 }]
)
invoice = invoices.drafts.create_document(draft.id) # finalize the draft
invoices.dispatch(invoiceId: invoice.id, method: ["email"], includeDocumentAttachments: true)
invoices.counter # current invoice number
invoices.create_counter(value: 1000) # initialize it
client.credit_notes(slug).create_full(invoiceId: invoice.id)
```
### Sales, purchases and payments
```ruby
sales = client.sales(slug)
sales.create(saleNumber: "1", date: "2026-01-01", kind: "external_invoice", lines: [...])
sales.payments(5).create(date: "2026-01-05", account: "1920:10001", amount: 125_000)
sales.settle(5, settledDate: "2026-01-10")
sales.write_off(5)
sales.delete(5, description: "duplicate")
```
### Attachments and the inbox (file uploads)
```ruby
client.invoices(slug).attachments(456).add(path: "invoice.pdf")
client.invoices(slug).attachments(456).add(io: pdf_io, filename: "invoice.pdf",
content_type: "application/pdf")
client.inbox(slug).create(path: "receipt.pdf", name: "Office supplies")
```
## Response objects & data structures
Responses are wrapped in `Fiken::Object`, a lightweight, read-only struct built
recursively from the JSON. The gem deliberately does **not** impose its own typed
models — the shape of every object is defined by the Fiken OpenAPI spec, so that is
your source of truth for field names and types:
- OpenAPI spec (YAML): <https://api.fiken.no/api/v2/docs/swagger.yaml>
- Interactive docs: <https://api.fiken.no/api/v2/docs>
For example, an invoice returned by `client.invoices(slug).find(id)` mirrors the
spec's `invoiceResult` schema, so the keys documented there are exactly what you
access:
```ruby
invoice = client.invoices(slug).find(456)
invoice.invoiceNumber # camelCase — exactly as in the spec
invoice.invoice_number # snake_case alias for the same field
invoice[:invoiceNumber] # hash-style access
invoice.lines.first.vatType # nested objects/arrays wrapped recursively
invoice.to_h # plain Hash with the original (camelCase) keys
invoice.keys # inspect available fields at runtime
```
Because access maps straight onto the spec schema, the schema name to look up for a
given call is predictable: `*Result` schemas for responses (e.g. `invoiceResult`,
`saleResult`, `contact`) and `*Request` schemas for the hashes you pass to
`create`/`update` (e.g. `invoiceRequest`, `fullCreditNoteRequest`). When in doubt,
`obj.to_h` shows you the raw structure as Fiken returned it.
## Error handling
Non-success responses raise a typed subclass of `Fiken::Error`, each carrying
`#status` and `#body`:
```ruby
begin
client.invoices(slug).find(999_999)
rescue Fiken::NotFound => e
e.status # => 404
rescue Fiken::RateLimited
# retried automatically first; raised only if retries are exhausted
rescue Fiken::Error => e
warn e.message
end
```
`BadRequest` (400), `Unauthorized` (401), `Forbidden` (403), `NotFound` (404),
`UnprocessableEntity` (422), `RateLimited` (429) and `ServerError` (5xx) are all
provided.
## Available resources
`user`, `companies`, `accounts`, `account_balances`, `bank_accounts`, `bank_balances`,
`contacts` (+ `contact_persons`), `groups`, `products`, `journal_entries`, `transactions`,
`invoices`, `credit_notes`, `offers`, `order_confirmations`, `sales`, `purchases`, `inbox`,
`projects`, `activities`, `time_entries`, `time_users`.
## Configuration
```ruby
Fiken::Client.new(
token: ENV["FIKEN_TOKEN"],
base_url: "https://api.fiken.no/api/v2", # default
user_agent: "my-app/1.0",
retry_options: { max: 3 }, # passed to faraday-retry
logger: Logger.new($stdout)
)
```
## Development
```sh
bin/setup # or: bundle install
bundle exec rspec
bundle exec rubocop
```
## License
The gem is available as open source under the terms of the [MIT License](LICENSE.txt).