Class: Deckle::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/deckle/client.rb

Constant Summary collapse

RETRYABLE_STATUS_CODES =

Status codes that are safe to retry.

[429, 500, 502, 503, 504].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(api_key:, base_url: "https://api.getdeckle.dev", timeout: 30, max_retries: 3) ⇒ Client

Create a new Deckle client.

Parameters:

  • api_key (String)

    Your Deckle API key.

  • base_url (String) (defaults to: "https://api.getdeckle.dev")

    API base URL.

  • timeout (Integer) (defaults to: 30)

    Request timeout in seconds.

  • max_retries (Integer) (defaults to: 3)

    Maximum number of retries for failed requests (default 3).

Raises:

  • (ArgumentError)


19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/deckle/client.rb', line 19

def initialize(api_key:, base_url: "https://api.getdeckle.dev", timeout: 30, max_retries: 3)
  raise ArgumentError, "Deckle API key is required" if api_key.nil? || api_key.empty?

  @api_key = api_key
  @base_url = base_url.chomp("/")
  @timeout = timeout
  @max_retries = max_retries

  @conn = Faraday.new(url: @base_url) do |f|
    f.options.timeout = @timeout
    f.options.open_timeout = @timeout
    f.headers["Authorization"] = "Bearer #{@api_key}"
    f.headers["Content-Type"] = "application/json"
    f.headers["User-Agent"] = "deckle-ruby/#{Deckle::VERSION}"
    f.adapter Faraday.default_adapter
  end

  @templates = Templates.new(self)
end

Instance Attribute Details

#templatesObject (readonly)

Returns the value of attribute templates.



8
9
10
# File 'lib/deckle/client.rb', line 8

def templates
  @templates
end

Instance Method Details

#batch(items:, webhook: nil) ⇒ Hash

Submit a batch of PDF generation jobs for async processing.

Parameters:

  • items (Array<Hash>)

    List of generation requests.

  • webhook (String, nil) (defaults to: nil)

    URL to POST when the batch completes.

Returns:

  • (Hash)

    Batch response with batch_id, total, and generation IDs.



93
94
95
96
97
98
# File 'lib/deckle/client.rb', line 93

def batch(items:, webhook: nil)
  body = { "items" => items }
  body["webhook"] = webhook if webhook

  request(:post, "/v1/generate/batch", body: body)
end

#from_react(react:, data: nil, styles: nil, options: nil, output: "url", webhook: nil) ⇒ Hash

Generate a PDF from a React component string.

Parameters:

  • react (String)

    JSX/TSX component source with default export.

  • data (Hash, nil) (defaults to: nil)

    Props to pass to the component.

  • styles (String, nil) (defaults to: nil)

    CSS styles to inject.

  • options (Hash, nil) (defaults to: nil)

    PDF rendering options.

  • output (String) (defaults to: "url")

    Output format - “url” or “base64”.

Returns:

  • (Hash)

    Generation response.



78
79
80
81
82
83
84
85
86
# File 'lib/deckle/client.rb', line 78

def from_react(react:, data: nil, styles: nil, options: nil, output: "url", webhook: nil)
  body = { "react" => react, "output" => output }
  body["data"] = data if data
  body["styles"] = styles if styles
  body["options"] = options if options
  body["webhook"] = webhook if webhook

  request(:post, "/v1/generate", body: body)
end

#from_template(template:, data:, options: nil, output: "url", webhook: nil) ⇒ Hash

Generate a PDF from a saved template with dynamic data.

Parameters:

  • template (String)

    Template ID (tmpl_xxx).

  • data (Hash)

    Data to merge into the template.

  • options (Hash, nil) (defaults to: nil)

    PDF rendering options.

  • output (String) (defaults to: "url")

    Output format - “url” or “base64”.

Returns:

  • (Hash)

    Generation response.



62
63
64
65
66
67
68
# File 'lib/deckle/client.rb', line 62

def from_template(template:, data:, options: nil, output: "url", webhook: nil)
  body = { "template" => template, "data" => data, "output" => output }
  body["options"] = options if options
  body["webhook"] = webhook if webhook

  request(:post, "/v1/generate", body: body)
end

#generate(html:, options: nil, output: "url", webhook: nil, watermark: nil) ⇒ Hash

Generate a PDF from raw HTML.

