flexops 1.1.0 → 2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ae38cf941276d4053203fda4a44fb9ab1adb873d24bba9ec1de7fc634be3e7e2
4
- data.tar.gz: 43b602d72cbac545bff0391aa2dee8ea28f73bd7807a6042ec17731fe7943881
3
+ metadata.gz: 0176ab2f4e6ee84a35449cc7c3cc2b0d13708b62fdef7552013db7cce25bff08
4
+ data.tar.gz: d6c47bc34a6f23aa44bb799c4a1165c87eab2a592439a8e8a10bd39774f5e6f4
5
5
  SHA512:
6
- metadata.gz: '0959231eed12a9abd27bc7d370ab9576e32393a131248e231f2d7bebb622c74c29c21596f55036c1af8df73a277a3cbb8ee282c516aca90fc787839c76f102dc'
7
- data.tar.gz: 42fa01ac062f0f54ed7632e403a0d3a5ed37e3e942e6f11a45797b047f7f343c3b0419d354e83af9be217d58d1877a322ee7fee7fa6c79e9749e069cfca0f7cf
6
+ metadata.gz: cf5f9b203dbf64a94243192fd7db18fc131275b0da8ceff6a5165bc88fb64b85e26efb0d71062e030a1acf45cec9e5a3717da17833636ab6685ddc010dcb6b62
7
+ data.tar.gz: 570d832da16f3449283d3d064197efe2eeba19e6fad36183179490564829a1217f488038bd81970357a67bfef685be2d3dd06b1a3ced0e6eb0ac1272cc48a752
data/README.md CHANGED
@@ -29,8 +29,8 @@ require "flexops"
29
29
 
30
30
  # API key authentication (recommended for server-to-server)
31
31
  client = FlexOps::Client.new(
32
- api_key: "fxk_live_...",
33
- workspace_id: "ws_abc123"
32
+ api_key: "live_...",
33
+ workspace_id: "your-workspace-id"
34
34
  )
35
35
 
36
36
  # Get shipping rates from all carriers
@@ -45,7 +45,9 @@ request = {
45
45
  origin: {name: "Warehouse", addressLine1: "123 Main St", city: "New York", stateProvince: "NY", postalCode: "10001", countryCode: "US"},
46
46
  destination: {name: "Customer", addressLine1: "456 Oak Ave", city: "Los Angeles", stateProvince: "CA", postalCode: "90210", countryCode: "US"},
47
47
  package: {weight: 16, weightUnit: "oz"},
48
- maximumPostageAmount: 10.25 # Caller-approved ceiling in USD.
48
+ maximumPostageAmount: 10.25, # Caller-approved ceiling in USD.
49
+ # Required without an orderId. The SDK never fills it in; NONE (0) = not hazardous.
50
+ hazmatClassification: FlexOps::HazmatClassification::NONE
49
51
  }
50
52
  preview = client.shipping.create_label(request)
51
53
  puts preview["quotedPostageAmount"]
@@ -88,57 +90,220 @@ separate contracts. The SDK never automatically confirms a preview.
88
90
  ### API key (recommended)
89
91
 
90
92
  ```ruby
91
- client = FlexOps::Client.new(api_key: "fxk_live_...", workspace_id: "ws_abc123")
93
+ client = FlexOps::Client.new(api_key: "live_...", workspace_id: "your-workspace-id")
92
94
  ```
93
95
 
94
96
  ### Email / password
95
97
 
96
98
  ```ruby
97
99
  client = FlexOps::Client.new(base_url: "https://gateway.flexops.io")
98
- client.auth.login(email: "user@example.com", password: "password")
99
- client.workspace_id = "ws_abc123"
100
+ session = client.auth.login(email: "user@example.com", password: "password")
101
+ client.set_access_token(session["data"]["token"])
102
+ client.workspace_id = "your-workspace-id"
100
103
  ```
101
104
 
102
105
  ## Sandbox / test keys
103
106
 
