The official Ruby SDK for INBIO, the premium URL shortener with click analytics and customizable QR codes. Shorten links, generate styled QR codes, read analytics, and manage folders and tags from Ruby.
- Free to start —
Inbio.shortenand the QR API need no account and no API key - Zero runtime dependencies — stdlib
net/httpandopensslonly, Ruby ≥ 3.0 - Complete — covers the entire INBIO REST API: links CRUD, lazy enumerator pagination, bulk create, QR codes, analytics, webhook signature verification
gem install inbioOr in your Gemfile:
gem "inbio"require "inbio"
puts Inbio.shorten("https://example.com/very/long/url").short_url
# => "https://in.bio/abc123"The free endpoint is keyless and rate-limited (5/min per IP). The result also
carries slug, qr_url, preview_url, claim_url (turn the anonymous link into
a permanent one on your account) and expires — anonymous links are deleted after
30 days unless claimed.
Create a token under Settings → API tokens (API access requires Pro or Business).
require "inbio"
client = Inbio::Client.new(token: "YOUR_API_TOKEN")
link = client.links.create("https://example.com/sale", slug: "spring-sale")
link.short_url # => "https://in.bio/spring-sale"
link.total_clicks # => 0With no token: argument the client reads ENV["INBIO_API_TOKEN"]:
client = Inbio::Client.new # uses INBIO_API_TOKENlist returns one page; iterate returns a lazy Enumerator that paginates
through everything for you:
page = client.links.list(status: "active", per_page: 50)
page.total # => 57
page.current_page # => 1
page.next? # => true
page.each { |link| puts link.short_url }
# Every link, across all pages, fetched on demand:
client.links.iterate(tag: "marketing").each do |link|
puts "#{link.slug}: #{link.total_clicks} clicks"
end
client.links.iterate(search: "sale").first(10) # stops fetching after 10Filters: search:, folder_id:, tag:, status: (active, disabled,
archived, expired, exhausted, blocked, pending_review), per_page: (1–100).
list_all is an alias for iterate.
Only the destination URL is required:
link = client.links.create(
"https://example.com/launch",
slug: "launch", # 4–64 chars [a-zA-Z0-9-_]; omitted => random
title: "Product launch",
description: "Fall campaign",
notes: "Internal note",
redirect_type: 301, # 301, 302 (default) or 307
folder_id: 3,
tags: ["marketing", "fall"], # created on the fly
expires_at: "2026-12-31T23:59:59Z", # Pro+
fallback_url: "https://example.com/expired", # Pro+
click_limit: 10_000, # Pro+
password: "s3cret", # Pro+
utm: { source: "newsletter", medium: "email", campaign: "fall" }
)link = client.links.get(42)
link = client.links.update(42, title: "New title", tags: ["archive"])
# Editing destination_url requires the edit-destination feature (Pro+).
client.links.disable(42) # active -> disabled
client.links.enable(42) # disabled -> active
# Any other transition raises Inbio::StateConflictError (error.current has the status).
client.links.delete(42) # => nil (204); the link stops redirectingUp to 100 links per call; rows fail independently:
result = client.links.bulk_create([
{ destination_url: "https://example.com/a", slug: "promo-a" },
{ destination_url: "https://example.com/b", slug: "promo-b" }
])
result.created.each { |link| puts link.short_url }
result.failed.each { |row| warn "row #{row['index']}: #{row['error']}" }qr returns raw image bytes — write them straight to disk:
File.binwrite("spring-sale.png", client.links.qr(42))
File.binwrite("spring-sale.svg", client.links.qr(42, format: "svg", size: 1024))The QR encodes the short URL, so editing the destination never invalidates printed codes.
stats = client.links.analytics(42, from: "2026-06-01", to: "2026-06-30")
stats.totals # => {"clicks"=>1240, "uniques"=>981, "botClicks"=>77}
stats.series # => [{"date"=>"2026-06-01", "clicks"=>40, "uniques"=>31}, ...]
stats.countries # top 10: [{"value"=>"US", "clicks"=>512}, ...]
stats.devices, stats.browsers, stats.referrers # same shapeBot traffic is excluded everywhere except totals["botClicks"].
client.folders.list.each { |f| puts "#{f.name} (#{f.links_count} links)" }
client.tags.list.each { |t| puts "#{t.name} (#{t.links_count} links)" }usage = client.account.usage
usage.plan # => "pro"
usage.usage["links_created"] # => 120
usage.limits["links_per_month"] # => 2000in.bio signs every delivery with X-Inbio-Signature: t=<unix>,v1=<hex> where
v1 = HMAC-SHA256(secret, "<t>.<raw body>"). Verify with the exact raw request
body (before parsing) — the comparison is constant-time and deliveries older
than tolerance (default 300s) are rejected to prevent replays.
Rails:
class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
def inbio
event = Inbio::Webhooks.verify(
request.raw_post,
request.headers["X-Inbio-Signature"],
ENV.fetch("INBIO_WEBHOOK_SECRET")
)
case event.event
when "link.clicked" then Metrics.count(event.data["slug"])
when "link.click_limit_reached" then AlertMailer.limit(event.data).deliver_later
end
head :ok
rescue Inbio::SignatureVerificationError
head :bad_request
end
endRack/Sinatra:
post "/webhooks/inbio" do
event = Inbio::Webhooks.verify(
request.body.read,
request.env["HTTP_X_INBIO_SIGNATURE"],
ENV.fetch("INBIO_WEBHOOK_SECRET")
)
# handle event.event / event.data ...
200
rescue Inbio::SignatureVerificationError
400
endInbio::Webhooks.construct_event is an alias; client.webhooks.verify(...)
works too. No HTTP is involved — it is a pure crypto helper.
Every error is a subclass of Inbio::Error, which exposes message, status,
error_type and the parsed response body.
| HTTP | Error | Extras |
|---|---|---|
| 401 | Inbio::AuthenticationError |
|
| 403 | Inbio::AccessError |
error_type ("plan", "scope", "account"), required_scope |
| 404 | Inbio::NotFoundError |
also returned for links you don't own |
| 409 | Inbio::StateConflictError |
current — the link's current status |
| 422 | Inbio::ValidationError |
errors — field name → array of messages |
422 (error.type=entitlement) |
Inbio::EntitlementError |
plan feature/limit hit |
| 429 | Inbio::RateLimitError |
retry_after (seconds) |
| 5xx | Inbio::ServerError |
|
| — | Inbio::SignatureVerificationError |
webhook verification only |
begin
client.links.create("https://example.com", slug: "x")
rescue Inbio::ValidationError => e
e.errors # => {"slug"=>["The slug must be at least 3 characters."]}
rescue Inbio::AccessError => e
e.required_scope # => "links:write" when a token scope is missing
rescue Inbio::RateLimitError => e
sleep e.retry_after
retry
endclient = Inbio::Client.new(
token: "...",
base_url: "https://in.bio", # default
timeout: 30, # seconds, default 30
max_retries: 2 # default 2
)Retries apply only to idempotent GET requests (on connection errors and 5xx) and
to 429 responses that carry Retry-After — with exponential backoff capped at
10 seconds. Everything else fails fast.
INBIO (in.bio) is a URL shortener and link-management
platform: short links with custom slugs, real-time click analytics
(countries, devices, browsers, referrers — bots filtered out), a QR code
studio with dot styles, marker shapes and colors, folders, tags, UTM
tools, and a REST API with webhooks. Free plan included.
- Website: https://in.bio
- Documentation: https://docs.in.bio
- Free shorten API (no key): https://docs.in.bio/api/free-shorten
- Free QR code API (no key): https://docs.in.bio/api/free-qr
- MCP server for AI agents: https://docs.in.bio/api/mcp
- All SDKs (JavaScript, Python, PHP, Ruby, Go): https://docs.in.bio/sdks
MIT © InBio, Inc.