Parameters:

  • html (String)

    HTML string to convert to PDF.

  • options (Hash, nil) (defaults to: nil)

    PDF rendering options (format, margin, orientation, etc.).

  • output (String) (defaults to: "url")

    Output format - “url” or “base64”.

  • webhook (String, nil) (defaults to: nil)

    URL to POST when generation completes.

Returns:

  • (Hash)

    Generation response with id, status, url, etc.



46
47
48
49
50
51
52
53
# File 'lib/deckle/client.rb', line 46

def generate(html:, options: nil, output: "url", webhook: nil, watermark: nil)
  body = { "html" => html, "output" => output }
  body["options"] = options if options
  body["webhook"] = webhook if webhook
  body["watermark"] = watermark if watermark

  request(:post, "/v1/generate", body: body)
end

#generate_template_from_prompt(prompt:, variables: nil, template_type: "other", style: "professional") ⇒ Object

Generate a template from a natural-language prompt. The HTML returned has been server-sanitized so it’s safe to render. Requires the server to be configured with ANTHROPIC_API_KEY.



296
297
298
299
300
301
# File 'lib/deckle/client.rb', line 296

def generate_template_from_prompt(prompt:, variables: nil,
                                  template_type: "other", style: "professional")
  body = { "prompt" => prompt, "type" => template_type, "style" => style }
  body["variables"] = variables if variables
  request(:post, "/v1/ai/generate-template", body: body)
end

#get_generation(id) ⇒ Hash

Get a generation by ID.

Parameters:

  • id (String)

    Generation ID.

Returns:

  • (Hash)

    Generation details.



104
105
106
# File 'lib/deckle/client.rb', line 104

def get_generation(id)
  request(:get, "/v1/generations/#{id}")
end

#get_template_version(template_id, version_id) ⇒ Object



282
283
284
# File 'lib/deckle/client.rb', line 282

def get_template_version(template_id, version_id)
  request(:get, "/v1/templates/#{template_id}/versions/#{version_id}")
end

#get_usageHash

Get usage statistics for the current billing period.

Returns:

  • (Hash)

    Usage statistics.



120
121
122
# File 'lib/deckle/client.rb', line 120

def get_usage
  request(:get, "/v1/usage")
end

#list_generations(limit: 50, offset: 0) ⇒ Hash

List recent generations.

Parameters:

  • limit (Integer) (defaults to: 50)

    Maximum number of results (default 50).

  • offset (Integer) (defaults to: 0)

    Offset for pagination (default 0).

Returns:

  • (Hash)

    List response with data array.



113
114
115
# File 'lib/deckle/client.rb', line 113

def list_generations(limit: 50, offset: 0)
  request(:get, "/v1/generations?limit=#{limit}&offset=#{offset}")
end

#list_template_versions(template_id) ⇒ Object

── Template versions ─────────────────────────────────────────



278
279
280
# File 'lib/deckle/client.rb', line 278

def list_template_versions(template_id)
  request(:get, "/v1/templates/#{template_id}/versions")
end

#marketplace_clone(id) ⇒ Object



235
236
237
# File 'lib/deckle/client.rb', line 235

def marketplace_clone(id)
  request(:post, "/v1/marketplace/#{id}/clone")
end

#marketplace_get(id) ⇒ Object



231
232
233
# File 'lib/deckle/client.rb', line 231

def marketplace_get(id)
  request(:get, "/v1/marketplace/#{id}")
end

#marketplace_listObject

── Marketplace ────────────────────────────────────────────────



227
228
229
# File 'lib/deckle/client.rb', line 227

def marketplace_list
  request(:get, "/v1/marketplace")
end

#marketplace_publish(id) ⇒ Object



239
240
241
# File 'lib/deckle/client.rb', line 239

def marketplace_publish(id)
  request(:post, "/v1/marketplace/#{id}/publish")
end

#marketplace_report(id, reason:, notes: nil) ⇒ Object

Report a public marketplace template for moderator review.

reason: one of “spam”, “malicious”, “copyright”, “inappropriate”, “other”. notes: optional, max 1000 chars.

Returns { “report_id” => …, “auto_actioned” => bool }. auto_actioned is true when this report tripped the auto-hide threshold (3 independent reports). Re-reporting the same template from the same user yields a 409 (Deckle::Error).



256
257
258
259
260
# File 'lib/deckle/client.rb', line 256

def marketplace_report(id, reason:, notes: nil)
  body = { "reason" => reason }
  body["notes"] = notes unless notes.nil?
  request(:post, "/v1/marketplace/#{id}/report", body: body)
end