104
- Use `fxk_test_...` (instead of `fxk_live_...`) to route to the sandbox environment. Mock carriers respond, nothing hits real carrier APIs, no charges, no real labels. Perfect for CI and integration tests.
107
+ Use `test_...` (instead of `live_...`) to route to the sandbox environment. Mock carriers respond, nothing hits real carrier APIs, no charges, no real labels. Perfect for CI and integration tests.
105
108
 
106
109
  ```ruby
107
- client = FlexOps::Client.new(api_key: "fxk_test_...", workspace_id: "ws_abc123")
110
+ client = FlexOps::Client.new(api_key: "test_...", workspace_id: "your-workspace-id")
108
111
  ```
109
112
 
110
- ## Direct carrier operations
113
+ ## Carrier-specific calls
111
114
 
112
- Access carrier-specific endpoints when you need full control:
115
+ `client.carriers.usps`, `.ups`, `.fedex` and `.dhl` call carrier routes through the legacy
116
+ `/api/ApiProxy` passthrough, which the Gateway retires on 2027-06-30.
117
+
118
+ - Lookups (GET), such as address checks, tracking and DHL rates, work with an API key or a user
119
+ token.
120
+ - The writes (POST, PUT and DELETE, such as `usps.get_domestic_rates`, `fedex.cancel_shipment`
121
+ or `ups.create_pickup`) are deprecated. They need a user token from `auth.login`: API keys get
122
+ 403 `API_KEY_SCOPE_REQUIRED`, and no API-key scope will be added for them. They stop working when
123
+ the proxy is retired.
124
+ - Label generation through the proxy is closed (410 Gone), so the SDK has no carrier label
125
+ method.
126
+
127
+ Use the native routes instead where they cover the call:
128
+
129
+ | Instead of | Use |
130
+ |---|---|
131
+ | Carrier rate calls | `client.shipping.get_rates` |
132
+ | Carrier labels | `client.shipping.create_label` (`client.returns.generate_label` for returns) |
133
+ | Label cancels | `client.shipping.cancel_label` |
134
+ | Carrier pickups | `client.pickups.schedule`, `list`, `get`, `cancel` |
135
+ | USPS scan forms | `client.scan_forms.create` |
136
+ | Address validation | `client.shipping.validate_address` |
137
+ | Tracking | `client.shipping.track` |
113
138
 
