# Coffrify Elixir SDK
[](https://hex.pm/packages/coffrify)
[](https://hexdocs.pm/coffrify)
[](LICENSE)
Official Elixir client for [Coffrify](https://coffrify.com) — 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 `:telemetry` events and optional OpenTelemetry binding.
- Drop-in `Coffrify.Plug.VerifyWebhook` and
`Coffrify.Phoenix.WebhookController` mixin.
- Test helpers: payload signing, fixture builders, Bypass-friendly.
## Installation
Add `:coffrify` to your `mix.exs`:
```elixir
def deps do
[
{:coffrify, "~> 0.9"}
]
end
```
Requires Elixir `~> 1.15` and OTP 26+.
## Quickstart
```elixir
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:
```elixir
client
|> Coffrify.Resources.Transfers.iterate(page_size: 100, status: "active")
|> Stream.take(500)
|> Enum.each(&IO.inspect/1)
```
## Webhook verification (plain Plug)
```elixir
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)
```elixir
# 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
```elixir
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
```elixir
{: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
```elixir
{: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)
```elixir
{: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
```elixir
{: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
```elixir
# Requires :opentelemetry_api and :opentelemetry in your deps
Coffrify.Runtime.Telemetry.attach_opentelemetry()
```
## Testing
```elixir
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 `:query` opts 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`](mix.exs).
## Error handling
```elixir
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](LICENSE)