#marketplace_unpublish(id) ⇒ Object



243
244
245
# File 'lib/deckle/client.rb', line 243

def marketplace_unpublish(id)
  request(:post, "/v1/marketplace/#{id}/unpublish")
end

#pdf_add_form_fields(pdf:, fields:, output: "url") ⇒ Object

Add text / checkbox / dropdown form fields to a PDF.



159
160
161
162
163
164
165
# File 'lib/deckle/client.rb', line 159

def pdf_add_form_fields(pdf:, fields:, output: "url")
  request(:post, "/v1/pdf/forms/add-fields", body: {
    "pdf" => pdf,
    "fields" => fields,
    "output" => output
  })
end

#pdf_fill_form(pdf:, fields:, flatten: false, output: "url") ⇒ Object

Fill named form fields in an existing AcroForm PDF.



149
150
151
152
153
154
155
156
# File 'lib/deckle/client.rb', line 149

def pdf_fill_form(pdf:, fields:, flatten: false, output: "url")
  request(:post, "/v1/pdf/forms/fill", body: {
    "pdf" => pdf,
    "fields" => fields,
    "flatten" => flatten,
    "output" => output
  })
end

#pdf_info(pdf:) ⇒ Object

Get metadata about a PDF (page count, title, author, etc.).



144
145
146
# File 'lib/deckle/client.rb', line 144

def pdf_info(pdf:)
  request(:post, "/v1/pdf/info", body: { "pdf" => pdf })
end

#pdf_list_form_fields(pdf:) ⇒ Object

List the form fields on a PDF.



168
169
170
# File 'lib/deckle/client.rb', line 168

def pdf_list_form_fields(pdf:)
  request(:post, "/v1/pdf/forms/list-fields", body: { "pdf" => pdf })
end

#pdf_merge(pdfs:, output: "url") ⇒ Object

Merge multiple base64-encoded PDFs into one. Requires >= 2 inputs.



132
133
134
# File 'lib/deckle/client.rb', line 132

def pdf_merge(pdfs:, output: "url")
  request(:post, "/v1/pdf/merge", body: { "pdfs" => pdfs, "output" => output })
end

#pdf_protect(pdf:, user_password: nil, owner_password: nil, permissions: nil, output: "url") ⇒ Object

AES-256 encrypt a PDF. At least one of user_password / owner_password must be set. If only one is supplied the other is mirrored on the server so an empty owner password cannot be used to strip restrictions.

permissions: hash with optional keys :print (“none” | “low” | “full”), :modify (bool), :copy (bool), :annotate (bool).



213
214
215
216
217
218
219
220
221
222
223
# File 'lib/deckle/client.rb', line 213

def pdf_protect(pdf:, user_password: nil, owner_password: nil,
                permissions: nil, output: "url")
  if user_password.nil? && owner_password.nil?
    raise ArgumentError, "user_password or owner_password is required"
  end
  body = { "pdf" => pdf, "output" => output }
  body["user_password"]  = user_password  unless user_password.nil?
  body["owner_password"] = owner_password unless owner_password.nil?
  body["permissions"]    = permissions    unless permissions.nil?
  request(:post, "/v1/pdf/protect", body: body)
end

#pdf_sign_annotation(pdf:, name:, reason: nil, location: nil, contact: nil, page: nil, x: nil, y: nil, width: nil, height: nil, output: "url", signature: nil) ⇒ Object

Sign a PDF.

Without :signature → adds a VISUAL annotation only. Response has signature_annotation_added=true and cryptographically_signed=false.

With :signature → also embeds a real PAdES-B-B cryptographic signature. :signature must be a hash with:

:p12      base64-encoded PKCS#12 (P12/PFX) blob, max 100 KB decoded
:password P12 passphrase ("" for unprotected P12s)

The P12 is sent over TLS and used ephemerally — never persisted.



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/deckle/client.rb', line 191

def pdf_sign_annotation(pdf:, name:, reason: nil, location: nil, contact: nil,
                        page: nil, x: nil, y: nil, width: nil, height: nil,
                        output: "url", signature: nil)
  body = { "pdf" => pdf, "name" => name, "output" => output }
  body["reason"]    = reason    unless reason.nil?
  body["location"]  = location  unless location.nil?
  body["contact"]   = contact   unless contact.nil?
  body["page"]      = page      unless page.nil?
  body["x"]         = x         unless x.nil?
  body["y"]         = y         unless y.nil?
  body["width"]     = width     unless width.nil?
  body["height"]    = height    unless height.nil?
  body["signature"] = signature unless signature.nil?
  request(:post, "/v1/pdf/sign", body: body)