114
139
  ```ruby
115
- # USPS domestic label
116
- label = client.carriers.usps.create_domestic_label(
117
- image_type: "PDF",
118
- mail_class: "PRIORITY_MAIL",
119
- weight_in_ounces: 16
140
+ # USPS city and state for a ZIP code (works with an API key)
141
+ place = client.carriers.usps.city_state_lookup("10001")
142
+ ```
143
+
144
+ ## Retries and idempotency
145
+
146
+ The client retries a request up to three times with exponential backoff:
147
+
148
+ - GET: after a dropped connection or timeout, and on 429, 500, 502, 503 and 504.
149
+ - POST, PUT and PATCH: after a dropped connection or timeout, and on 429, 502, 503, 504 and
150
+ 409 `IDEMPOTENCY_CONFLICT` (the first request with that key is still running). Never on 500:
151
+ the Gateway does not replay a 5xx, so a retry could run the write (and charge the wallet)
152
+ again.
153
+ - DELETE: only on 429.
154
+
155
+ A key reused with a different request body gets 422 `IDEMPOTENCY_KEY_REUSED`; it is not retried
156
+ and raises `FlexOps::Error` with that `code`. Error codes are matched exactly, so a label
157
+ purchase's own 409 `IdempotencyConflict` (altered inputs under a used key) is not retried either.
158
+
159
+ Every POST, PUT and PATCH carries an `Idempotency-Key` header. Pass your own to
160
+ `shipping.create_label(request, idempotency_key: ...)`, `insurance.purchase(..., idempotency_key: ...)`
161
+ or `carbon_offsets.purchase(..., idempotency_key: ...)`, and store it so a retry your app makes after
162
+ an error or a timeout reuses it; otherwise the client generates one UUID
163
+ per call and sends it on every retry of that call, so the Gateway replays a write it already
164
+ finished instead of running it twice. A 2xx response with no body returns `nil`.
165
+
166
+ ## Workspace members
167
+
168
+ Roles are the Gateway `WorkspaceRole` numbers (Owner 0, Admin 1, Member 2, Merchant 3). Pass a
169
+ `FlexOps::WorkspaceRole` constant or a name; the SDK sends the number.
170
+
171
+ ```ruby
172
+ client.workspaces.invite_member(email: "ops@example.com", role: :member)
173
+ client.workspaces.update_member_role(user_id, FlexOps::WorkspaceRole::ADMIN)
174
+
175
+ result = client.workspaces.list_members
176
+ members = result["data"]["members"] # also "pendingInvites", "userLimit", "canInviteMore"
177
+ ```
178
+
179
+ ## Recurring shipments, report schedules and email templates
180
+
181
+ Reads work with any API key or a user token. Writes (every POST, PUT and DELETE, including the
182
+ email template preview) need a user token from `auth.login` or an API key granted the
183
+ resource's opt-in scope: `recurring-shipments:write`, `report-schedules:write` or
184
+ `email-templates:write`. Older keys do not have these scopes; a key without one gets 403
185
+ `API_KEY_SCOPE_REQUIRED` with `requiredScopes`. Enums are sent as numbers and ids are GUID
186
+ strings.
187
+
188
+ ```ruby
189
+ # Recurring shipments (bare JSON). interval: Daily 0, Weekly 1, Biweekly 2, Monthly 3, Custom 4
190
+ schedule = client.recurring_shipments.create(
191
+ name: "Weekly restock", addressId: address_id, presetId: preset_id,
192
+ interval: 1, firstRunUtc: "2026-11-02T14:00:00Z"
120
193
  )
194
+ client.recurring_shipments.pause(schedule["id"]) # nil (204); also resume, delete
195
+ client.recurring_shipments.trigger(schedule["id"], idempotency_key: run_key) # creates an order now
196
+
197
+ # Report schedules ({ success, data, message } envelope).
198
+ # format: Csv 0, Json 1; frequencyType: Daily 0, Weekly 1, Monthly 2
199
+ report = client.report_schedules.create(
200
+ reportType: "shipments", format: 0, frequencyType: 1, dayOfWeek: 1,
201
+ timeOfDayUtc: "08:00:00", recipientEmails: "ops@example.com"
202
+ )["data"]
203
+ client.report_schedules.toggle(report["id"]) # flips isEnabled
204
+
205
+ # Tracking email templates (bare JSON), one per event. eventType: LabelCreated 0, InTransit 1,
206
+ # OutForDelivery 2, Delivered 3, Exception 4, ReturnToSender 5
207
+ client.email_templates.upsert(eventType: 3, subjectTemplate: "Delivered: {{tracking_number}}",
208
+ htmlBodyTemplate: "<p>Your package arrived.</p>")
209
+ client.email_templates.preview(subject_template: "Hi {{recipient_name}}", html_body_template: "<p>{{status}}</p>")
210
+ ```
121
211
 
122
- # FedEx rate quote
123
- rates = client.carriers.fedex.get_rates(...)
212
+ ## Multi-package labels
124
213
 
125
- # UPS tracking
126
- info = client.carriers.ups.track(tracking_number: "1Z999AA10123456784")
214
+ One order shipped in several boxes with the same carrier and service; each box gets its own label and
215
+ a box that fails does not stop the others. Multi-package labels cannot carry HAZMAT: send
216
+ `FlexOps::HazmatClassification::NONE` or leave `hazmatClassification` out.
127
217
 
