xero-kiwi 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.env.example +2 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +89 -0
- data/Rakefile +89 -0
- data/docs/accounting/address.md +54 -0
- data/docs/accounting/branding-theme.md +92 -0
- data/docs/accounting/contact-group.md +91 -0
- data/docs/accounting/contact.md +166 -0
- data/docs/accounting/credit-note.md +97 -0
- data/docs/accounting/external-link.md +33 -0
- data/docs/accounting/invoice.md +134 -0
- data/docs/accounting/organisation.md +119 -0
- data/docs/accounting/overpayment.md +94 -0
- data/docs/accounting/payment-terms.md +58 -0
- data/docs/accounting/payment.md +99 -0
- data/docs/accounting/phone.md +45 -0
- data/docs/accounting/prepayment.md +111 -0
- data/docs/accounting/user.md +109 -0
- data/docs/client.md +174 -0
- data/docs/connections.md +166 -0
- data/docs/errors.md +271 -0
- data/docs/getting-started.md +138 -0
- data/docs/oauth.md +508 -0
- data/docs/retries-and-rate-limits.md +224 -0
- data/docs/tokens.md +339 -0
- data/lib/xero_kiwi/accounting/address.rb +58 -0
- data/lib/xero_kiwi/accounting/allocation.rb +66 -0
- data/lib/xero_kiwi/accounting/branding_theme.rb +76 -0
- data/lib/xero_kiwi/accounting/contact.rb +153 -0
- data/lib/xero_kiwi/accounting/contact_group.rb +57 -0
- data/lib/xero_kiwi/accounting/contact_person.rb +45 -0
- data/lib/xero_kiwi/accounting/credit_note.rb +115 -0
- data/lib/xero_kiwi/accounting/external_link.rb +38 -0
- data/lib/xero_kiwi/accounting/invoice.rb +142 -0
- data/lib/xero_kiwi/accounting/line_item.rb +64 -0
- data/lib/xero_kiwi/accounting/organisation.rb +138 -0
- data/lib/xero_kiwi/accounting/overpayment.rb +107 -0
- data/lib/xero_kiwi/accounting/payment.rb +105 -0
- data/lib/xero_kiwi/accounting/payment_terms.rb +77 -0
- data/lib/xero_kiwi/accounting/phone.rb +46 -0
- data/lib/xero_kiwi/accounting/prepayment.rb +109 -0
- data/lib/xero_kiwi/accounting/tracking_category.rb +42 -0
- data/lib/xero_kiwi/accounting/user.rb +80 -0
- data/lib/xero_kiwi/client.rb +576 -0
- data/lib/xero_kiwi/connection.rb +78 -0
- data/lib/xero_kiwi/errors.rb +34 -0
- data/lib/xero_kiwi/identity.rb +40 -0
- data/lib/xero_kiwi/oauth/id_token.rb +102 -0
- data/lib/xero_kiwi/oauth/pkce.rb +51 -0
- data/lib/xero_kiwi/oauth.rb +232 -0
- data/lib/xero_kiwi/token.rb +99 -0
- data/lib/xero_kiwi/token_refresher.rb +53 -0
- data/lib/xero_kiwi/version.rb +5 -0
- data/lib/xero_kiwi.rb +33 -0
- data/llms-full.txt +3351 -0
- data/llms.txt +56 -0
- data/sig/xero_kiwi.rbs +4 -0
- metadata +164 -0
data/docs/errors.md
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# Errors
|
|
2
|
+
|
|
3
|
+
Every failure path in XeroKiwi raises a typed exception. This page walks through
|
|
4
|
+
the hierarchy, explains what each class means, and tells you what to catch
|
|
5
|
+
in common situations.
|
|
6
|
+
|
|
7
|
+
## The hierarchy
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
StandardError
|
|
11
|
+
└─ XeroKiwi::Error (root — catch this for "anything XeroKiwi raised")
|
|
12
|
+
├─ XeroKiwi::APIError (root for HTTP responses; carries status + body)
|
|
13
|
+
│ ├─ XeroKiwi::AuthenticationError (401)
|
|
14
|
+
│ │ ├─ XeroKiwi::TokenRefreshError (refresh round-trip failed)
|
|
15
|
+
│ │ └─ XeroKiwi::OAuth::CodeExchangeError (auth-code exchange failed)
|
|
16
|
+
│ ├─ XeroKiwi::ClientError (other 4xx)
|
|
17
|
+
│ ├─ XeroKiwi::ServerError (5xx)
|
|
18
|
+
│ └─ XeroKiwi::RateLimitError (429, with retry_after + problem)
|
|
19
|
+
├─ XeroKiwi::OAuth::StateMismatchError (CSRF check failed)
|
|
20
|
+
└─ XeroKiwi::OAuth::IDTokenError (ID token JWT verification failed)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
A few things worth noticing about the shape:
|
|
24
|
+
|
|
25
|
+
- **Everything XeroKiwi raises** descends from `XeroKiwi::Error`. If you only want
|
|
26
|
+
one rescue clause for "the Xero integration broke," catch this.
|
|
27
|
+
- **HTTP responses** descend from `XeroKiwi::APIError`, which carries `status`
|
|
28
|
+
and `body` attributes you can inspect.
|
|
29
|
+
- **OAuth-specific errors** for state mismatch and ID token verification
|
|
30
|
+
descend from `XeroKiwi::Error` directly (not `APIError`) because they don't
|
|
31
|
+
come from an HTTP response — they're local validation failures.
|
|
32
|
+
- **`XeroKiwi::TokenRefreshError`** and **`XeroKiwi::OAuth::CodeExchangeError`** are
|
|
33
|
+
both `AuthenticationError` subclasses, because the practical recovery is
|
|
34
|
+
the same as for any 401: the user needs to re-authorise.
|
|
35
|
+
|
|
36
|
+
## Class reference
|
|
37
|
+
|
|
38
|
+
### `XeroKiwi::Error`
|
|
39
|
+
|
|
40
|
+
The root class. Inherits from `StandardError`. You almost never raise this
|
|
41
|
+
directly — it exists as a catch-all for code that wants to rescue "any XeroKiwi
|
|
42
|
+
problem" without enumerating every subclass.
|
|
43
|
+
|
|
44
|
+
### `XeroKiwi::APIError`
|
|
45
|
+
|
|
46
|
+
The base class for HTTP errors. Constructor: `APIError.new(status, body, message = nil)`.
|
|
47
|
+
|
|
48
|
+
Attributes:
|
|
49
|
+
|
|
50
|
+
| Attribute | Type | What it is |
|
|
51
|
+
|-----------|------|------------|
|
|
52
|
+
| `error.status` | `Integer` | The HTTP status code (e.g. 401, 429, 500). |
|
|
53
|
+
| `error.body` | parsed body | The response body, parsed as JSON when the response was JSON, otherwise the raw string. |
|
|
54
|
+
| `error.message` | `String` | A descriptive message. Defaults to `"Xero API responded with #{status}: #{body.inspect}"`. |
|
|
55
|
+
|
|
56
|
+
### `XeroKiwi::AuthenticationError` (HTTP 401)
|
|
57
|
+
|
|
58
|
+
The access token was rejected. The most common causes:
|
|
59
|
+
|
|
60
|
+
- Token has expired and there's no refresh capability.
|
|
61
|
+
- Token was revoked.
|
|
62
|
+
- Token has the wrong scopes for this endpoint.
|
|
63
|
+
- The wrong tenant ID was passed in the `Xero-Tenant-Id` header.
|
|
64
|
+
|
|
65
|
+
If your client has refresh capability, XeroKiwi will already have tried to
|
|
66
|
+
refresh and retry exactly once before this raises. Seeing `AuthenticationError`
|
|
67
|
+
on a refresh-capable client means **the second 401 also failed** — refresh
|
|
68
|
+
won't fix it, and you need to either re-authorise or surface the error.
|
|
69
|
+
|
|
70
|
+
### `XeroKiwi::TokenRefreshError`
|
|
71
|
+
|
|
72
|
+
A subclass of `AuthenticationError`. Raised when the refresh round-trip
|
|
73
|
+
itself fails. Most common cause: **the refresh token has been rotated by
|
|
74
|
+
another process** (see [the rotation gotcha in the tokens
|
|
75
|
+
doc](tokens.md#refresh-token-rotation)).
|
|
76
|
+
|
|
77
|
+
This is different from "the user must re-auth" in subtle ways:
|
|
78
|
+
|
|
79
|
+
- **Refresh token rotated**: another process refreshed before you did.
|
|
80
|
+
Reload the credential from storage and retry.
|
|
81
|
+
- **Refresh token genuinely expired** (60 days unused): the user must
|
|
82
|
+
re-authorise.
|
|
83
|
+
|
|
84
|
+
You can't tell these apart from the exception alone — both look like
|
|
85
|
+
`invalid_grant` from Xero. The recovery pattern in
|
|
86
|
+
[Tokens — multi-process refresh](tokens.md#multi-process-refresh) handles
|
|
87
|
+
both cases.
|
|
88
|
+
|
|
89
|
+
### `XeroKiwi::OAuth::CodeExchangeError`
|
|
90
|
+
|
|
91
|
+
Also a subclass of `AuthenticationError`. Raised when `oauth.exchange_code`
|
|
92
|
+
fails:
|
|
93
|
+
|
|
94
|
+
- The auth code expired (Xero's codes are very short-lived).
|
|
95
|
+
- The code was already used (codes are single-use).
|
|
96
|
+
- The `redirect_uri` doesn't match what was used at authorise time.
|
|
97
|
+
- The PKCE verifier doesn't match the challenge sent at authorise time.
|
|
98
|
+
- The client credentials are wrong.
|
|
99
|
+
|
|
100
|
+
The user needs to restart the OAuth flow from the authorise step.
|
|
101
|
+
|
|
102
|
+
### `XeroKiwi::ClientError` (other HTTP 4xx)
|
|
103
|
+
|
|
104
|
+
A 4xx response that isn't 401 or 429. Most commonly:
|
|
105
|
+
|
|
106
|
+
| Status | Meaning |
|
|
107
|
+
|--------|---------|
|
|
108
|
+
| 400 | Bad request — usually a malformed body or query param |
|
|
109
|
+
| 403 | The token is valid but doesn't have the right scope/permission |
|
|
110
|
+
| 404 | The resource doesn't exist (or was already deleted) |
|
|
111
|
+
| 422 | Validation error — Xero rejected the payload |
|
|
112
|
+
|
|
113
|
+
The body will usually contain a Xero-specific error structure with details.
|
|
114
|
+
Inspect `error.body` to surface it.
|
|
115
|
+
|
|
116
|
+
### `XeroKiwi::ServerError` (HTTP 5xx)
|
|
117
|
+
|
|
118
|
+
A 5xx response that wasn't retried. XeroKiwi retries 502/503/504 automatically
|
|
119
|
+
(see [retries and rate limits](retries-and-rate-limits.md)) so by the time
|
|
120
|
+
you see this, the retries are exhausted or the status was 500 (which XeroKiwi
|
|
121
|
+
deliberately doesn't retry, since 500s are usually persistent bugs in the
|
|
122
|
+
request rather than transient infrastructure issues).
|
|
123
|
+
|
|
124
|
+
### `XeroKiwi::RateLimitError` (HTTP 429)
|
|
125
|
+
|
|
126
|
+
A subclass of `APIError`. Raised when retries on a 429 are exhausted.
|
|
127
|
+
|
|
128
|
+
Extra attributes beyond `APIError`:
|
|
129
|
+
|
|
130
|
+
| Attribute | Type | What it is |
|
|
131
|
+
|-----------|------|------------|
|
|
132
|
+
| `error.retry_after` | `Float` or `nil` | The value of the `Retry-After` header in seconds, if Xero sent one. |
|
|
133
|
+
| `error.problem` | `String` or `nil` | The value of the `X-Rate-Limit-Problem` header — typically `"minute"`, `"day"`, or `"appminute"`. Tells you which limit you hit. |
|
|
134
|
+
|
|
135
|
+
`retry_after` lets your application decide how to back off — for example, a
|
|
136
|
+
Sidekiq job can re-enqueue itself with that delay rather than hammering
|
|
137
|
+
Xero.
|
|
138
|
+
|
|
139
|
+
### `XeroKiwi::OAuth::StateMismatchError`
|
|
140
|
+
|
|
141
|
+
Raised by `XeroKiwi::OAuth.verify_state!` when the `state` parameter Xero
|
|
142
|
+
echoed back doesn't match what you stashed before redirecting. **This
|
|
143
|
+
indicates a forged callback or a session that was lost between request and
|
|
144
|
+
response.** Treat it as a security event — log it and refuse to proceed.
|
|
145
|
+
|
|
146
|
+
This is *not* a Xero error; it's a local CSRF check that fires before any
|
|
147
|
+
HTTP call.
|
|
148
|
+
|
|
149
|
+
### `XeroKiwi::OAuth::IDTokenError`
|
|
150
|
+
|
|
151
|
+
Raised by `XeroKiwi::OAuth::IDToken.verify` (or `oauth.verify_id_token`) when
|
|
152
|
+
the ID token JWT can't be verified. Causes:
|
|
153
|
+
|
|
154
|
+
- Bad signature.
|
|
155
|
+
- Wrong issuer.
|
|
156
|
+
- Wrong audience.
|
|
157
|
+
- Token expired.
|
|
158
|
+
- Nonce mismatch (when nonce verification was requested).
|
|
159
|
+
- Network failure fetching JWKS.
|
|
160
|
+
|
|
161
|
+
The error message has a brief description of which check failed (e.g.
|
|
162
|
+
`"ID token verification failed: Signature verification failed"`,
|
|
163
|
+
`"ID token nonce mismatch"`).
|
|
164
|
+
|
|
165
|
+
## What to catch when
|
|
166
|
+
|
|
167
|
+
### "Any XeroKiwi failure"
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
begin
|
|
171
|
+
client.connections
|
|
172
|
+
rescue XeroKiwi::Error => e
|
|
173
|
+
Rails.logger.error("Xero call failed: #{e.message}")
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### "Authentication broke; user needs to re-auth"
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
begin
|
|
181
|
+
client.connections
|
|
182
|
+
rescue XeroKiwi::AuthenticationError
|
|
183
|
+
redirect_to xero_reauth_path
|
|
184
|
+
end
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
This catches `TokenRefreshError` and `CodeExchangeError` too, since they're
|
|
188
|
+
both `AuthenticationError` subclasses. That's usually what you want — they
|
|
189
|
+
all imply "the credentials are dead, restart the flow."
|
|
190
|
+
|
|
191
|
+
### "Rate limited — back off"
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
begin
|
|
195
|
+
client.connections
|
|
196
|
+
rescue XeroKiwi::RateLimitError => e
|
|
197
|
+
RetryWorker.perform_in(e.retry_after.seconds, args)
|
|
198
|
+
end
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### "Anything Xero said no to"
|
|
202
|
+
|
|
203
|
+
```ruby
|
|
204
|
+
begin
|
|
205
|
+
client.connections
|
|
206
|
+
rescue XeroKiwi::APIError => e
|
|
207
|
+
Rails.logger.warn("Xero #{e.status}: #{e.body}")
|
|
208
|
+
raise
|
|
209
|
+
end
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### "OAuth callback failed for any reason"
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
def callback
|
|
216
|
+
XeroKiwi::OAuth.verify_state!(received: params[:state], expected: session.delete(:xero_state))
|
|
217
|
+
token = oauth.exchange_code(code: params[:code], code_verifier: session.delete(:xero_verifier))
|
|
218
|
+
oauth.verify_id_token(token.id_token)
|
|
219
|
+
# ...
|
|
220
|
+
rescue XeroKiwi::OAuth::StateMismatchError
|
|
221
|
+
redirect_to root_path, alert: "Authentication failed (CSRF check)"
|
|
222
|
+
rescue XeroKiwi::OAuth::CodeExchangeError
|
|
223
|
+
redirect_to root_path, alert: "Could not complete Xero authorisation"
|
|
224
|
+
rescue XeroKiwi::OAuth::IDTokenError
|
|
225
|
+
redirect_to root_path, alert: "Could not verify Xero identity"
|
|
226
|
+
end
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Three separate rescue clauses give you three different user-facing
|
|
230
|
+
messages, which is usually what you want for an OAuth callback — different
|
|
231
|
+
failures imply different user actions.
|
|
232
|
+
|
|
233
|
+
### Distinguishing TokenRefreshError from generic 401
|
|
234
|
+
|
|
235
|
+
If you want to handle the rotation race specifically:
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
begin
|
|
239
|
+
client.connections
|
|
240
|
+
rescue XeroKiwi::TokenRefreshError
|
|
241
|
+
credential.reload
|
|
242
|
+
if credential.refresh_token != original_refresh_token
|
|
243
|
+
retry # another process refreshed; pick up their token
|
|
244
|
+
else
|
|
245
|
+
credential.update!(needs_reauth: true)
|
|
246
|
+
raise
|
|
247
|
+
end
|
|
248
|
+
rescue XeroKiwi::AuthenticationError
|
|
249
|
+
# 401 that wasn't a refresh failure (e.g. token revoked at Xero's end)
|
|
250
|
+
credential.update!(needs_reauth: true)
|
|
251
|
+
end
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Order matters — `TokenRefreshError` is more specific than
|
|
255
|
+
`AuthenticationError`, so it must come first.
|
|
256
|
+
|
|
257
|
+
## Things the error system deliberately does NOT do
|
|
258
|
+
|
|
259
|
+
- **No "this error is retryable" predicate.** XeroKiwi already retries the
|
|
260
|
+
cases that *should* be retried at the HTTP level. By the time an exception
|
|
261
|
+
reaches your code, the retries are exhausted and the situation is
|
|
262
|
+
application-level. Adding `error.retryable?` would create a tempting
|
|
263
|
+
foot-gun where callers retry inside their own code, doubling up on the
|
|
264
|
+
retries XeroKiwi is already doing.
|
|
265
|
+
- **No automatic Sentry / Bugsnag integration.** Errors raise normally;
|
|
266
|
+
configure your own observability layer to catch them at the boundary.
|
|
267
|
+
- **No `error.code` enum.** The HTTP `status` is the enum. Inspecting it
|
|
268
|
+
in user code is usually fine (`error.status == 404`).
|
|
269
|
+
- **No localised error messages.** Messages are English and developer-facing.
|
|
270
|
+
Translate them to user-facing language at your application's surface,
|
|
271
|
+
not in the gem.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
This guide walks you from a fresh checkout to your first successful Xero API
|
|
4
|
+
call. If you already know what OAuth2 is and just want the API reference, jump
|
|
5
|
+
straight to [Client](client.md) or [OAuth](oauth.md).
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
Before you can talk to Xero from Ruby, you need:
|
|
10
|
+
|
|
11
|
+
1. **A Xero developer account.** Sign up at
|
|
12
|
+
<https://developer.xero.com>.
|
|
13
|
+
2. **A Xero app**, registered in the developer portal. The app gives you a
|
|
14
|
+
`client_id` and `client_secret` and lets you configure a redirect URL.
|
|
15
|
+
3. **Ruby 3.4.1 or newer.**
|
|
16
|
+
|
|
17
|
+
## Installing the gem
|
|
18
|
+
|
|
19
|
+
Add XeroKiwi to your `Gemfile`:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
gem "xero-kiwi"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
bundle install
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## The mental model
|
|
32
|
+
|
|
33
|
+
XeroKiwi is built around a small set of objects, each with one job:
|
|
34
|
+
|
|
35
|
+
| Object | What it does |
|
|
36
|
+
|--------|--------------|
|
|
37
|
+
| [`XeroKiwi::OAuth`](oauth.md) | Drives the OAuth2 authorization-code flow. Builds authorise URLs, exchanges codes for tokens, verifies ID tokens, revokes tokens. |
|
|
38
|
+
| [`XeroKiwi::Token`](tokens.md) | An immutable value object holding an access/refresh pair plus expiry metadata. Knows when it's expired or expiring. |
|
|
39
|
+
| [`XeroKiwi::Client`](client.md) | The API gateway. You give it a token (or full credentials) and call methods like `client.connections`. Handles retries, refresh, error mapping. |
|
|
40
|
+
| [`XeroKiwi::Connection`](connections.md) | A Xero "connection" — one tenant (organisation or practice) that an access token is authorised against. |
|
|
41
|
+
| [`XeroKiwi::Accounting::Organisation`](accounting/organisation.md) | A Xero organisation — the accounting entity (company, trust, etc.) behind a tenant. |
|
|
42
|
+
|
|
43
|
+
The flow is always: **OAuth → Token → Client → resources.** OAuth gets you a
|
|
44
|
+
Token; you hand the Token to a Client; the Client lets you call resource
|
|
45
|
+
methods.
|
|
46
|
+
|
|
47
|
+
## Your first request — the short version
|
|
48
|
+
|
|
49
|
+
If you already have an access token (e.g. from a previous OAuth dance, or from
|
|
50
|
+
the Xero developer portal's "Try it" tooling), making a request is one line:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
require "xero_kiwi"
|
|
54
|
+
|
|
55
|
+
client = XeroKiwi::Client.new(access_token: "ya29...")
|
|
56
|
+
client.connections # => [#<XeroKiwi::Connection ...>, ...]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This is enough to get started, but the access token will expire after 30
|
|
60
|
+
minutes and you have no way to refresh it. For anything beyond a quick script,
|
|
61
|
+
read on.
|
|
62
|
+
|
|
63
|
+
## Your first request — the full version
|
|
64
|
+
|
|
65
|
+
A production-grade integration looks more like this:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
require "xero_kiwi"
|
|
69
|
+
|
|
70
|
+
# 1. Set up the OAuth helper. You only need redirect_uri for the auth-code
|
|
71
|
+
# flow itself; for refresh-only / revoke-only callers it's optional.
|
|
72
|
+
oauth = XeroKiwi::OAuth.new(
|
|
73
|
+
client_id: ENV.fetch("XERO_CLIENT_ID"),
|
|
74
|
+
client_secret: ENV.fetch("XERO_CLIENT_SECRET"),
|
|
75
|
+
redirect_uri: "https://app.example.com/xero/callback"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# 2. Send the user to Xero. Stash the state + PKCE verifier in your session
|
|
79
|
+
# so you can verify them when the user comes back.
|
|
80
|
+
state = XeroKiwi::OAuth.generate_state
|
|
81
|
+
pkce = XeroKiwi::OAuth.generate_pkce
|
|
82
|
+
session[:xero_state] = state
|
|
83
|
+
session[:xero_verifier] = pkce.verifier
|
|
84
|
+
|
|
85
|
+
redirect_to oauth.authorization_url(
|
|
86
|
+
scopes: %w[openid profile email accounting.transactions offline_access],
|
|
87
|
+
state: state,
|
|
88
|
+
pkce: pkce
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# 3. In your callback handler, verify state, exchange the code, persist the
|
|
92
|
+
# tokens, and you're ready to call the API.
|
|
93
|
+
XeroKiwi::OAuth.verify_state!(
|
|
94
|
+
received: params[:state],
|
|
95
|
+
expected: session.delete(:xero_state)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
token = oauth.exchange_code(
|
|
99
|
+
code: params[:code],
|
|
100
|
+
code_verifier: session.delete(:xero_verifier)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
XeroCredential.create!(
|
|
104
|
+
access_token: token.access_token,
|
|
105
|
+
refresh_token: token.refresh_token,
|
|
106
|
+
expires_at: token.expires_at
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# 4. Now build a refresh-capable client and use it. The client will refresh
|
|
110
|
+
# automatically when the token expires; the on_token_refresh callback
|
|
111
|
+
# persists the rotated token back to your storage.
|
|
112
|
+
credential = XeroCredential.last
|
|
113
|
+
|
|
114
|
+
client = XeroKiwi::Client.new(
|
|
115
|
+
access_token: credential.access_token,
|
|
116
|
+
refresh_token: credential.refresh_token,
|
|
117
|
+
expires_at: credential.expires_at,
|
|
118
|
+
client_id: ENV.fetch("XERO_CLIENT_ID"),
|
|
119
|
+
client_secret: ENV.fetch("XERO_CLIENT_SECRET"),
|
|
120
|
+
on_token_refresh: ->(token) { credential.update!(token.to_h) }
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
client.connections.each do |connection|
|
|
124
|
+
puts "#{connection.tenant_name} (#{connection.tenant_id})"
|
|
125
|
+
end
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
That's the whole loop: authorise, exchange, persist, call the API.
|
|
129
|
+
|
|
130
|
+
## Where to go next
|
|
131
|
+
|
|
132
|
+
- [OAuth](oauth.md) — the full reference for the auth-code flow, PKCE, ID token
|
|
133
|
+
verification, and revocation.
|
|
134
|
+
- [Client](client.md) — every constructor option for `XeroKiwi::Client`, the request
|
|
135
|
+
lifecycle, custom adapters and retry policy.
|
|
136
|
+
- [Tokens](tokens.md) — how refresh works, the persistence callback, manual vs
|
|
137
|
+
automatic refresh, multi-process gotchas.
|
|
138
|
+
- [Errors](errors.md) — the full error hierarchy and what to rescue when.
|