Getting started

Errors

Every failure is a typed exception carrying the stable machine-readable code from the API. Branch on the code, never on the message.

Every exception the SDK raises for something that went wrong at runtime inherits Envless::Error, a StandardError, so one rescue Envless::Error catches all of them. A mistake in the call itself, such as an empty slug, a limit: that is not an Integer or binary data passed as plaintext, raises ArgumentError instead, before anything is sent.

ExceptionRaised when
Envless::ApiErrorThe API answered and the answer was a failure. Carries status, code and the request id. Also raised before sending when a variable name is unusable.
Envless::NetworkErrorNothing came back to classify: connection refused, DNS failure, a TLS error, or the timeout passing.
Envless::DecryptionErrorA value cannot be decrypted with the passphrase or key you passed, or is not an Envless envelope at all.
Envless::WebhookVerificationErrorA webhook delivery fails signature, timestamp or payload checks in verify_webhook_signature.
ruby
require "envless/sdk"

begin
  Envless.projects.create(name: "Billing", slug: "billing")
rescue Envless::ApiError => error
  if error.needs_upgrade?
    puts "Upgrade needed: #{error.message}"
  elsif error.scope_missing?
    puts "Key needs: #{error.resource}"
  elsif error.conflict?
    puts "Slug already taken"
  else
    puts "#{error.code} #{error.request_id}"
  end
rescue Envless::NetworkError => error
  puts "Could not reach the API: #{error.message}"
  puts "Timed out" if error.timeout?
end

Never branch on error.message. It is written for humans and can be reworded at any time. error.code is a stable contract.

An Envless::ApiError nobody rescues prints its status, its code and the request id next to the message, so a crashed Rake task or job log already carries what support needs.

Envless::ApiError

AttributeTypeNotes
statusInteger or nilThe HTTP status. A variable name refused before sending carries 400, the status the API would answer with.
codeStringThe stable code, such as variable.ciphertext_invalid. Always present, falling back to server.error.
messageStringThe API's human readable message.
resourceString or nilWhat the failure relates to. On a scope failure this is the exact scope needed, such as variables:write.
fieldString or nilThe request field that caused it.
request_idString or nilRead from the response's _req.reqId, or its Request-Id header. nil on a refusal made before anything was sent. This is what support needs. Log it.
retry_after_secondsNumeric or nilFrom the Retry-After header, on a 429 or a transient 5xx.

Predicates

MethodTrue when
auth?code starts with auth.
scope_missing?code is exactly auth.scope_missing
validation?status is 400
needs_upgrade?status is 402
not_found?status is 404
conflict?status is 409
rate_limited?status is 429

They cover the branches most callers need. For anything finer, match on code, which reads well in a case:

ruby
require "envless/sdk"

begin
  Envless.variables.get("api", "production", "STRIPE_SECRET_KEY")
rescue Envless::ApiError => error
  case error.code
  when "auth.scope_missing"
    warn "Mint a key with #{error.resource}"
  when "project.ambiguous_product"
    warn "Pass product: to pick the project"
  else
    raise
  end
end

Envless::NetworkError

AttributeTypeNotes
messageStringWhat went wrong, without the token or the request body.
timeout?true or falsetrue when the timeout passed before the response finished.
causeException or nilThe underlying Net::HTTP, socket or OpenSSL error, as Ruby chains it.

The codes worth handling

Full tables live on each endpoint page. These are the ones that come up when you drive the API from a script.

CodeStatusWhat to do
auth.api_key_not_found401The key is invalid, inactive or revoked. Stop, do not retry.
auth.scope_missing403Mint a key with error.resource.
auth.scope_ceiling_exceeded403You asked for scopes the minting key does not hold.
auth.key_project_restricted403The key is fenced to other projects.
auth.product_restricted403The key owner has no grant on a private product.
request.body_invalid400Fix the payload. error.field names the offender.
request.invalid_pagination400limit outside 1 to 100.
variable.ciphertext_invalid400You sent plaintext. See Encryption.
variable.baseline_drifted409Somebody edited since you read the baseline. Re-read and retry.
project.ambiguous_product409Pass product:.
plan.upgrade_required402A tier limit or a paid feature, such as an older version or a rollback on Free. An upgrade fixes it. Not retryable.
plan.limit_reached409The 1,000-variables-per-environment fair-use cap. No plan raises it; contact support. Not retryable.

Retrying safely

The client already retries transient failures on idempotent methods. What it deliberately does not do is retry a POST or a PATCH, because a create that timed out may have landed. If you want to retry a write, make it safe first.

For variables.apply the baseline makes this genuinely safe: a retry either applies cleanly or fails with variable.baseline_drifted and writes nothing, so the loop below re-reads the environment and tries again.

ruby
require "envless/sdk"

3.times do |attempt|
  current = Envless.variables.list_all("api", "production")
  baseline = current.map do |variable|
    {
      uniqueId: variable[:id],
      name: variable[:name],
      updatedAt: variable[:updatedAt] || variable[:createdAt]
    }
  end
  deletes = current.filter_map do |variable|
    next unless variable[:name] == "LEGACY_API_URL"

    {uniqueId: variable[:id]}
  end

  Envless.variables.apply("api", "production",
    baseline:, deletes:)
  break
rescue Envless::ApiError => error
  drifted = error.code == "variable.baseline_drifted"
  raise unless drifted && attempt < 2
end

Logging

Log the code and the request id, never the payload. Variable values are ciphertext, but names, member emails and workspace slugs are not.

ruby
require "logger"
require "envless/sdk"

logger = Logger.new($stderr)

begin
  Envless.variables.delete("api", "production", "LEGACY_API_URL")
rescue Envless::ApiError => error
  logger.error(
    "envless request failed code=#{error.code} " \
    "status=#{error.status} resource=#{error.resource} " \
    "field=#{error.field} request_id=#{error.request_id}"
  )
  raise
end