128
- # DHL shipment
129
- shipment = client.carriers.dhl.create_shipment(...)
218
+ ```ruby
219
+ begin
220
+ shipment = client.shipping.create_multi_package(
221
+ { orderId: 12345, carrierCode: "USPS", serviceCode: "PRIORITY",
222
+ hazmatClassification: FlexOps::HazmatClassification::NONE,
223
+ packages: [{ weightInOunces: 16, lengthInInches: 10, widthInInches: 8, heightInInches: 4, packageReference: "Box 1 of 2" },
224
+ { weightInOunces: 8, lengthInInches: 6, widthInInches: 6, heightInInches: 6, packageReference: "Box 2 of 2" }] },
225
+ idempotency_key: "order-12345-boxes" # optional; reuse it when you retry
226
+ )
227
+ rescue FlexOps::Error => e
228
+ # No label was bought: 422 AllPackagesFailed, or 409 OutcomeUnknown when a label may exist
229
+ # (check the order before buying again). e.data holds the per-box results.
230
+ end
130
231
  ```
131
232
 
132
- ## Webhook verification
233
+ `client.returns.get` reads a received return by its numeric return id; read an RMA with
234
+ `client.returns.get_rma("RMA-1001")` or page through them with `client.returns.list_rmas`.
235
+
236
+ ## Wallet balance
133
237
 
134
238
  ```ruby
135
- valid = FlexOps::Resources::Webhooks.verify_signature(
136
- payload: request.body.read,
137
- signature: request.headers["X-FlexOps-Signature"],
138
- secret: "whsec_..."
239
+ balance = client.wallet.get_balance # {"hasWallet" => true, "balanceCents" => 1250, "balance" => 12.5}
240
+ ```
241
+
242
+ The request carries `X-Workspace-Id` set to the client's `workspace_id`. Funding and
243
+ transaction history are managed in the FlexOps portal.
244
+
245
+ ## Webhook subscriptions
246
+
247
+ ```ruby
248
+ # Leave events out to receive every event.
249
+ webhook = client.webhooks.create(
250
+ url: "https://your-app.com/webhooks/flexops",
251
+ events: [FlexOps::WebhookEventType::LABEL_CREATED,
252
+ FlexOps::WebhookEventType::LABEL_CANCELED,
253
+ FlexOps::WebhookEventType::BATCH_COMPLETED]
139
254
  )
255
+ signing_secret = webhook["secret"] # "whsec_...", returned only on create; store it
256
+
257
+ # Change the events (or url, description, status: "Active", "Paused", "Disabled").
258
+ client.webhooks.update(webhook["id"], events: [FlexOps::WebhookEventType::LABEL_CREATED])
259
+ ```
260
+
261
+ `create` returns the subscription itself (`"id"`, `"url"`, `"eventTypes"`, `"status"`, `"secret"`, ...), because the Gateway answers it with the bare subscription. The other webhook methods return the usual `{"success" => true, "data" => ...}` envelope.
262
+
263
+ `events` is sent as the Gateway's comma-separated `eventTypes`. FlexOps sends these events, named exactly as below (`FlexOps::WebhookEventType::ALL` lists them). Names are PascalCase and matched exactly, so a name like `label.created` would never match; `create` and `update` raise `ArgumentError`, before any request, for an unknown name, an empty list or a non-array.
264
+
265
+ | Event | Sent when |
266
+ |---|---|
267
+ | `LabelCreated` | A live label is purchased (sandbox labels send nothing) |
268
+ | `LabelCanceled` | A live label is voided |
269
+ | `BatchCompleted` | A batch label job finishes |
270
+ | `TrackingUpdated` | A USPS tracking scan arrives (USPS push-tracking pilot workspaces only) |
271
+ | `TrackingDelivered` | A USPS package is delivered (USPS push-tracking pilot workspaces only) |
272
+
273
+ You can send a raw `eventTypes:` string (`"*"` or names joined by commas with no spaces) instead of `events:`; it is passed through unchecked, for example to subscribe to an event newer than your SDK version.
274
+
275
+ ## Webhook verification
276
+
277
+ FlexOps signs each delivery with HMAC-SHA256 over `{X-FlexOps-Timestamp}.{raw body}`, keyed with your subscription secret (`whsec_...`, used as-is), and sends it as `X-FlexOps-Signature: sha256=<hex>`. Verify the raw body before you parse it.
278
+
279
+ ```ruby
280
+ # Rails controller action
281
+ def flexops_webhook
282
+ payload = request.raw_post
283
+
284
+ valid = FlexOps::Resources::Webhooks.verify_signature(
285
+ payload: payload,
286
+ signature: request.headers["X-FlexOps-Signature"], # "sha256=..."
287
+ timestamp: request.headers["X-FlexOps-Timestamp"], # Unix seconds
288
+ secret: ENV.fetch("FLEXOPS_WEBHOOK_SECRET") # "whsec_..."
289
+ )
290
+ return head :unauthorized unless valid
291
+
292
+ # The body is the event data itself (no type/data envelope).
293
+ # The event name is in a header; test events send "test".
294
+ data = JSON.parse(payload)
295
+ case request.headers["X-FlexOps-Event"]
296
+ when "test"
297
+ # data: {"type"=>"test", "message"=>"...", "timestamp"=>"...", "subscriptionId"=>"..."}
298
+ when "TrackingUpdated"
299
+ # data["trackingNumber"], data["status"], ...
300
+ end
301
+ head :ok
302
+ end
140
303
  ```
