Coffrify Elixir SDK
Official Elixir client for Coffrify — encrypted
file-transfer infrastructure. This SDK mirrors the JavaScript SDK
(@coffrify/sdk v0.9.0) feature-for-feature.
- 35 resources covering the entire Coffrify API surface (transfers, webhooks, API keys, audit, analytics, branding, domains, folders, collections, members, notifications, GDPR, sessions, downloads, alerts, delegated tokens, templates, request inboxes, coffres, magic links, quotas, billing, status, changelog, recipients, rooms, MFA, marketing, SSO, SCIM, webhook extras, workspace extras).
- Standard Webhooks v2 verification (
webhook-id/webhook-timestamp/webhook-signature) with key-rotation support. - Pluggable retry policies (exponential backoff, decorrelated jitter,
fibonacci backoff, fixed delay) honoring
Retry-After. - Local circuit breaker, client-side rate limiter (token / leaky bucket), Idempotency-Key auto-generation, crash-safe idempotency store (memory or Redis).
- First-class
:telemetryevents and optional OpenTelemetry binding. - Drop-in
Coffrify.Plug.VerifyWebhookandCoffrify.Phoenix.WebhookControllermixin. - Test helpers: payload signing, fixture builders, Bypass-friendly.
Installation
Add :coffrify to your mix.exs:
def deps do
[
{:coffrify, "~> 0.9"}
]
end
Requires Elixir ~> 1.15 and OTP 26+.
Quickstart
client =
Coffrify.new(
api_key: System.fetch_env!("COFFRIFY_API_KEY"),
timeout_ms: 30_000
)
# List recent transfers
{:ok, page} = Coffrify.Resources.Transfers.list(client, limit: 20)
# Create a webhook subscription — STORE the returned secret immediately
{:ok, %{"webhook" => wh, "secret" => secret}} =
Coffrify.Resources.Webhooks.create(client, %{
name: "Production",
url: "https://api.example.com/hooks/coffrify",
events: ["transfer.created", "transfer.downloaded"]
})
IO.puts("Save this secret in your secret manager: #{secret}")
Stream every transfer lazily:
client
|> Coffrify.Resources.Transfers.iterate(page_size: 100, status: "active")
|> Stream.take(500)
|> Enum.each(&IO.inspect/1)
Webhook verification (plain Plug)
defmodule MyApp.Router do
use Plug.Router
plug :match
plug Plug.Parsers,
parsers: [:json],
body_reader: {Coffrify.Plug.VerifyWebhook, :cache_raw_body, []},
json_decoder: Jason
plug Coffrify.Plug.VerifyWebhook,
secret: {System, :fetch_env!, ["COFFRIFY_WEBHOOK_SECRET"]}
plug :dispatch
post "/hooks/coffrify" do
event = conn.assigns.coffrify_event
MyApp.Webhooks.handle(event)
send_resp(conn, 200, "ok")
end
end
Webhook verification (Phoenix)
# router.ex
pipeline :coffrify_webhook do
plug :accepts, ["json"]
plug Coffrify.Plug.VerifyWebhook,
secret: {System, :fetch_env!, ["COFFRIFY_WEBHOOK_SECRET"]},
replay_store: MyApp.CoffrifyReplay
end
scope "/integrations" do
pipe_through :coffrify_webhook
post "/coffrify", MyAppWeb.CoffrifyWebhookController, :handle
end
# controller.ex
defmodule MyAppWeb.CoffrifyWebhookController do
use Coffrify.Phoenix.WebhookController
@impl Coffrify.Phoenix.WebhookController
def handle_event(%{"type" => "transfer.created"} = event, _conn) do
MyApp.Analytics.log(event)
:ok
end
def handle_event(%{"type" => "ping"}, _conn), do: :ok
def handle_event(_event, _conn), do: :ignore
end
The handle_event/2 callback returns:
:ok/{:ok, _}→ HTTP 200:ignore→ HTTP 202 (no retry){:error, _}→ HTTP 500 (Coffrify will retry per its backoff schedule)
Runtime utilities
Custom retry policy
policy = Coffrify.Runtime.Retry.DecorrelatedJitter.new(
max_attempts: 5,
base_delay_ms: 200,
max_delay_ms: 10_000
)
client = Coffrify.new(api_key: key, retry_policy: policy)
Circuit breaker
{:ok, breaker} =
Coffrify.Runtime.CircuitBreaker.start_link(
name: MyApp.CoffrifyBreaker,
failure_threshold: 5,
open_ms: 30_000
)
client = Coffrify.new(api_key: key, circuit_breaker: breaker)
Rate limiter
{:ok, limiter} =
Coffrify.Runtime.RateLimit.TokenBucket.start_link(
name: MyApp.CoffrifyLimiter,
capacity: 20,
refill_per_second: 10
)
client =
Coffrify.new(
api_key: key,
rate_limiter: {Coffrify.Runtime.RateLimit.TokenBucket, limiter}
)
Idempotency store (crash-safe)
{:ok, store} = Coffrify.Runtime.Idempotency.Memory.start_link(name: MyApp.CoffrifyIdem)
client = Coffrify.new(api_key: key, idempotency_store: {Coffrify.Runtime.Idempotency.Memory, store})
# Redis (uses Redix)
{:ok, redis} = Redix.start_link("redis://localhost:6379")
store = Coffrify.Runtime.Idempotency.Redis.new(conn: redis)
client = Coffrify.new(api_key: key, idempotency_store: store)
Webhook replay protection
{:ok, replay} = Coffrify.Runtime.WebhookReplay.Memory.start_link(name: MyApp.CoffrifyReplay)
# Pass to Coffrify.Plug.VerifyWebhook via :replay_store
Telemetry
The SDK emits these events:
| Event | Measurements | Metadata |
|---|---|---|
[:coffrify, :request, :start] | system_time | method, url, attempt |
[:coffrify, :request, :stop] | duration | method, url, status, attempt, result |
[:coffrify, :request, :exception] | duration | method, url, kind, reason, stacktrace |
[:coffrify, :request, :retry] | delay_ms | method, url, attempt, reason |
[:coffrify, :webhook, :verified] | %{} | event_type, event_id |
[:coffrify, :webhook, :rejected] | %{} | reason, event_type |
OpenTelemetry
# Requires :opentelemetry_api and :opentelemetry in your deps
Coffrify.Runtime.Telemetry.attach_opentelemetry()
Testing
defmodule MyApp.WebhookTest do
use ExUnit.Case, async: true
alias Coffrify.Testing
alias Coffrify.Testing.Fixtures
test "valid signature" do
event = Fixtures.webhook_event("transfer.created", %{"transfer" => Fixtures.transfer()})
body = Jason.encode!(event)
{body, headers} = Testing.sign_payload_test("whsec_test_secret_123", body)
assert {:ok, decoded} = Coffrify.Webhook.Verification.verify(
"whsec_test_secret_123",
body,
headers
)
assert decoded["type"] == "transfer.created"
end
end
Resource reference
Every resource module follows the same conventions:
list/2,get/3— read endpoints (kw-list of:queryopts on lists).create/2,update/3,delete/3— mutations.- Functions take a
%Coffrify{}client as the first argument. - Return
{:ok, body}or{:error, %Coffrify.Error{}}.
The full module list lives in mix.exs.
Error handling
case Coffrify.Resources.Transfers.get(client, transfer_id) do
{:ok, transfer} -> transfer
{:error, %Coffrify.Error.NotFound{}} -> :gone
{:error, %Coffrify.Error.RateLimited{retry_after_ms: ms}} -> Process.sleep(ms)
{:error, %Coffrify.Error.Transport{}} -> :network_blip
{:error, %Coffrify.Error{status: status, message: m}} -> Logger.error(m, status: status)
end