OnetimeSecret Ruby Client
The official Ruby client for the OnetimeSecret API. Share sensitive information through a link that can only be viewed once.
The gem is onetime. This repository, onetime-ruby, is the Ruby SDK
in the OnetimeSecret SDK family — each language has its own repository, and the
repository name is not the package name.
- Zero runtime dependencies — built entirely on the Ruby standard library
(
net/http,uri,json), so it drops into any environment without pulling transitive gems. - Supports API v1 and v2 — pick a version per client. The same resource methods exist for both, but each version returns its own response shape (they are different APIs; the client does not normalize them). v1 is deprecated and included only because the service still serves it — use v2 for new work.
- Ruby 3.1+.
The
onetimecommand-line tool has moved to a separateonetime-cligem so that this library stays dependency-free.
Installation
gem install onetime
Or in a Gemfile:
gem "onetime", "~> 0.6" # the constraint matters, see below
Maintainers: see docs/releasing.md.
Quick start
require "onetime"
client = Onetime::Client.new(
base_url: "https://ca.onetimesecret.com", # choose your region's API host (required)
customer: "ur1abc23defghijklmnop", # customer extid (see below)
api_token: ENV["ONETIME_API_TOKEN"], # API token from your account page
api_version: :v2, # :v1 or :v2 (default)
)
# Conceal a secret you already have
res = client.secrets.conceal(secret: "hunter2", ttl: 3600, passphrase: "pw12")
res.dig("record", "receipt", "identifier") # the receipt (creator) key
res.dig("record", "secret", "identifier") # the secret (recipient) key
# Generate a random secret server-side
client.secrets.generate(ttl: 86_400)
# Reveal (consume) a secret — one-time only
secret = client.secrets.reveal("abc123secretkey", passphrase: "pw12")
secret.dig("record", "secret_value")
# Service status (works on v1 and v2)
client.status
Authentication
Authentication uses HTTP Basic, where the customer extid is the username and your API token is the password.
The customer extid is a short, opaque identifier that begins with ur — for
example ur1abc23def. It is shown, with a copy button, at the bottom of the
user menu when you are signed in. Other identifiers the API uses are not
interchangeable with it.
The format is checked at construction, so a value of the wrong kind fails immediately rather than at the first request:
Onetime::Client.new(base_url: "https://ca.onetimesecret.com",
customer: "acct-123", api_token: ENV["ONETIME_API_TOKEN"])
# => Onetime::ConfigurationError: customer "acct-123" is not a customer extid:
# extids begin with "ur" (e.g. "ur1abc23def"). ...
Self-hosted servers older than onetimesecret#3945 silently accept invalid credentials and create the secret anonymously instead of returning 401 — the call succeeds, but the secret never appears in your account. Current servers return 401.
Base URL (required)
base_url must be the API host for your region, your self-hosted domain, or
your custom domain. There is no default: deployments are region-isolated for
data sovereignty, so the client cannot guess one for you.
Regional API hosts:
| Region | Host |
|---|---|
| United States | https://us.onetimesecret.com |
| Europe | https://eu.onetimesecret.com |
| United Kingdom | https://uk.onetimesecret.com |
| Canada | https://ca.onetimesecret.com |
| Aotearoa New Zealand | https://nz.onetimesecret.com |
The apex
onetimesecret.comis the company website, not an API host, and is rejected with a helpful error.
Configuration
| Option | Default | Notes |
|---|---|---|
base_url |
— (required) | Region/self-hosted/custom domain |
api_version |
:v2 |
:v1 or :v2 |
customer |
ENV["ONETIME_CUSTOMER_EXTID"] |
Customer extid (ur...) |
api_token |
ENV["ONETIME_API_TOKEN"] |
API token |
timeout |
30 |
Read timeout (seconds) |
open_timeout |
10 |
Connect timeout (seconds) |
max_retries |
2 |
Retries for idempotent (GET) requests |
logger |
nil |
Receives per-request debug lines |
Environment fallbacks: base_url ← ONETIME_BASE_URL; customer ←
ONETIME_CUSTOMER_EXTID; api_token ← ONETIME_API_TOKEN.
Clients are thread-safe: they hold only configuration and a stateless transport, opening a fresh connection per request.
Anonymous and guest usage
A client created without credentials is anonymous and can use public and guest endpoints:
guest = Onetime::Client.new(base_url: "https://ca.onetimesecret.com") # no credentials
guest.secrets.conceal(secret: "no account needed", guest: true)
Resources
client.secrets
| Method | v1 | v2 |
|---|---|---|
conceal(secret:, ttl:, passphrase:, recipient:, share_domain:) |
yes | yes |
generate(ttl:, passphrase:, recipient:, share_domain:) |
yes | yes |
reveal(key, passphrase:, continue:) |
yes | yes |
show(key) |
— | yes |
status(key) |
— | yes |
status_list(keys) |
— | yes |
conceal is also available as share. reveal/show accept either a bare
key or a full secret URL.
client.receipts
| Method | v1 | v2 |
|---|---|---|
show(key) |
yes | yes |
recent |
yes | yes |
burn(key, passphrase:, continue:) |
yes | yes |
update(key, memo:) |
— | yes |
Meta
client.status (v1 & v2), client.version / client.supported_locales
(v2 only), client.authcheck (v1 only).
Responses
Resource methods return an Onetime::Response with indifferent (String or
Symbol) key access:
res = client.secrets.conceal(secret: "hi")
res["record"] # Hash
res.dig(:record, :secret, :secret_value) # deep access
res.success? # 2xx?
res.http_status # Integer
res.to_h # the parsed body
The response shape is the API's, unchanged. v1 and v2 differ on purpose —
v1 returns flat, all-string fields; v2 nests data under record/details
with richer typing — and the client does not reconcile them. Read the
API docs for the shape of the version
you target.
Errors
HTTP errors (status >= 400) raise typed exceptions following the API's
ADR-013 error contract ({ error:, error_type:, ... }):
begin
client.secrets.reveal("missing")
rescue Onetime::NotFoundError => e
e. # human-readable message ("error" field)
e.error_type # machine-readable type ("RecordNotFound")
e.http_status # 404
rescue Onetime::RateLimitError => e
e.retry_after
rescue Onetime::APIError => e
# any other API error
end
| Exception | When |
|---|---|
Onetime::BadRequestError |
400 / FormError (see #field) |
Onetime::AuthenticationError |
401 |
Onetime::AccountRequiredError |
the operation needs an authenticated account (requires_account); subclasses AuthenticationError, keeps #field |
Onetime::ForbiddenError |
403 / Forbidden, GuestRoutesDisabled |
Onetime::EntitlementError |
EntitlementRequired (see #entitlement) |
Onetime::NotFoundError |
404 / RecordNotFound |
Onetime::RateLimitError |
429 / LimitExceeded (see #retry_after) |
Onetime::ServerError |
5xx |
Onetime::TransportError / TimeoutError |
network failures |
Onetime::ConfigurationError |
bad base_url, malformed customer extid, incomplete credentials (raised at construction) |
All inherit from Onetime::Error.
AccountRequiredError exists because the service reports "this needs an
account" inside a 400 form-error body (field: "requires_account"). Mapping
that to BadRequestError sent people auditing their request payload for a
problem that was never there, so it gets its own class under
AuthenticationError:
begin
client.secrets.conceal(secret: "hi")
rescue Onetime::AccountRequiredError => e
e.field # "requires_account" — preserved
e.http_status # whatever the server sent (400 today)
end
Development
bundle install
rake test
License
See LICENSE.txt.