EnfinitOS Brand SDK — Ruby
enfinitos_brand — the read-only Ruby gem a brand (advertiser) uses to query its own delivery proof, metering, and settlement records directly from the EnfinitOS platform, without going through the operator's reporting plane.
Mirrors @enfinitos/sdk-brand (TypeScript), enfinitos_brand (Python), enfinitos-brand-go (Go), and com.enfinitos:sdk-brand (Java) one-for-one. When one moves, this gem moves with it.
Who should use it
A brand engineering team that wants to:
- pull a Merkle-rooted signed proof of every billable delivery and verify it against the Auditor gem;
- reconcile its own attribution numbers against the platform's metered usage before paying the invoice;
- iterate settlement invoices and lines for finance/AP;
- open a dispute (with signed counter-evidence) when its own auditor disagrees with what was billed.
The SDK is scoped read-only to campaigns the calling brand owns. The single write operation — disputes.open — is idempotent and bound to the brand's auditor key.
Authentication
Every request carries:
Authorization: Bearer <brand_api_key>— issued by the platform to a single brand tenant; rotatable; read-only on owned campaigns plus dispute-open;X-Enfinitos-Brand: <brand_id>— the brand's tenant id; allows the platform WAF to rate-limit per-tenant before auth decode.
The platform rejects any mismatch between the key's owner and the header.
Installation
This gem is currently distributed inside the EnfinitOS monorepo. Add to your Gemfile:
gem "enfinitos_brand", "~> 0.0.1", path: "vendor/enfinitos_brand"
Requires Ruby 3.2+. Zero third-party runtime dependencies.
Getting started
require "enfinitos_brand"
client = EnfinitOS::Brand::Client.new(
api_base_url: "https://api.enfinitos.com",
brand_id: "brand_acme_co",
api_key: ENV.fetch("ENFINITOS_BRAND_API_KEY")
)
# 1. List my campaigns.
page = client.campaigns.list
page.items.each do |c|
puts "#{c.campaign_id} [#{c.status}] billed=#{c.total_billed_minor}"
end
# 2. Fetch a signed proof pack for one campaign.
pack = client.proof.pack(page.items.first.campaign_id)
# 3. Verify against the Auditor gem (separate gem —
# enfinitos_auditor — performs Merkle + signature verification
# offline, with no network round-trip).
#
# EnfinitOS::Auditor::Client.new(...).verify_pack(pack)
Module reference
client.campaigns
list(status:, cursor:, limit:)— cursor-paginated list of owned campaigns. All keyword args optional.get(campaign_id)— single campaign.
client.proof
summary(campaign_id)— cheap Merkle-root rollup (merkle_root,record_count, signer info).pack(campaign_id)— full signedSignedProofPack; pass to the Auditor gem for offline verification.chain(campaign_id, cursor:, limit:)— cursor-paginated per-leaf records.
For verification use pack — the chain endpoint is for inspection and incremental reconciliation only.
client.metering
summary(campaign_id)— per-unit totals.breakdown(campaign_id, unit:)— per-day, per-substrate rollup of a single billable unit ("impressions","plays","seconds","meters","interactions","verified_views").
client.settlement
invoices(from:, to:)— invoices issued to the brand; optional inclusive(from, to)window onissued_at. AcceptsTimeor ISO-8601 strings.invoice(invoice_id)— single invoice with lines.line(line_id)— single invoice line; carries theproof_slice_rootthat pins it to the corresponding Merkle subtree.
client.disputes
open(campaign_id:, reason:, evidence:, idempotency_key:)— open a dispute. The gem auto-generates a UUID v4 idempotency key when one isn't supplied; cron re-runs are safe.list(status:)— list the brand's disputes.get(dispute_id)— single dispute.
The dispute body's free-form reason is informational; the operator's response is bound to the SignedEvidence only.
Error model
The gem raises three exception families, all under EnfinitOS::Brand:
APIError— the platform answered with a non-2xx status or a 2xx envelope carryingok: false. Carriescode,http_status,correlation_id,details, plus the helpersclient_error?andretryable?.TransportError— connection-level failure (DNS, timeout, TLS, refused).Error— base class; rarely raised directly.
Typical pattern:
begin
client.campaigns.get(id)
rescue EnfinitOS::Brand::APIError => e
if e.retryable?
# 408 / 429 / 5xx — defer to your worker's backoff.
elsif e.code == "CAMPAIGN_NOT_FOUND"
# tenant-bound 404 — the campaign either doesn't exist
# or isn't owned by this brand. The platform deliberately
# does NOT distinguish the two.
else
raise
end
end
The SDK does not retry by default. Brand-side systems sit downstream of the brand's own retry middleware (Sidekiq, Faktory, shoryuken, ...); doubling them up has caused duplicate-dispute filings in the past. Opt in by checking retryable? and re-issuing yourself.
Cross-reference
- Auditor gem (
enfinitos_auditor) — offline verification of signed proof packs returned byproof.pack. Pure crypto; no network. - Operator Reporting — the platform's operator-facing report surface answers the same questions from the operator's point of view; brands deliberately do NOT consume it directly.
- REST contract —
apps/apiis the source of truth; the SDK pins shapes viaData.definetypes and is regenerated when contracts move.
Wire conventions
Every request also includes:
X-Enfinitos-Sdk: brand-rbX-Enfinitos-Sdk-Version: 0.0.1X-Enfinitos-Contract: 1User-Agent: enfinitos-sdk-brand-rb/0.0.1 [(tag)]Idempotency-Key: <uuid>onPOST /v1/brand/disputes(auto-generated by the SDK when the caller omits one).
The platform's response envelope is:
{ "ok": true, "data": { ... }, "contractVersion": 1 }
or
{ "ok": false, "error": { "code": "...", "message": "...", "correlationId": "..." } }
The SDK unwraps both and surfaces only data or the typed APIError. The X-Contract-Version response header is captured on errors for drift-monitoring tooling.
Tests
bundle exec rspec
Tests use the injectable :transport keyword on Client.new; no real HTTP server is required.