Skip to content

alyasdds/signingstudio-ruby

Repository files navigation

Signing Studio Ruby SDK

Gem Version CI License

Official Ruby client for the Signing Studio e-signature API. Covers every public v1 endpoint — send documents from templates, poll signing progress, manage templates and their fields, and verify webhook deliveries.

Requires Ruby 3.0+.

Install

gem install signingstudio

Or add to your Gemfile:

gem "signingstudio", "~> 1.0"

Quick start

require "signingstudio"

client = Signingstudio::Client.new(api_key: ENV.fetch("SIGNING_STUDIO_API_KEY"))

doc = client.documents.send(
  template_id: "11111111-2222-3333-4444-555555555555",
  title:       "MSA — Acme",
  recipients:  [{ name: "Alex Doe", email: "alex@acme.com" }],
)

puts "Sent #{doc["id"]} (#{doc["status"]})"

Get your API key from Signing Studio → Settings → API Keys. The plaintext key is shown once at creation — store it in Rails credentials, Vault, or an environment variable.

Configuration

client = Signingstudio::Client.new(
  api_key: "sk_live_...",
  base_url: "https://api.signingstudio.com",   # default
  max_retries: 3,                              # 429 + 5xx + network
  request_timeout: 120,                        # seconds
  user_agent: "my-app/1.4",
)

Retry policy — conservative and predictable:

  • 429 with Retry-After ≤ 60s → sleep and retry, up to max_retries.
  • 429 with Retry-After > 60s (typically monthly quota) → raise RateLimitError immediately.
  • 5xx and network errors → exponential backoff (500ms · 1s · 2s · 4s) with jitter.

Documents

listing = client.documents.list(status: "sent", view: "active", limit: 50)

doc = client.documents.send(
  template_id: template_id,
  title: "MSA — Acme",
  subject: "Please sign",
  message: "Signing at your convenience",
  expires_at: "2026-08-01T00:00:00Z",
  recipients: [
    { name: "Alex", email: "alex@acme.com", signing_order: 0 },
    { name: "Bo",   email: "bo@acme.com",   signing_order: 1 },
  ],
  prefill_values: [{ field_name: "company", value: "Acme Inc." }],
)

client.documents.get(doc["id"])
client.documents.progress(doc["id"])   # cheap; ideal for polling
client.documents.activity(doc["id"])

client.documents.cancel(doc["id"])
client.documents.archive(doc["id"])
client.documents.unarchive(doc["id"])
client.documents.restore(doc["id"])
client.documents.delete(doc["id"])                # soft
client.documents.delete(doc["id"], hard: true)    # hard purge

remind = client.documents.remind(doc["id"], recipient_id)
fresh_url = remind["signing_url"]

url = client.documents.download_url(doc["id"])["url"]
bytes = client.documents.download_pdf(doc["id"])

Templates

templates = client.templates.list

template = client.templates.create(
  "./msa.pdf",
  {
    name: "MSA v2",
    signer_count: 1,
    delivery_methods: ["email"],
    signers: [{ role: "Customer", delivery: ["email"] }],
  },
)

client.templates.update(template["id"], name: "MSA v3")
client.templates.delete(template["id"])

client.templates.replace_pdf(template["id"], "./msa-updated.pdf")
versions = client.templates.history(template["id"])
old_pdf_url = client.templates.history_pdf_url(template["id"], versions[0]["id"])["url"]

client.templates.set_fields(template["id"], [
  { field_type: "signature", page: 1, x: 60, y: 82, width: 30, height: 6, signer_index: 0, required: true },
  { field_type: "text",      page: 1, x: 10, y: 20, width: 30, height: 4, signer_index: 0, name: "company", label: "Company name", required: true },
])

PDF upload constraints

  • Max 50 MB per file.
  • application/pdf only.
  • Multipart field name must be file — the SDK sets this for you.

Webhooks

require "sinatra"
require "json"
require "signingstudio"

SECRET = ENV.fetch("SIGNING_STUDIO_WEBHOOK_SECRET")
verifier = Signingstudio::Client.webhook_verifier(SECRET)

post "/webhooks/signing-studio" do
  raw = request.body.read     # RAW body — do NOT re-serialize
  sig = request.env["HTTP_X_DDS_SIGNATURE"] || ""
  halt 401 unless verifier.valid?(raw, sig)

  payload = JSON.parse(raw)
  # payload["event"] is one of:
  #   "document.sent" | "document.viewed" | "document.signed"
  #   | "document.declined" | "document.completed"
  content_type :json
  { received: true }.to_json
end

Always sign the RAW body, not a parsed-and-re-serialized body.

Errors

begin
  client.documents.send(payload)
rescue Signingstudio::ValidationError => e
  # e.errors is Hash{String => Array<String>}
rescue Signingstudio::RateLimitError => e
  # e.window is "minute" | "day" | nil
  # e.retry_after is Integer | nil
rescue Signingstudio::AuthenticationError
  # Refresh the API key.
rescue Signingstudio::NotFoundError
  # Doesn't exist on this tenant.
rescue Signingstudio::ApiError => e
  Rails.logger.error("signingstudio request=#{e.request_id} status=#{e.status_code}")
end

All SDK-raised exceptions inherit from Signingstudio::Error.

Rate limits

Every response carries:

  • X-RateLimit-Limit-Minute, X-RateLimit-Remaining-Minute
  • X-RateLimit-Limit-Day, X-RateLimit-Remaining-Day

Platform defaults: 120 req/min and 20,000 req/day per API key.

Testing

bundle install
bundle exec rspec
bundle exec rubocop

CI runs against Ruby 3.0 – 3.3 on every push/PR.

Versioning

Semantic versioning. CHANGELOG.md records every release.

License

MIT. See LICENSE.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages