flashpi 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/CHANGELOG.md +25 -0
- data/LICENSE +21 -0
- data/README.en.md +379 -0
- data/README.md +383 -0
- data/lib/flashpi/api_response.rb +33 -0
- data/lib/flashpi/client.rb +112 -0
- data/lib/flashpi/errors.rb +116 -0
- data/lib/flashpi/internal/engine.rb +355 -0
- data/lib/flashpi/internal/json_text.rb +108 -0
- data/lib/flashpi/internal/multipart.rb +141 -0
- data/lib/flashpi/internal/problems.rb +111 -0
- data/lib/flashpi/internal/request.rb +231 -0
- data/lib/flashpi/internal/resource.rb +125 -0
- data/lib/flashpi/internal/secret.rb +41 -0
- data/lib/flashpi/internal/settings.rb +88 -0
- data/lib/flashpi/internal/transport.rb +432 -0
- data/lib/flashpi/internal/validate.rb +122 -0
- data/lib/flashpi/page.rb +82 -0
- data/lib/flashpi/resources/documents.rb +136 -0
- data/lib/flashpi/resources/events.rb +36 -0
- data/lib/flashpi/resources/issuer_onboardings.rb +47 -0
- data/lib/flashpi/resources/issuers.rb +179 -0
- data/lib/flashpi/resources/webhook_endpoints.rb +110 -0
- data/lib/flashpi/version.rb +6 -0
- data/lib/flashpi/webhooks.rb +223 -0
- data/lib/flashpi.rb +42 -0
- data/sig/flashpi/internal.rbs +376 -0
- data/sig/flashpi/resources.rbs +712 -0
- data/sig/flashpi/sdk_types.rbs +114 -0
- data/sig/flashpi/types.rbs +2495 -0
- data/sig/flashpi.rbs +187 -0
- data/sig/manifest.yaml +8 -0
- metadata +80 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e99de7bd1116dba76f96d4777acde01faab1f23d53741d93796d7f761111920c
|
|
4
|
+
data.tar.gz: b3048538f68a053c888dccf4bbf87cfb6537da48e3442311451e68797405cb6f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ae6dfcd60c93b516e82b96731446151f0c102c6624018eae17fae6998dfbf114201117a509c89429e9c21434b6da09f3715328328c1864b1499f3f28b0878bd8
|
|
7
|
+
data.tar.gz: 10263d7cc9844ed4cebcf7a90b86a1c9df748d840286adcd7cb7d7069232ff2c76d004b5de776918a4997ca0f3ee7c9c5b3cf9094344f96336d2fa15eb0e9e15
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-11-14)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add the client and every API operation ([c267d7c](https://github.com/flashpi1/flashpi-ruby/commit/c267d7c83cadbc1a3c5c2f9696db58dee6c7b5fd))
|
|
9
|
+
* add the errors ([542b470](https://github.com/flashpi1/flashpi-ruby/commit/542b4703900d3e5ba426a49c2456d63ac9801c7e))
|
|
10
|
+
* add the request engine and its net/http transport ([cb83b6f](https://github.com/flashpi1/flashpi-ruby/commit/cb83b6f04e4e08197c59bedae824b8b385f624a5))
|
|
11
|
+
* generate RBS types from the OpenAPI contract ([e590c97](https://github.com/flashpi1/flashpi-ruby/commit/e590c97a6c39af50a1cc7495ed0268d6570a8e3f))
|
|
12
|
+
* type the gem with RBS signatures checked by Steep ([7b73fab](https://github.com/flashpi1/flashpi-ruby/commit/7b73fab55389e6dc438ae8c04da87714da883d5d))
|
|
13
|
+
* verify the webhooks Flashpi sends ([338c0fb](https://github.com/flashpi1/flashpi-ruby/commit/338c0fbf9885c45731df9a51ea2c05e242ceffc7))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug fixes
|
|
17
|
+
|
|
18
|
+
* check ids, filters and certificate fields as UTF-8 text ([8ec57d7](https://github.com/flashpi1/flashpi-ruby/commit/8ec57d7a3702b379544b47b0eaa7da87d5c5775e))
|
|
19
|
+
* **ci:** name the owning account when the release exchanges its token ([c36e333](https://github.com/flashpi1/flashpi-ruby/commit/c36e333ff67cf080f71b90e3cfa8e09ed0bfb9f0))
|
|
20
|
+
* end every bad webhook in a WebhookVerificationError ([3ef3ba7](https://github.com/flashpi1/flashpi-ruby/commit/3ef3ba7997f8bbcff10c07884bf57daf039f95a6))
|
|
21
|
+
* harden the transport's proxies, truncated answers and kept-alive connections ([387a725](https://github.com/flashpi1/flashpi-ruby/commit/387a725c47ce17e90148b06e2e7e1999ac8f7ec8))
|
|
22
|
+
* keep a failed upload's .p12 and password out of the engine's locals ([1dc5853](https://github.com/flashpi1/flashpi-ruby/commit/1dc5853993a60100c4e524ff7fe2abb7e8c1fdaa))
|
|
23
|
+
* let a StopIteration of the caller's block through Page#each ([4910a1c](https://github.com/flashpi1/flashpi-ruby/commit/4910a1c8a7bd7f7fb5eb902af3a2c392b6355866))
|
|
24
|
+
* **scripts:** type path-level parameters and refuse serialization settings ([4e6a5e6](https://github.com/flashpi1/flashpi-ruby/commit/4e6a5e6ff587c9850b6287eca136d9f3a11adc75))
|
|
25
|
+
* send a POST without a body with no Content-Type on Ruby 3.3 and 3.4 ([d3f6888](https://github.com/flashpi1/flashpi-ruby/commit/d3f6888a6a2ae56a324616384304c604090a22b6))
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SIGNIADIGITAL S.A.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.en.md
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
# Flashpi for Ruby
|
|
2
|
+
|
|
3
|
+
The official Ruby SDK for [Flashpi](https://flashpi.com.ec), the electronic invoicing API for
|
|
4
|
+
Ecuador's SRI.
|
|
5
|
+
|
|
6
|
+
[Español](https://github.com/flashpi1/flashpi-ruby/blob/main/README.md)
|
|
7
|
+
|
|
8
|
+
- The 45 operations of the API: the six document types, documents (XML, RIDE PDF, email),
|
|
9
|
+
issuers with establishments, emission points and .p12 certificates, webhooks and events.
|
|
10
|
+
- One client (`Flashpi::Client`) you can share between threads, which reuses its connections.
|
|
11
|
+
- RBS signatures generated from the API's OpenAPI document, so Steep and the editors that read
|
|
12
|
+
RBS know every field.
|
|
13
|
+
- Safe retries: every issuing request carries an `Idempotency-Key`, so a retry never issues a
|
|
14
|
+
document twice.
|
|
15
|
+
- Typed errors (RFC 9457) with their `code`, field errors and `request_id`.
|
|
16
|
+
- Automatic pagination: every list is `Enumerable` and fetches the pages as you go.
|
|
17
|
+
- Webhook signature verification (Standard Webhooks) for Rails, Sinatra or any Rack app.
|
|
18
|
+
- Ruby 3.3 or later. No dependencies: the standard library only (net/http).
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
bundle add flashpi
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or without Bundler: `gem install flashpi`.
|
|
27
|
+
|
|
28
|
+
## Getting started
|
|
29
|
+
|
|
30
|
+
Create an API key in the [console](https://flashpi.com.ec/app/) and keep it in the
|
|
31
|
+
`FLASHPI_API_KEY` environment variable. An `fp_test_` key issues in the SRI's test environment;
|
|
32
|
+
an `fp_live_` key issues real documents in production.
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
require "flashpi"
|
|
36
|
+
|
|
37
|
+
# Reads FLASHPI_API_KEY; you can also pass it: Flashpi::Client.new(api_key: "fp_test_...")
|
|
38
|
+
flashpi = Flashpi::Client.new
|
|
39
|
+
|
|
40
|
+
invoice = flashpi.invoices.create(
|
|
41
|
+
{
|
|
42
|
+
issuerId: "<the demo issuer id or yours>",
|
|
43
|
+
establishment: "001",
|
|
44
|
+
emissionPoint: "001",
|
|
45
|
+
customer: {
|
|
46
|
+
idType: "ruc",
|
|
47
|
+
id: "1760013210001",
|
|
48
|
+
name: "PRUEBAS SERVICIO DE RENTAS INTERNAS",
|
|
49
|
+
address: "Av. Amazonas y Villalengua, Quito"
|
|
50
|
+
},
|
|
51
|
+
items: [
|
|
52
|
+
{
|
|
53
|
+
code: "P-001",
|
|
54
|
+
description: "Test service",
|
|
55
|
+
quantity: "1",
|
|
56
|
+
unitPrice: "1.00",
|
|
57
|
+
taxes: [{ type: "vat", rate: "15" }]
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
payments: [{ method: "cash", amount: "1.15" }],
|
|
61
|
+
options: { waitForAuthorization: true }
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
puts invoice[:status], invoice[:accessKey]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Requests and answers are hashes with the API's own keys, in camelCase, as symbols
|
|
69
|
+
(`invoice[:accessKey]`), typed in `Flashpi::Types` (`Flashpi::Types::create_invoice`,
|
|
70
|
+
`Flashpi::Types::document`). Amounts and quantities are **strings** (`"1.15"`), never numbers, so
|
|
71
|
+
no decimals are lost. Write the body in braces, `create({ ... })`: without them, Ruby would take
|
|
72
|
+
it as the options of the call.
|
|
73
|
+
|
|
74
|
+
With the **demo issuer** you can issue test documents without uploading a certificate: its id is
|
|
75
|
+
on the console's Overview (up to 50 documents a day). See
|
|
76
|
+
[Sandbox and demo issuer](https://flashpi.com.ec/docs/en/sandbox/).
|
|
77
|
+
|
|
78
|
+
A client keeps its connections open for the next requests and can be shared between threads:
|
|
79
|
+
create one and reuse it (in Rails, for example, in an initializer). `flashpi.close` closes those
|
|
80
|
+
connections.
|
|
81
|
+
|
|
82
|
+
## Issuing documents
|
|
83
|
+
|
|
84
|
+
| Document | Method |
|
|
85
|
+
| ------------------------------------------- | ------------------------------------------- |
|
|
86
|
+
| Invoice (factura) | `flashpi.invoices.create(body)` |
|
|
87
|
+
| Credit note (nota de crédito) | `flashpi.credit_notes.create(body)` |
|
|
88
|
+
| Debit note (nota de débito) | `flashpi.debit_notes.create(body)` |
|
|
89
|
+
| Purchase settlement (liquidación de compra) | `flashpi.purchase_settlements.create(body)` |
|
|
90
|
+
| Waybill (guía de remisión) | `flashpi.waybills.create(body)` |
|
|
91
|
+
| Withholding (comprobante de retención) | `flashpi.withholdings.create(body)` |
|
|
92
|
+
|
|
93
|
+
They all return the document. By default the answer comes right away with the status
|
|
94
|
+
`processing`, and the SRI's outcome arrives by webhook. With
|
|
95
|
+
`options: { waitForAuthorization: true }` the API waits for the SRI (15 s by default, up to 30 s
|
|
96
|
+
with `waitTimeoutSeconds`) and returns `authorized`, `rejected` or `failed`; if the SRI takes
|
|
97
|
+
longer, it returns `processing` or `pending_sri` and the document carries on.
|
|
98
|
+
|
|
99
|
+
## Documents
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
document = flashpi.documents.get(invoice[:id])
|
|
103
|
+
puts document[:status]
|
|
104
|
+
|
|
105
|
+
xml = flashpi.documents.xml(invoice[:id]) # xml[:kind]: "authorized" once the SRI authorized it
|
|
106
|
+
pdf = flashpi.documents.pdf(invoice[:id]) # the RIDE, as bytes
|
|
107
|
+
File.binwrite("#{invoice[:accessKey]}.pdf", pdf)
|
|
108
|
+
puts xml[:xml] if xml[:kind] == "authorized"
|
|
109
|
+
|
|
110
|
+
flashpi.documents.send_email(invoice[:id]) # emails the RIDE and the XML to the buyer again
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Lists and pagination
|
|
114
|
+
|
|
115
|
+
A list call returns a page that is `Enumerable`: `each` walks every page, fetching them as you
|
|
116
|
+
go, and `first(10)` fetches only the pages it needs.
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
flashpi.documents.list({ status: "authorized", issueDateFrom: "2026-11-01" }).each do |document|
|
|
120
|
+
puts document[:accessKey]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
page = flashpi.documents.list({ limit: 50 })
|
|
124
|
+
puts page.data.size # the documents of this page
|
|
125
|
+
page = page.next_page if page.has_next_page?
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The filters use the API's names (`issueDateFrom`, `issuerId`) and go in a hash in braces, before
|
|
129
|
+
the options of the call.
|
|
130
|
+
|
|
131
|
+
## Errors
|
|
132
|
+
|
|
133
|
+
Every API error is a subclass of `Flashpi::APIError` by HTTP status. Branch on `code`, which is
|
|
134
|
+
stable and in English; `message` follows the requested language.
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
begin
|
|
138
|
+
flashpi.invoices.create(body)
|
|
139
|
+
rescue Flashpi::UnprocessableEntityError => e
|
|
140
|
+
puts e.code # "validation_failed", "total_mismatch", ...
|
|
141
|
+
e.errors.each { |field| puts "#{field[:field]}: #{field[:message]}" }
|
|
142
|
+
raise
|
|
143
|
+
rescue Flashpi::APIError => e
|
|
144
|
+
puts e.status, e.code, e.request_id # share request_id with support
|
|
145
|
+
raise
|
|
146
|
+
end
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
| Class | Status | Examples of `code` |
|
|
150
|
+
| ----------------------------------- | ------ | ----------------------------------------------------------- |
|
|
151
|
+
| `Flashpi::BadRequestError` | 400 | `malformed_json`, `invalid_idempotency_key` |
|
|
152
|
+
| `Flashpi::AuthenticationError` | 401 | `authentication_required`, `invalid_api_key` |
|
|
153
|
+
| `Flashpi::PermissionDeniedError` | 403 | `key_read_only`, `key_scope_mismatch`, `ip_not_allowed` |
|
|
154
|
+
| `Flashpi::NotFoundError` | 404 | `not_found` |
|
|
155
|
+
| `Flashpi::ConflictError` | 409 | `sequential_already_used`, `ride_not_available` |
|
|
156
|
+
| `Flashpi::UnprocessableEntityError` | 422 | `validation_failed`, `total_mismatch` |
|
|
157
|
+
| `Flashpi::RateLimitError` | 429 | `demo_issuer_quota_exceeded`, `account_email_limit_reached` |
|
|
158
|
+
| `Flashpi::InternalServerError` | 5xx | `internal_error` |
|
|
159
|
+
| `Flashpi::APIConnectionError` | — | no answer (network); `APITimeoutError` when it timed out |
|
|
160
|
+
|
|
161
|
+
They all extend `Flashpi::Error`. Some errors carry extra data in `error.problem`, such as
|
|
162
|
+
`documentId`, `limit`, `resetsAt` or `rideStatus`. For messages in Spanish:
|
|
163
|
+
`Flashpi::Client.new(language: "es")`.
|
|
164
|
+
|
|
165
|
+
## Retries and idempotency
|
|
166
|
+
|
|
167
|
+
The SDK retries network errors, timeouts, and 408, 429 and 5xx answers (when they ask to wait
|
|
168
|
+
60 s or less; not 501 nor `demo_issuer_unavailable`) up to 2 times, with growing waits. Every POST
|
|
169
|
+
carries an `Idempotency-Key` that its retries repeat, so the API never issues twice:
|
|
170
|
+
|
|
171
|
+
- When a retry finds that the first attempt did create the document, the SDK returns that
|
|
172
|
+
document.
|
|
173
|
+
- When the first attempt is still running on the API (for example, the connection dropped while
|
|
174
|
+
it waited for the SRI), the SDK waits for it with the same key, up to the call's time limit,
|
|
175
|
+
without using up retries.
|
|
176
|
+
|
|
177
|
+
To use your own key (for example your order id), pass it to the call:
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
flashpi.invoices.create(body, idempotency_key: "order-#{order.id}")
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
If issuing still fails, the error (a `Flashpi::Error`) carries the key it used in
|
|
184
|
+
`error.idempotency_key`. Trying again with `idempotency_key: error.idempotency_key` never issues
|
|
185
|
+
a second document: within 24 hours the API returns the first answer again; after that (or if the
|
|
186
|
+
API lost track of the first attempt), the call fails with a 422 `idempotency_key_reused` whose
|
|
187
|
+
`error.problem[:documentId]` is the existing document (read it with `flashpi.documents.get`).
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
begin
|
|
191
|
+
flashpi.invoices.create(body)
|
|
192
|
+
rescue Flashpi::Error => e
|
|
193
|
+
save_for_retry(body, e.idempotency_key) if e.idempotency_key # your retry queue
|
|
194
|
+
raise
|
|
195
|
+
end
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
A call you interrupt from outside (with `Timeout.timeout`, or by stopping the process or the
|
|
199
|
+
thread) raises no `Flashpi::Error` and has no key: if you do that to issuing calls, pass your own
|
|
200
|
+
`idempotency_key`.
|
|
201
|
+
|
|
202
|
+
Daily limits (such as the demo issuer's quota) are not retried: they are raised as a
|
|
203
|
+
`Flashpi::RateLimitError` with `retry_after` in seconds.
|
|
204
|
+
|
|
205
|
+
## Webhooks
|
|
206
|
+
|
|
207
|
+
Verify every webhook with the body **exactly as it arrived** (not parsed) and the endpoint's
|
|
208
|
+
`whsec_` secret. `Flashpi::Webhooks.unwrap` needs no API key (it is also `flashpi.webhooks`). It
|
|
209
|
+
takes the headers as a hash, a Rack `env` or Rails' `request.headers`.
|
|
210
|
+
|
|
211
|
+
With Rails:
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
class FlashpiWebhooksController < ApplicationController
|
|
215
|
+
skip_forgery_protection
|
|
216
|
+
|
|
217
|
+
def create
|
|
218
|
+
event = Flashpi::Webhooks.unwrap(request.raw_post, request.headers, ENV.fetch("FLASHPI_WEBHOOK_SECRET"))
|
|
219
|
+
|
|
220
|
+
case event[:type]
|
|
221
|
+
when "webhook_endpoint.verification"
|
|
222
|
+
# The endpoint's activation: answer the challenge.
|
|
223
|
+
return render(json: { challenge: event[:data][:challenge] })
|
|
224
|
+
when "invoice.authorized"
|
|
225
|
+
Rails.logger.info("Authorized #{event[:data][:accessKey]}") # event[:data] is the document
|
|
226
|
+
when "invoice.rejected"
|
|
227
|
+
Rails.logger.info("Rejected #{event[:data][:sriMessages]}")
|
|
228
|
+
end
|
|
229
|
+
head :no_content
|
|
230
|
+
rescue Flashpi::WebhookVerificationError
|
|
231
|
+
head :bad_request
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
With Sinatra (or any Rack app, with its `env`):
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
require "sinatra"
|
|
240
|
+
require "flashpi"
|
|
241
|
+
|
|
242
|
+
post "/webhooks/flashpi" do
|
|
243
|
+
event = Flashpi::Webhooks.unwrap(request.body.read, request.env, ENV.fetch("FLASHPI_WEBHOOK_SECRET"))
|
|
244
|
+
if event[:type] == "webhook_endpoint.verification"
|
|
245
|
+
content_type :json
|
|
246
|
+
JSON.generate({ challenge: event[:data][:challenge] })
|
|
247
|
+
else
|
|
248
|
+
handle_event(event) # your code, like the branches of the Rails example
|
|
249
|
+
204
|
|
250
|
+
end
|
|
251
|
+
rescue Flashpi::WebhookVerificationError
|
|
252
|
+
400
|
|
253
|
+
end
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Store `event[:id]` and drop repeats: an event may arrive more than once. During a secret
|
|
257
|
+
rotation Flashpi signs with both the new and the previous secret, so either one works. If the
|
|
258
|
+
endpoint uses the `thin` mode (no `data`), read the event with `flashpi.events.get(event[:id])`.
|
|
259
|
+
|
|
260
|
+
## Issuers and certificates
|
|
261
|
+
|
|
262
|
+
These operations need an account-wide key with read and write access.
|
|
263
|
+
|
|
264
|
+
An issuer starts from its electronic signature (.p12): Flashpi checks it, reads the RUC and asks
|
|
265
|
+
the SRI, and returns a `prefill` with what it found so you can build the issuer.
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
onboarding = flashpi.issuer_onboardings.create(
|
|
269
|
+
{ file: File.binread("firma.p12"), password: ENV.fetch("P12_PASSWORD") }
|
|
270
|
+
)
|
|
271
|
+
while onboarding[:status] == "verifying"
|
|
272
|
+
sleep 2
|
|
273
|
+
onboarding = flashpi.issuer_onboardings.get(onboarding[:id])
|
|
274
|
+
end
|
|
275
|
+
unless onboarding[:status] == "ready"
|
|
276
|
+
rejection = onboarding[:rejection]
|
|
277
|
+
raise(rejection ? rejection[:message] : onboarding[:status])
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
prefill = onboarding[:prefill]
|
|
281
|
+
completed =
|
|
282
|
+
if onboarding[:existingIssuer]
|
|
283
|
+
# You already have that RUC: the certificate is added to that issuer (a renewal).
|
|
284
|
+
flashpi.issuer_onboardings.complete(onboarding[:id])
|
|
285
|
+
else
|
|
286
|
+
# A new RUC: you confirm the issuer's data, starting from the prefill.
|
|
287
|
+
tax_profile = prefill && prefill[:taxProfile]
|
|
288
|
+
rimpe_regime = tax_profile && tax_profile[:rimpeRegime]
|
|
289
|
+
flashpi.issuer_onboardings.complete(
|
|
290
|
+
onboarding[:id],
|
|
291
|
+
{
|
|
292
|
+
legalName: (prefill && prefill[:legalName]) || "MY COMPANY S.A.",
|
|
293
|
+
headOfficeAddress: (prefill && prefill[:headOfficeAddress]) || "Av. Amazonas N24-100, Quito",
|
|
294
|
+
taxProfile: {
|
|
295
|
+
keepsAccounting: tax_profile ? tax_profile[:keepsAccounting] == true : false,
|
|
296
|
+
# The RIMPE regime, if any ("general" is not one).
|
|
297
|
+
rimpeRegime: case rimpe_regime when "entrepreneur", "popular_business" then rimpe_regime end
|
|
298
|
+
# For a special taxpayer or a withholding agent, add their resolutions.
|
|
299
|
+
}
|
|
300
|
+
# When onboarding[:rucNeedsConfirmation] is true, send ruc: too.
|
|
301
|
+
}
|
|
302
|
+
)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
flashpi.issuers.establishments.list(completed[:issuer][:id]).each do |establishment|
|
|
306
|
+
puts establishment[:code]
|
|
307
|
+
end
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
The `file` can be the bytes of the .p12 (`File.binread("firma.p12")`) or a file opened in binary
|
|
311
|
+
mode (`File.open("firma.p12", "rb")`), up to 1 MiB.
|
|
312
|
+
|
|
313
|
+
To **renew an issuer's signature** for test and live mode at once, run the same flow with the
|
|
314
|
+
new .p12: Flashpi recognizes the RUC (`existingIssuer`) and `complete` with no data adds the
|
|
315
|
+
certificate. `flashpi.issuers.certificates.upload(issuer_id, { file: ..., password: ... })`,
|
|
316
|
+
instead, uploads a certificate for the mode of the key you use only (test or live).
|
|
317
|
+
|
|
318
|
+
## Options
|
|
319
|
+
|
|
320
|
+
```ruby
|
|
321
|
+
flashpi = Flashpi::Client.new(
|
|
322
|
+
api_key: ENV.fetch("FLASHPI_API_KEY"), # defaults to the FLASHPI_API_KEY variable
|
|
323
|
+
language: "es", # language of the messages: "en" (default) or "es"
|
|
324
|
+
timeout: 60, # seconds per attempt (at least the SRI wait + 15 s with waitForAuthorization)
|
|
325
|
+
max_retries: 2 # 0 turns retries off
|
|
326
|
+
)
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Every method also takes the options of that request as keyword arguments: `idempotency_key`,
|
|
330
|
+
`timeout`, `max_retries`, `language`, `correlation_id` (your own id, which Flashpi logs) and
|
|
331
|
+
`extra_headers`.
|
|
332
|
+
|
|
333
|
+
```ruby
|
|
334
|
+
document = flashpi.documents.get(document_id, timeout: 10, language: "es", correlation_id: "order-42")
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
To see the HTTP answer and the request id, call the method through `with_response`:
|
|
338
|
+
|
|
339
|
+
```ruby
|
|
340
|
+
result = flashpi.documents.with_response.get(document_id)
|
|
341
|
+
puts result.data[:status], result.status, result.request_id
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
The timeout is one deadline per attempt, from sending it to reading the whole answer: connecting,
|
|
345
|
+
the TLS handshake and every read and write count against it (only the DNS lookup can run past
|
|
346
|
+
it). Requests go out through net/http with the proxy of the environment variables
|
|
347
|
+
(`https_proxy` or `HTTPS_PROXY`, an `http://` URL, and `no_proxy`), read when the client is
|
|
348
|
+
created, and the SDK never follows redirects. In your tests, WebMock or VCR can answer in place of
|
|
349
|
+
the API.
|
|
350
|
+
|
|
351
|
+
## Types
|
|
352
|
+
|
|
353
|
+
The gem ships its RBS signatures in `sig/`. With [Steep](https://github.com/soutaro/steep),
|
|
354
|
+
`rbs collection install` finds them through your `Gemfile.lock`, and Steep checks the names of
|
|
355
|
+
the fields, their possible values and the options of every call. The signatures need rbs 3.5 or
|
|
356
|
+
later (Steep 1.7 or later); Ruby 3.3 comes with rbs 3.4.
|
|
357
|
+
|
|
358
|
+
```ruby
|
|
359
|
+
# @type var body: Flashpi::Types::create_invoice
|
|
360
|
+
body = {
|
|
361
|
+
issuerId: "<the issuer id>",
|
|
362
|
+
establishment: "001",
|
|
363
|
+
emissionPoint: "001",
|
|
364
|
+
customer: { idType: "final_consumer" },
|
|
365
|
+
items: [{ description: "Bread", quantity: "2", unitPrice: "0.50", taxes: [{ type: "vat", rate: "15" }] }],
|
|
366
|
+
payments: [{ method: "cash", amount: "1.15" }]
|
|
367
|
+
}
|
|
368
|
+
document = flashpi.invoices.create(body) # Flashpi::Types::document
|
|
369
|
+
puts document[:accessKey]
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## Servers only
|
|
373
|
+
|
|
374
|
+
Your API key is a secret: use the SDK on your servers, never in code that runs on your users'
|
|
375
|
+
devices. `flashpi.inspect` shows the environment and the URL, never the key.
|
|
376
|
+
|
|
377
|
+
## License
|
|
378
|
+
|
|
379
|
+
[MIT](https://github.com/flashpi1/flashpi-ruby/blob/main/LICENSE) © 2026 SIGNIADIGITAL S.A.
|