Coffrify Elixir SDK

Hex.pmHex DocsLicense: MIT

Official Elixir client for Coffrify — encrypted file-transfer infrastructure. This SDK mirrors the JavaScript SDK (@coffrify/sdk v0.9.0) feature-for-feature.

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:

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:

EventMeasurementsMetadata
[:coffrify, :request, :start]system_timemethod, url, attempt
[:coffrify, :request, :stop]durationmethod, url, status, attempt, result
[:coffrify, :request, :exception]durationmethod, url, kind, reason, stacktrace
[:coffrify, :request, :retry]delay_msmethod, 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:

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

License

MIT