141
304
 
305
+ Deliveries whose timestamp is more than 300 seconds from your clock (either way) are rejected to limit replays; pass `tolerance:` (seconds) to change that. `verify_signature!` takes the same arguments and raises `FlexOps::WebhookSignatureError` instead of returning `false`; its `code` says why (`WEBHOOK_SIGNATURE_MISMATCH`, `WEBHOOK_TIMESTAMP_OUTSIDE_TOLERANCE`, `WEBHOOK_INVALID_TIMESTAMP`, `WEBHOOK_MISSING_SIGNATURE`, `WEBHOOK_MISSING_SECRET`).
306
+
142
307
  ## Curl quickstart
143
308
 
144
309
  Every SDK method is a thin wrapper around the FlexOps REST API. If you want to verify the API before committing to the SDK — or you're integrating from a language we don't ship a SDK for — these curl invocations hit the same endpoints:
@@ -146,7 +311,7 @@ Every SDK method is a thin wrapper around the FlexOps REST API. If you want to v
146
311
  ```bash
147
312
  # Shop rates across all connected carriers
148
313
  curl -X POST https://gateway.flexops.io/api/shipping/rates \
149
- -H "X-API-Key: fxk_live_..." \
314
+ -H "X-API-Key: live_..." \
150
315
  -H "Content-Type: application/json" \
151
316
  -d '{
152
317
  "origin": {"addressLine1": "123 Main St", "city": "New York", "stateProvince": "NY", "postalCode": "10001", "countryCode": "US"},
@@ -155,15 +320,17 @@ curl -X POST https://gateway.flexops.io/api/shipping/rates \
155
320
  }'
156
321
 
157
322
  # Preview a live label; this does not purchase it.
158
- curl -X POST https://gateway.flexops.io/api/workspaces/ws_abc123/shipping/labels \
159
- -H "X-API-Key: fxk_live_..." \
323
+ # hazmatClassification 0 = not hazardous; required when there is no orderId.
324
+ curl -X POST https://gateway.flexops.io/api/shipping/labels \
325
+ -H "X-API-Key: live_..." \
160
326
  -H "Content-Type: application/json" \
161
327
  -d '{
162
328
  "carrierCode": "USPS", "serviceCode": "GROUND_ADVANTAGE",
163
329
  "origin": {"name": "Warehouse", "addressLine1": "123 Main St", "city": "New York", "stateProvince": "NY", "postalCode": "10001", "countryCode": "US"},
164
330
  "destination": {"name": "Customer", "addressLine1": "456 Oak Ave", "city": "Los Angeles", "stateProvince": "CA", "postalCode": "90210", "countryCode": "US"},
165
331
  "package": {"weight": 16, "weightUnit": "oz"},
166
- "maximumPostageAmount": 10.25
332
+ "maximumPostageAmount": 10.25,
333
+ "hazmatClassification": 0
167
334
  }'
168
335
 
169
336
  # After explicit approval, resend the same body with confirmationToken from
@@ -171,55 +338,56 @@ curl -X POST https://gateway.flexops.io/api/workspaces/ws_abc123/shipping/labels
171
338
  # See the SDK example above; retain that key for retries.
172
339
 
173
340
  # Track a shipment
174
- curl https://gateway.flexops.io/api/workspaces/ws_abc123/shipping/track/9400111899223456789012 \
175
- -H "X-API-Key: fxk_live_..."
341
+ curl https://gateway.flexops.io/api/shipping/track/9400111899223456789012 \
342
+ -H "X-API-Key: live_..."
176
343
 
177
- # Cancel a label (via the unified carrier-agnostic endpoint)
178
- curl -X DELETE https://gateway.flexops.io/api/v3.0/shipping/Usps/cancel/9400111899223456789012 \
179
- -H "X-API-Key: fxk_live_..."
344
+ # Cancel a label
345
+ curl -X DELETE "https://gateway.flexops.io/api/shipping/labels/lbl_123?carrierCode=USPS" \
346
+ -H "X-API-Key: live_..."
180
347
  ```
181
348
 
182
- Use an `fxk_test_...` key instead of `fxk_live_...` to hit the sandbox environment; mock carriers respond, no real charges, no real labels.
349
+ Use a `test_...` key instead of `live_...` to hit the sandbox environment; mock carriers respond, no real charges, no real labels.
183
350
 
184
351
  ## Resources
185
352
 
186
353
  | Resource | Description |
187
354
  |----------|-------------|
188
- | `client.auth` | Login, register, password management |
189
- | `client.workspaces` | Workspace CRUD, membership, branding |
190
- | `client.shipping` | Rate shopping, labels, tracking, batch, cancel |
191
- | `client.carriers` | USPS, UPS, FedEx, DHL direct endpoints |
192
- | `client.webhooks` | Subscription CRUD, signature verification, delivery logs |
193
- | `client.wallet` | Balance, transactions, auto-reload |
194
- | `client.insurance` | Quotes, purchase, claims (first-party + U-PIC) |
195
- | `client.returns` | RMA lifecycle: create, batch, QR codes, photo upload, cost recovery |
196
- | `client.api_keys` | Key creation, rotation, revocation |
197
- | `client.analytics` | Shipments, orders, carrier performance |
198
- | `client.orders` | Order management |
199
- | `client.inventory` | Warehouse inventory |
200
- | `client.pickups` | Carrier pickup scheduling |
355
+ | `client.auth` | Login, token refresh, logout |
356
+ | `client.workspaces` | List and read workspaces; list, invite and remove members, change roles |
357
+ | `client.shipping` | Rate shopping, labels, multi-package labels, tracking, address validation, batch, cancel |
358
+ | `client.carriers` | USPS, UPS, FedEx and DHL calls through the legacy proxy; writes are deprecated and need a user token |
359
+ | `client.webhooks` | Subscription create/update/delete, secret rotation, test events, delivery logs, signature verification |
360
+ | `client.wallet` | Workspace balance |
361
+ | `client.insurance` | Providers, quotes, policies, void, claims |
362
+ | `client.returns` | List and read received returns; create, read and list RMAs; return labels; receive |
363
+ | `client.analytics` | Shipments, orders, carrier performance (legacy proxy reads) |
364
+ | `client.orders` | Order reads (legacy proxy) |
365
+ | `client.inventory` | Inventory reads (legacy proxy) |
366
+ | `client.pickups` | Schedule, list, read and cancel carrier pickups |
201
367
  | `client.scan_forms` | USPS scan forms |