end

#pdf_split(pdf:, ranges: nil, output: "url") ⇒ Object

Split a PDF by page ranges. Pass nil ranges to split every page.



137
138
139
140
141
# File 'lib/deckle/client.rb', line 137

def pdf_split(pdf:, ranges: nil, output: "url")
  body = { "pdf" => pdf, "output" => output }
  body["ranges"] = ranges if ranges
  request(:post, "/v1/pdf/split", body: body)
end

#pdf_to_pdfa(pdf:, title: nil, author: nil, subject: nil, output: "url") ⇒ Object

Convert a PDF to PDF/A-1b archival format.



173
174
175
176
177
178
179
# File 'lib/deckle/client.rb', line 173

def pdf_to_pdfa(pdf:, title: nil, author: nil, subject: nil, output: "url")
  body = { "pdf" => pdf, "output" => output }
  body["title"] = title if title
  body["author"] = author if author
  body["subject"] = subject if subject
  request(:post, "/v1/pdf/pdfa", body: body)
end

#request(method, path, body: nil) ⇒ Hash

Make an HTTP request and handle errors. Retries on 429/5xx with exponential backoff.

NOTE: this method is intentionally public so that Templates (a separate class) can call ‘@client.request(…)`. Ruby’s ‘protected` keyword forbids cross-class invocations, which previously caused every `templates.*` call to raise NoMethodError.

Parameters:

  • method (Symbol)

    HTTP method (:get, :post, :put, :delete).

  • path (String)

    API path.

  • body (Hash, nil) (defaults to: nil)

    Request body.

Returns:

  • (Hash)

    Parsed JSON response.



315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
# File 'lib/deckle/client.rb', line 315

def request(method, path, body: nil)
  last_exception = nil

  (@max_retries + 1).times do |attempt|
    begin
      response = @conn.run_request(method, path, body ? JSON.generate(body) : nil, nil)
    rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
      last_exception = e
      if attempt < @max_retries
        sleep(2**attempt)
        next
      end
      raise
    end

    begin
      data = JSON.parse(response.body)
    rescue JSON::ParserError
      raise Error.new(
        "Non-JSON response from API (status #{response.status})",
        status_code: response.status,
        code: "INVALID_RESPONSE"
      )
    end

    # Retry on retryable status codes unless exhausted
    if RETRYABLE_STATUS_CODES.include?(response.status) && attempt < @max_retries
      delay = if response.status == 429
                (response.headers["Retry-After"] || (2**attempt).to_s).to_f
              else
                2**attempt
              end
      sleep(delay)
      next
    end

    if response.status == 401
      raise AuthenticationError, data.dig("error", "message") || "Unauthorized"
    end

    if response.status == 403
      raise UsageLimitError, data.dig("error", "message") || "Forbidden"
    end

    if response.status == 404
      raise NotFoundError, data.dig("error", "message") || "Not found"
    end

    if response.status == 429
      retry_after = (response.headers["Retry-After"] || "1").to_i
      raise RateLimitError.new(
        data.dig("error", "message") || "Rate limit exceeded",
        retry_after: retry_after
      )
    end

    unless response.success?
      error = data["error"] || {}
      raise Error.new(
        error["message"] || "Request failed",
        status_code: response.status,
        code: error["code"] || "UNKNOWN"
      )
    end

    return data
  end

  raise last_exception
end

#restore_template_version(template_id, version_id) ⇒ Object



286
287
288
289
# File 'lib/deckle/client.rb', line 286

def restore_template_version(template_id, version_id)
  request(:post, "/v1/templates/#{template_id}/restore",
          body: { "version_id" => version_id })
end

#starter_templates_clone(slug) ⇒ Object



272
273
274
# File 'lib/deckle/client.rb', line 272

def starter_templates_clone(slug)
  request(:post, "/v1/starter-templates/#{slug}/clone")
end

#starter_templates_get(slug) ⇒ Object



268
269
270
# File 'lib/deckle/client.rb', line 268

def starter_templates_get(slug)
  request(:get, "/v1/starter-templates/#{slug}")
end

#starter_templates_listObject

── Starter templates ──────────────────────────────────────────



264
265
266
# File 'lib/deckle/client.rb', line 264

def starter_templates_list
  request(:get, "/v1/starter-templates")
end