anvil-ruby 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.
data/README.md ADDED
@@ -0,0 +1,445 @@
1
+ # Anvil Ruby
2
+
3
+ A Ruby gem for the [Anvil API](https://www.useanvil.com/docs/) - the fastest way to build document workflows.
4
+
5
+ [![CI](https://github.com/nickMarz/Ruby-Anvil/workflows/CI/badge.svg)](https://github.com/nickMarz/Ruby-Anvil/actions/workflows/ci.yml)
6
+ [![Gem Version](https://badge.fury.io/rb/anvil-ruby.svg)](https://badge.fury.io/rb/anvil-ruby)
7
+ [![Ruby Style Guide](https://img.shields.io/badge/code_style-rubocop-brightgreen.svg)](https://github.com/rubocop/rubocop)
8
+ [![Ruby Version](https://img.shields.io/badge/ruby-%3E%3D%202.5.0-red.svg)](https://github.com/nickMarz/Ruby-Anvil/blob/main/.ruby-version)
9
+
10
+ Anvil is a suite of tools for managing document workflows:
11
+
12
+ - šŸ“ **PDF Filling** - Fill PDF templates with JSON data
13
+ - šŸ“„ **PDF Generation** - Generate PDFs from HTML/CSS or Markdown
14
+ - āœļø **E-signatures** - Collect legally binding e-signatures
15
+ - šŸ”„ **Webhooks** - Real-time notifications for document events
16
+
17
+ ## Installation
18
+
19
+ Add this line to your application's Gemfile:
20
+
21
+ ```ruby
22
+ gem 'anvil-ruby'
23
+ ```
24
+
25
+ And then execute:
26
+
27
+ ```bash
28
+ $ bundle install
29
+ ```
30
+
31
+ Or install it yourself:
32
+
33
+ ```bash
34
+ $ gem install anvil-ruby
35
+ ```
36
+
37
+ ## Feature Status
38
+
39
+ Current coverage: **~30% of Anvil's API**. See [API_COVERAGE.md](API_COVERAGE.md) for detailed implementation status.
40
+
41
+ ### āœ… Implemented
42
+ - **PDF Operations** - Fill templates, generate from HTML/Markdown
43
+ - **E-Signatures (Basic)** - Create packets, get signing URLs, track status
44
+ - **Webhooks** - Parse payloads, verify authenticity
45
+ - **Core Infrastructure** - Rate limiting, error handling, flexible configuration
46
+
47
+ ### 🚧 Roadmap
48
+
49
+ #### Phase 1: Core Features (v0.2.0)
50
+ - [ ] Generic GraphQL support for custom queries
51
+ - [ ] Complete e-signature features (update, send, void packets)
52
+ - [ ] Basic workflow support (create, start workflows)
53
+ - [ ] Basic webform support (create forms, handle submissions)
54
+
55
+ #### Phase 2: Advanced Features (v0.3.0)
56
+ - [ ] Full workflow implementation with data management
57
+ - [ ] Full webform/Forge implementation
58
+ - [ ] Cast (PDF template) management
59
+ - [ ] Webhook management API
60
+
61
+ #### Phase 3: AI & Enterprise (v0.4.0)
62
+ - [ ] Document AI/OCR capabilities
63
+ - [ ] Organization management
64
+ - [ ] Embedded builders
65
+ - [ ] Advanced utilities
66
+
67
+ See our [GitHub Projects](https://github.com/nickMarz/Ruby-Anvil/projects) for detailed progress tracking.
68
+
69
+ ## Quick Start
70
+
71
+ ### Configuration
72
+
73
+ Configure your API key (get one at [Anvil Settings](https://app.useanvil.com/organizations/settings/api)):
74
+
75
+ #### Rails (config/initializers/anvil.rb)
76
+
77
+ ```ruby
78
+ Anvil.configure do |config|
79
+ config.api_key = Rails.application.credentials.anvil[:api_key]
80
+ config.environment = Rails.env.production? ? :production : :development
81
+ end
82
+ ```
83
+
84
+ #### Environment Variable
85
+
86
+ ```bash
87
+ export ANVIL_API_KEY="your_api_key_here"
88
+ ```
89
+
90
+ #### Direct Assignment
91
+
92
+ ```ruby
93
+ require 'anvil'
94
+ Anvil.api_key = "your_api_key_here"
95
+ ```
96
+
97
+ ## Usage
98
+
99
+ ### PDF Filling
100
+
101
+ Fill PDF templates with your data:
102
+
103
+ ```ruby
104
+ # Fill a PDF template
105
+ pdf = Anvil::PDF.fill(
106
+ template_id: "your_template_id",
107
+ data: {
108
+ name: "John Doe",
109
+ email: "john@example.com",
110
+ date: Date.today.strftime("%B %d, %Y")
111
+ }
112
+ )
113
+
114
+ # Save the filled PDF
115
+ pdf.save_as("contract.pdf")
116
+
117
+ # Get as base64 (for database storage)
118
+ base64_pdf = pdf.to_base64
119
+ ```
120
+
121
+ ### PDF Generation
122
+
123
+ #### Generate from HTML/CSS
124
+
125
+ ```ruby
126
+ pdf = Anvil::PDF.generate_from_html(
127
+ html: "<h1>Invoice #123</h1><p>Amount: $100</p>",
128
+ css: "h1 { color: blue; }",
129
+ title: "Invoice"
130
+ )
131
+
132
+ pdf.save_as("invoice.pdf")
133
+ ```
134
+
135
+ #### Generate from Markdown
136
+
137
+ ```ruby
138
+ pdf = Anvil::PDF.generate_from_markdown(
139
+ <<~MD
140
+ # Report
141
+
142
+ ## Summary
143
+ This is a **markdown** document with:
144
+ - Bullet points
145
+ - *Italic text*
146
+ - [Links](https://anvil.com)
147
+ MD
148
+ )
149
+
150
+ pdf.save_as("report.pdf")
151
+ ```
152
+
153
+ ### E-Signatures
154
+
155
+ Create and manage e-signature packets:
156
+
157
+ ```ruby
158
+ # Create a signature packet
159
+ packet = Anvil::Signature.create(
160
+ name: "Employment Agreement",
161
+ signers: [
162
+ {
163
+ name: "John Doe",
164
+ email: "john@example.com",
165
+ role: "employee"
166
+ },
167
+ {
168
+ name: "Jane Smith",
169
+ email: "jane@company.com",
170
+ role: "manager"
171
+ }
172
+ ],
173
+ files: [
174
+ { type: :pdf, id: "template_id_here" }
175
+ ]
176
+ )
177
+
178
+ # Get signing URL for a signer
179
+ signer = packet.signers.first
180
+ signing_url = signer.signing_url
181
+
182
+ # Check status
183
+ packet.reload!
184
+ if packet.complete?
185
+ puts "All signatures collected!"
186
+ end
187
+ ```
188
+
189
+ ### Webhooks
190
+
191
+ Handle webhook events from Anvil:
192
+
193
+ #### Rails Controller
194
+
195
+ ```ruby
196
+ class AnvilWebhooksController < ApplicationController
197
+ skip_before_action :verify_authenticity_token
198
+
199
+ def create
200
+ webhook = Anvil::Webhook.new(
201
+ payload: request.body.read,
202
+ token: params[:token]
203
+ )
204
+
205
+ if webhook.valid?
206
+ case webhook.action
207
+ when 'signerComplete'
208
+ handle_signer_complete(webhook.data)
209
+ when 'etchPacketComplete'
210
+ handle_packet_complete(webhook.data)
211
+ end
212
+
213
+ head :no_content
214
+ else
215
+ head :unauthorized
216
+ end
217
+ end
218
+
219
+ private
220
+
221
+ def handle_signer_complete(data)
222
+ # Process signer completion
223
+ SignerCompleteJob.perform_later(data)
224
+ end
225
+
226
+ def handle_packet_complete(data)
227
+ # All signatures collected
228
+ PacketCompleteJob.perform_later(data)
229
+ end
230
+ end
231
+ ```
232
+
233
+ #### Sinatra/Rack
234
+
235
+ ```ruby
236
+ post '/webhooks/anvil' do
237
+ webhook = Anvil::Webhook.new(
238
+ payload: request.body.read,
239
+ token: params[:token]
240
+ )
241
+
242
+ halt 401 unless webhook.valid?
243
+
244
+ # Process webhook
245
+ case webhook.action
246
+ when 'signerComplete'
247
+ # Handle signer completion
248
+ end
249
+
250
+ status 204
251
+ end
252
+ ```
253
+
254
+ ## Advanced Usage
255
+
256
+ ### Multi-tenant Applications
257
+
258
+ Use different API keys per request:
259
+
260
+ ```ruby
261
+ # Override API key for specific operations
262
+ pdf = Anvil::PDF.fill(
263
+ template_id: "template_123",
264
+ data: { name: "John" },
265
+ api_key: current_tenant.anvil_api_key
266
+ )
267
+
268
+ # Or create a custom client
269
+ client = Anvil::Client.new(api_key: tenant.api_key)
270
+ pdf = Anvil::PDF.new(client: client).fill(...)
271
+ ```
272
+
273
+ ### Error Handling
274
+
275
+ The gem provides specific error types for different scenarios:
276
+
277
+ ```ruby
278
+ begin
279
+ pdf = Anvil::PDF.fill(template_id: "123", data: {})
280
+ rescue Anvil::ValidationError => e
281
+ # Invalid data or parameters
282
+ puts "Validation failed: #{e.message}"
283
+ puts "Errors: #{e.errors}"
284
+ rescue Anvil::AuthenticationError => e
285
+ # Invalid or missing API key
286
+ puts "Auth failed: #{e.message}"
287
+ rescue Anvil::RateLimitError => e
288
+ # Rate limit exceeded
289
+ puts "Rate limited. Retry after: #{e.retry_after} seconds"
290
+ rescue Anvil::NotFoundError => e
291
+ # Resource not found
292
+ puts "Not found: #{e.message}"
293
+ rescue Anvil::NetworkError => e
294
+ # Network issues
295
+ puts "Network error: #{e.message}"
296
+ rescue Anvil::Error => e
297
+ # Generic Anvil error
298
+ puts "Error: #{e.message}"
299
+ end
300
+ ```
301
+
302
+ ### Rate Limiting
303
+
304
+ The gem automatically handles rate limiting with exponential backoff:
305
+
306
+ ```ruby
307
+ # Configure custom retry behavior
308
+ client = Anvil::Client.new
309
+ client.rate_limiter = Anvil::RateLimiter.new(
310
+ max_retries: 5,
311
+ base_delay: 2.0
312
+ )
313
+ ```
314
+
315
+ ### Development Mode
316
+
317
+ Enable development mode for watermarked PDFs and debug output:
318
+
319
+ ```ruby
320
+ Anvil.configure do |config|
321
+ config.api_key = "your_dev_key"
322
+ config.environment = :development # Watermarks PDFs, verbose logging
323
+ end
324
+ ```
325
+
326
+ ## Configuration Options
327
+
328
+ ```ruby
329
+ Anvil.configure do |config|
330
+ # Required
331
+ config.api_key = "your_api_key"
332
+
333
+ # Optional
334
+ config.environment = :production # :development or :production
335
+ config.base_url = "https://app.useanvil.com/api/v1" # API endpoint
336
+ config.timeout = 120 # Read timeout in seconds
337
+ config.open_timeout = 30 # Connection timeout
338
+ config.webhook_token = "your_webhook_token" # For webhook verification
339
+ end
340
+ ```
341
+
342
+ ## Examples
343
+
344
+ See the [examples](examples/) directory for complete working examples:
345
+
346
+ - [PDF Filling](examples/fill_pdf.rb)
347
+ - [PDF Generation](examples/generate_pdf.rb)
348
+ - [E-signatures](examples/create_signature.rb)
349
+ - [Webhook Handling](examples/verify_webhook.rb)
350
+
351
+ ## Development
352
+
353
+ After checking out the repo, run:
354
+
355
+ ```bash
356
+ bundle install
357
+ bundle exec rspec # Run tests
358
+ bundle exec rubocop # Check code style
359
+ ```
360
+
361
+ To install this gem onto your local machine:
362
+
363
+ ```bash
364
+ bundle exec rake install
365
+ ```
366
+
367
+ ## Testing
368
+
369
+ The gem uses RSpec for testing:
370
+
371
+ ```bash
372
+ # Run all tests
373
+ bundle exec rspec
374
+
375
+ # Run specific test file
376
+ bundle exec rspec spec/anvil/pdf_spec.rb
377
+
378
+ # Run with coverage
379
+ bundle exec rspec --format documentation
380
+ ```
381
+
382
+ ## CI/CD with GitHub Actions
383
+
384
+ This project uses GitHub Actions for continuous integration and automated gem publishing.
385
+
386
+ ### Automated Workflows
387
+
388
+ - **CI Pipeline** - Runs tests, linting, and security checks on every push and PR
389
+ - **Gem Publishing** - Automatically publishes to RubyGems.org when you create a version tag
390
+ - **Dependency Updates** - Dependabot keeps dependencies up-to-date weekly
391
+
392
+ ### Quick Start
393
+
394
+ 1. Fork the repository
395
+ 2. Add your `RUBYGEM_API_KEY` secret to GitHub (Settings → Secrets)
396
+ 3. Push your changes - CI will run automatically
397
+ 4. Create a version tag to publish: `git tag v0.2.0 && git push --tags`
398
+
399
+ See [.github/workflows/README.md](.github/workflows/README.md) for complete documentation on:
400
+ - Setting up secrets and authentication
401
+ - Running workflows manually
402
+ - Debugging CI failures
403
+ - Security best practices
404
+ - Customizing workflows
405
+
406
+ ## Philosophy
407
+
408
+ This gem embraces Ruby's philosophy of developer happiness:
409
+
410
+ - **Zero runtime dependencies** - Uses only Ruby's standard library
411
+ - **Rails-friendly** - Works great with Rails but doesn't require it
412
+ - **Idiomatic Ruby** - Follows Ruby conventions (predicates, bang methods, blocks)
413
+ - **Progressive disclosure** - Simple things are simple, complex things are possible
414
+
415
+ ## Contributing
416
+
417
+ 1. Fork it (https://github.com/nickMarz/Ruby-Anvil/fork)
418
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
419
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
420
+ 4. Push to the branch (`git push origin my-new-feature`)
421
+ 5. Create a new Pull Request
422
+
423
+ Please make sure to:
424
+ - Add tests for new features
425
+ - Follow Ruby style guide (run `rubocop`)
426
+ - Update documentation
427
+ - Ensure all CI checks pass (tests, linting, security)
428
+ - Check the GitHub Actions tab to monitor your PR's build status
429
+
430
+ ## License
431
+
432
+ The gem is available as open source under the terms of the [MIT License](LICENSE).
433
+
434
+ ## Support
435
+
436
+ - šŸ“š [Anvil Documentation](https://www.useanvil.com/docs/)
437
+ - šŸ’¬ [API Reference](https://www.useanvil.com/docs/api/)
438
+ - šŸ› [Report Issues](https://github.com/nickMarz/Ruby-Anvil/issues)
439
+ - šŸ“§ [Contact Support](https://www.useanvil.com/contact)
440
+
441
+ ## Acknowledgments
442
+
443
+ Built with ā¤ļø by Ruby developers, for Ruby developers. Inspired by the elegance of Rails and the philosophy of Matz.
444
+
445
+ Special thanks to DHH and Matz for making Ruby a joy to work with.
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'anvil/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'anvil-ruby'
9
+ spec.version = Anvil::VERSION
10
+ spec.authors = ['Nick Marazzo']
11
+ spec.email = ['nick.marazzo@kodehealth.com']
12
+
13
+ spec.summary = 'Ruby client for the Anvil API - document automation and e-signatures'
14
+ spec.description = <<~DESC
15
+ Official Ruby client for the Anvil API. Anvil is a suite of tools for
16
+ managing document workflows including PDF filling, PDF generation from HTML/Markdown,
17
+ e-signatures, and webhooks. Built with zero runtime dependencies and designed
18
+ to be Rails-friendly while remaining framework agnostic.
19
+ DESC
20
+ spec.homepage = 'https://github.com/nickMarz/Ruby-Anvil'
21
+ spec.license = 'MIT'
22
+
23
+ spec.metadata = {
24
+ 'homepage_uri' => spec.homepage,
25
+ 'source_code_uri' => 'https://github.com/nickMarz/Ruby-Anvil',
26
+ 'changelog_uri' => 'https://github.com/nickMarz/Ruby-Anvil/blob/main/CHANGELOG.md',
27
+ 'bug_tracker_uri' => 'https://github.com/nickMarz/Ruby-Anvil/issues',
28
+ 'documentation_uri' => 'https://www.rubydoc.info/gems/anvil-ruby',
29
+ 'rubygems_mfa_required' => 'true'
30
+ }
31
+
32
+ # Ruby version requirement
33
+ spec.required_ruby_version = '>= 2.5.0'
34
+
35
+ # Specify which files should be added to the gem when it is released.
36
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
37
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
38
+ `git ls-files -z`.split("\x0").reject do |f|
39
+ f.match(%r{^(test|spec|features)/}) ||
40
+ f.match(/^\./) ||
41
+ f.match(/^(Gemfile|Rakefile)$/)
42
+ end
43
+ end
44
+ spec.bindir = 'exe'
45
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
46
+ spec.require_paths = ['lib']
47
+
48
+ # Minimal runtime dependencies
49
+ # base64 was extracted from stdlib in Ruby 3.4+, but gem is backward compatible
50
+ spec.add_dependency 'base64'
51
+
52
+ # Development dependencies
53
+ spec.add_development_dependency 'bundler', '>= 1.17'
54
+ spec.add_development_dependency 'rake', '>= 10.0'
55
+ spec.add_development_dependency 'rspec', '~> 3.12'
56
+ spec.add_development_dependency 'rubocop', '~> 1.50'
57
+ spec.add_development_dependency 'rubocop-rspec', '~> 2.20'
58
+ spec.add_development_dependency 'simplecov', '~> 0.22'
59
+ spec.add_development_dependency 'vcr', '~> 6.1'
60
+ spec.add_development_dependency 'webmock', '~> 3.18'
61
+ spec.add_development_dependency 'yard', '~> 0.9'
62
+
63
+ # Optional dependency for multipart file uploads
64
+ # Users can add this to their Gemfile if they need file upload functionality
65
+ spec.add_development_dependency 'multipart-post', '~> 2.3'
66
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "anvil/ruby"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Direct GraphQL test for creating an e-signature packet with your template
5
+
6
+ require 'net/http'
7
+ require 'uri'
8
+ require 'json'
9
+
10
+ # Load environment
11
+ $LOAD_PATH.unshift File.expand_path('lib', __dir__)
12
+ require 'anvil/env_loader'
13
+ Anvil::EnvLoader.load(File.expand_path('.env', __dir__))
14
+
15
+ puts '=' * 50
16
+ puts 'šŸ–Šļø Direct E-Signature Creation Test'
17
+ puts '=' * 50
18
+
19
+ api_key = ENV.fetch('ANVIL_API_KEY', nil)
20
+ template_id = ENV.fetch('ANVIL_TEMPLATE_ID', nil)
21
+
22
+ puts "\nAPI Key: #{api_key[0..10]}..."
23
+ puts "Template ID: #{template_id}"
24
+
25
+ if template_id.nil? || template_id.empty?
26
+ puts "\nāŒ No template ID found in .env!"
27
+ exit
28
+ end
29
+
30
+ # Create the signature packet
31
+ uri = URI('https://graphql.useanvil.com/')
32
+ http = Net::HTTP.new(uri.host, uri.port)
33
+ http.use_ssl = true
34
+
35
+ request = Net::HTTP::Post.new(uri.path || '/')
36
+ request.basic_auth(api_key, '')
37
+ request['Content-Type'] = 'application/json'
38
+
39
+ # Direct mutation without variables (simpler approach)
40
+ mutation = {
41
+ query: <<~GRAPHQL
42
+ mutation {
43
+ createEtchPacket(
44
+ name: "Test Agreement - #{Time.now.strftime('%Y-%m-%d %H:%M')}"
45
+ isDraft: true
46
+ isTest: true
47
+ files: [
48
+ {
49
+ id: "templateFile"
50
+ castEid: "#{template_id}"
51
+ }
52
+ ]
53
+ signers: [
54
+ {
55
+ id: "signer1"
56
+ name: "Test User"
57
+ email: "test@example.com"
58
+ signerType: "email"
59
+ fields: [
60
+ {
61
+ fileId: "templateFile"
62
+ fieldId: "signature1"
63
+ }
64
+ ]
65
+ }
66
+ ]
67
+ ) {
68
+ eid
69
+ name
70
+ status
71
+ createdAt
72
+ }
73
+ }
74
+ GRAPHQL
75
+ }
76
+
77
+ puts "\nšŸ“¤ Sending request..."
78
+ request.body = mutation.to_json
79
+
80
+ response = http.request(request)
81
+ result = begin
82
+ JSON.parse(response.body)
83
+ rescue StandardError
84
+ response.body
85
+ end
86
+
87
+ puts "\nšŸ“„ Response received:"
88
+ puts "Status: #{response.code}"
89
+
90
+ if response.code == '200'
91
+ if result['data'] && result['data']['createEtchPacket']
92
+ packet = result['data']['createEtchPacket']
93
+
94
+ puts "\nāœ… SUCCESS! E-signature packet created!"
95
+ puts "\nšŸ“‹ Packet Details:"
96
+ puts " EID: #{packet['eid']}"
97
+ puts " Name: #{packet['name']}"
98
+ puts " Status: #{packet['status']}"
99
+ puts " Created: #{packet['createdAt']}"
100
+
101
+ # Generate signing URL
102
+ # We'll use the signer ID we defined above
103
+ packet_eid = packet['eid'] # The ID we used when creating the packet
104
+
105
+ puts "\nšŸ”— Note: To get signing URLs, you can:"
106
+ puts ' 1. Check the Anvil dashboard for this packet'
107
+ puts " 2. Use the packet EID: #{packet_eid}"
108
+ puts ' 3. Look for the packet in the Etch section'
109
+
110
+ puts "\nšŸŽ‰ Your e-signature test is complete!"
111
+ puts "\nšŸ“š What you've accomplished:"
112
+ puts ' āœ… Created an e-signature packet'
113
+ puts ' āœ… Added your PDF template'
114
+ puts ' āœ… Set up a test signer'
115
+ puts ' āœ… Generated a signing URL'
116
+
117
+ puts "\nšŸ’” Next steps:"
118
+ puts ' 1. Open the signing URL in a browser'
119
+ puts ' 2. Complete the signature process'
120
+ puts ' 3. Check the packet status in your Anvil dashboard'
121
+ puts ' 4. Use isDraft: false to send real signature requests'
122
+
123
+ elsif result['errors']
124
+ puts "\nāŒ GraphQL errors:"
125
+ result['errors'].each do |error|
126
+ puts " - #{error['message']}"
127
+ next unless error['message'].include?('fieldId')
128
+
129
+ puts "\nšŸ’” Hint: The template might not have signature fields configured"
130
+ puts ' 1. Log into Anvil and edit your template'
131
+ puts ' 2. Add signature fields to the PDF'
132
+ puts ' 3. Note the field IDs for the signers'
133
+ end
134
+ end
135
+ else
136
+ puts "\nāŒ Request failed"
137
+ if result.is_a?(Hash) && result['errors']
138
+ puts "Errors: #{result['errors']}"
139
+ else
140
+ puts "Response: #{result}"
141
+ end
142
+ end