202
- | `client.rules` | Shipping automation rules |
203
- | `client.offsets` | Carbon offset purchases |
204
- | `client.hs_codes` | HS code lookup for international customs |
205
- | `client.recurring_shipments` | Scheduled recurring shipments |
206
- | `client.email_templates` | Branded post-purchase email templates |
207
- | `client.reports` | Report generation and scheduled delivery |
368
+ | `client.rules` | Shipping rules: create, read, update, delete, reorder, test |
369
+ | `client.hs_codes` | HS code search and lookup, landed cost |
370
+ | `client.carbon_offsets` | Carbon offset quotes, purchases, summary and settings |
371
+ | `client.recurring_shipments` | Recurring shipment schedules: create, update, pause, resume, trigger, delete |
372
+ | `client.report_schedules` | Scheduled report emails: create, update, toggle, delete |
373
+ | `client.email_templates` | Tracking email templates: upsert, preview, delete |
374
+
375
+ API keys, workspace settings and account management are managed in the FlexOps portals.
208
376
 
209
377
  ## Configuration
210
378
 
211
379
  ```ruby
212
380
  client = FlexOps::Client.new(
213
381
  base_url: "https://gateway.flexops.io", # API base URL
214
- api_key: "fxk_live_...", # API key auth
215
- workspace_id: "ws_abc123", # Default workspace
382
+ api_key: "live_...", # API key auth
383
+ workspace_id: "your-workspace-id", # Default workspace
216
384
  timeout: 30 # Request timeout (seconds)
217
385
  )
218
386
  ```
219
387
 
220
388
  ## Requirements
221
389
 
222
- - Ruby 3.0+
390
+ - Ruby 3.1+
223
391
 
224
392
  ## License
225
393
 
@@ -10,9 +10,9 @@ module FlexOps
10
10
  class Client
11
11
  attr_accessor :workspace_id
12
12
  attr_reader :auth, :workspaces, :shipping, :carriers, :webhooks, :wallet,
13
- :insurance, :returns, :api_keys, :analytics, :orders, :inventory,
14
- :pickups, :scan_forms, :rules, :offsets, :hs_codes,
15
- :recurring_shipments, :email_templates, :reports
13
+ :insurance, :returns, :analytics, :orders, :inventory,
14
+ :pickups, :scan_forms, :rules, :hs_codes, :carbon_offsets,
15
+ :recurring_shipments, :report_schedules, :email_templates
16
16
 
17
17
  def initialize(api_key: nil, access_token: nil, base_url: "https://gateway.flexops.io", workspace_id: nil, timeout: 30)
18
18
  @http = HttpClient.new(base_url: base_url, api_key: api_key, access_token: access_token, timeout: timeout)
@@ -28,18 +28,17 @@ module FlexOps
28
28
  @wallet = Resources::Wallet.new(@http, ws_id_proc)
29
29
  @insurance = Resources::Insurance.new(@http, ws_id_proc)
30
30
  @returns = Resources::Returns.new(@http, ws_id_proc)
31
- @api_keys = Resources::ApiKeys.new(@http, ws_id_proc)
32
31
  @analytics = Resources::Analytics.new(@http)
33
32
  @orders = Resources::Orders.new(@http)
34
33
  @inventory = Resources::Inventory.new(@http)
35
34
  @pickups = Resources::Pickups.new(@http, ws_id_proc)
36
35
  @scan_forms = Resources::ScanForms.new(@http, ws_id_proc)
37
36
  @rules = Resources::Rules.new(@http, ws_id_proc)
38
- @offsets = Resources::Offsets.new(@http, ws_id_proc)
39
- @hs_codes = Resources::HsCodes.new(@http, ws_id_proc)
37
+ @hs_codes = Resources::HsCodes.new(@http)
38
+ @carbon_offsets = Resources::CarbonOffsets.new(@http, ws_id_proc)
40
39
  @recurring_shipments = Resources::RecurringShipments.new(@http, ws_id_proc)
