Signing Studio Ruby SDK
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 signingstudioOr 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 tomax_retries. -
429 with
Retry-After > 60s(typically monthly quota) → raiseRateLimitErrorimmediately. - 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/pdfonly. - 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
endAlways 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}")
endAll 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 rubocopCI runs against Ruby 3.0 – 3.3 on every push/PR.
Versioning
Semantic versioning. CHANGELOG.md records every release.
License
MIT. See LICENSE.