40
+ @report_schedules = Resources::ReportSchedules.new(@http, ws_id_proc)
41
41
  @email_templates = Resources::EmailTemplates.new(@http, ws_id_proc)
42
- @reports = Resources::Reports.new(@http, ws_id_proc)
43
42
  end
44
43
 
45
44
  def set_access_token(token)
@@ -8,13 +8,16 @@
8
8
 
9
9
  module FlexOps
10
10
  class Error < StandardError
11
- attr_reader :status, :code, :errors
11
+ # data is the envelope's "data" on an error reply, when the Gateway sends one. A
12
+ # multi-package 422 AllPackagesFailed or 409 OutcomeUnknown carries the per-box results there.
13
+ attr_reader :status, :code, :errors, :data
12
14
 
13
- def initialize(message, status: 0, code: nil, errors: nil)
15
+ def initialize(message, status: 0, code: nil, errors: nil, data: nil)
14
16
  super(message)
15
17
  @status = status
16
18
  @code = code
17
19
  @errors = errors
20
+ @data = data
18
21
  end
19
22
  end
20
23
 
@@ -32,4 +35,13 @@ module FlexOps
32
35
  @retry_after = retry_after
33
36
  end
34
37
  end
38
+
39
+ # Raised by Resources::Webhooks.verify_signature! when a webhook delivery
40
+ # is not authentic or not fresh. +code+ says why, for example
41
+ # "WEBHOOK_SIGNATURE_MISMATCH" or "WEBHOOK_TIMESTAMP_OUTSIDE_TOLERANCE".
42
+ class WebhookSignatureError < Error
43
+ def initialize(message, code:)
44
+ super(message, code: code)
45
+ end
46
+ end
35
47
  end
@@ -0,0 +1,34 @@
1
+ # ***********************************************************************
2
+ # Package : flexops
3
+ # Author : FlexOps, LLC
4
+ # Created : 2026-10-12
5
+ #
6
+ # Copyright (c) 2021-2026 by FlexOps, LLC. All rights reserved.
7
+ # ***********************************************************************
8
+
9
+ module FlexOps
10
+ # USPS HAZMAT classification, sent as the Gateway HazmatClassification number.
11
+ #
12
+ # There is no default: leave hazmatClassification out when you have no answer, or send
13
+ # HazmatClassification::NONE (0) to declare that the parcel holds no dangerous goods.
14
+ # A label without an orderId must declare one.
15
+ module HazmatClassification
16
+ NONE = 0
17
+ LIMITED_QUANTITY = 1
18
+ LITHIUM_BATTERY_CONTAINED_IN_EQUIPMENT = 2
19
+ LITHIUM_BATTERY_PACKED_WITH_EQUIPMENT = 3
20
+ LITHIUM_BATTERY_STANDALONE = 4
21
+ OTHER_RESTRICTED = 5
22
+ LITHIUM_BATTERY_USED_DEVICE = 6
23
+
24
+ ALL = {
25
+ NONE: NONE,
26
+ LIMITED_QUANTITY: LIMITED_QUANTITY,
27
+ LITHIUM_BATTERY_CONTAINED_IN_EQUIPMENT: LITHIUM_BATTERY_CONTAINED_IN_EQUIPMENT,
28
+ LITHIUM_BATTERY_PACKED_WITH_EQUIPMENT: LITHIUM_BATTERY_PACKED_WITH_EQUIPMENT,
29
+ LITHIUM_BATTERY_STANDALONE: LITHIUM_BATTERY_STANDALONE,
30
+ OTHER_RESTRICTED: OTHER_RESTRICTED,
31
+ LITHIUM_BATTERY_USED_DEVICE: LITHIUM_BATTERY_USED_DEVICE
32
+ }.freeze
33
+ end
34
+ end