Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/components/cards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,18 @@ export function RustSdkCard() {
);
}

export function ElixirSdkCard() {
return (
<Card
description="MPP SDK for Elixir — Plug middleware for Phoenix and Plug apps"
icon="simple-icons:elixir"
title="Elixir"
to="/sdk/elixir"
topRight={<Badge variant="tip">Community</Badge>}
/>
);
}

export function MppxCreateReferenceCard({ to }: { to: string }) {
return (
<Card
Expand Down
89 changes: 89 additions & 0 deletions src/pages/sdk/elixir/core.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
imageDescription: "Challenge, Credential, and Receipt types for working with the MPP protocol in Elixir"
---

# Core types [Challenge, Credential, and Receipt modules]

These modules map directly to HTTP headers — `WWW-Authenticate`, `Authorization`, and `Payment-Receipt` — and can be used independently of the `MPP.Plug` middleware.

## Challenge

Create an HMAC-bound challenge and format it as a `WWW-Authenticate` header:

```elixir
alias MPP.{Challenge, Headers}

challenge = Challenge.create(
secret_key: "your-hmac-secret",
realm: "api.example.com",
method: "stripe",
intent: "charge",
request_b64: Base.url_encode64(Jason.encode!(%{amount: "1000", currency: "usd"}), padding: false)
)

header = Headers.format_www_authenticate(challenge)
# → "Payment id=\"abc...\", realm=\"api.example.com\", ..."
```

Verify a challenge ID on the retry request:

```elixir
{:ok, true} = Challenge.verify(challenge, "your-hmac-secret")
```

## Credential

Parse an `Authorization: Payment` header into a credential struct:

```elixir
alias MPP.{Credential, Headers}

{:ok, credential} = Headers.parse_authorization(auth_header)

# Access the echoed challenge fields
credential.challenge_id
credential.method
credential.intent

# Access the payment payload
credential.payload
```

## Receipt

Build a receipt and format it as a `Payment-Receipt` header:

```elixir
alias MPP.{Receipt, Headers}

receipt = %Receipt{
status: "success",
challenge_id: challenge.id,
method: "stripe",
reference: "pi_abc123"
}

header = Headers.format_receipt(receipt)
# → base64url-encoded JSON
```

## Headers

The `MPP.Headers` module handles all header parsing and formatting:

| Function | Description |
|----------|-------------|
| `format_www_authenticate/1` | Serialize a Challenge to a `WWW-Authenticate` header value |
| `format_receipt/1` | Serialize a Receipt to a `Payment-Receipt` header value |
| `parse_authorization/1` | Parse an `Authorization` header into a Credential |

## Type reference

| Module | Description |
|--------|-------------|
| `MPP.Challenge` | Server challenge with HMAC-SHA256 ID binding |
| `MPP.Credential` | Client credential parsed from `Authorization` header |
| `MPP.Receipt` | Server receipt for `Payment-Receipt` header |
| `MPP.Headers` | Header parsing and formatting utilities |
| `MPP.Intents.Charge` | Charge intent request schema (amount, currency, recipient) |
| `MPP.Errors` | RFC 9457 Problem Detail error types |
118 changes: 118 additions & 0 deletions src/pages/sdk/elixir/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
title: "Elixir SDK for MPP"
imageDescription: "The mpp Elixir library for adding payment-gated endpoints to Phoenix and Plug applications"
---

import * as SdkBadge from '../../../components/SdkBadge'
import { Callout } from 'vocs'

# Elixir SDK [Plug middleware for Phoenix and Plug apps]

## Overview

The `mpp` Elixir library provides Plug middleware that adds pay-per-call billing to any Phoenix or Plug application. Mount the plug on a route, set a price, and the middleware handles the full 402 challenge/credential/receipt flow.

<div className="flex gap-2">
<SdkBadge.GitHub repo="ZenHive/mpp" />
<SdkBadge.Maintainer name="ZenHive" href="https://github.com/ZenHive" />
</div>

<Callout type="info">
This is a community-maintained SDK. Check [hex.pm/packages/mpp](https://hex.pm/packages/mpp) for the latest version and [hexdocs.pm/mpp](https://hexdocs.pm/mpp) for full API documentation.
</Callout>

## Install

Add `mpp` to your dependencies in `mix.exs`:

```elixir [mix.exs]
defp deps do
[
{:mpp, "~> 0.1"} # check hex.pm for latest version
]
end
```

Then fetch dependencies:

```bash [install.sh]
$ mix deps.get
```

## Requirements

- Elixir 1.17+
- A Plug-compatible application (Phoenix, Bandit, Cowboy)

## Quick start

### Server

Mount `MPP.Plug` in a Phoenix router to gate endpoints behind payment:

```elixir [router.ex]
defmodule MyAppWeb.Router do
use MyAppWeb, :router

pipeline :paid do
plug MPP.Plug,
secret_key: "your-hmac-secret",
realm: "api.example.com",
method: MPP.Methods.Stripe,
method_config: %{
"stripe_secret_key" => System.get_env("STRIPE_SECRET_KEY"),
"network_id" => System.get_env("STRIPE_NETWORK_ID")
},
amount: "1000",
currency: "usd"
end

scope "/api", MyAppWeb do
pipe_through [:api, :paid]
get "/data", DataController, :show
end
end
```

The middleware handles the full flow automatically:

1. Request without `Authorization: Payment` header returns `402` with a `WWW-Authenticate` challenge
2. Client pays off-band (e.g. via Stripe), retries with `Authorization: Payment <credential>`
3. Valid credential passes through with a `Payment-Receipt` header attached
4. Invalid credential returns `402` with a fresh challenge and an RFC 9457 error body

### Per-route pricing

Each route can have its own price — just mount the plug with different options:

```elixir [router.ex]
scope "/api", MyAppWeb do
pipe_through :api

scope "/basic" do
plug MPP.Plug, amount: "100", currency: "usd",
secret_key: secret, realm: realm, method: MPP.Methods.Stripe,
method_config: stripe_config
get "/data", DataController, :show
end

scope "/premium" do
plug MPP.Plug, amount: "5000", currency: "usd",
secret_key: secret, realm: realm, method: MPP.Methods.Stripe,
method_config: stripe_config
get "/report", ReportController, :show
end
end
```

## Design

- **Stateless**: Challenge verification uses HMAC-SHA256 binding — no database or challenge store needed
- **Explicit configuration**: No `Application.get_env` or ENV fallbacks — pass credentials directly via plug opts
- **Pluggable methods**: Implement the `MPP.Method` behaviour to add new payment methods beyond Stripe

## Next steps

- [Core types](/sdk/elixir/core): Challenge, Credential, and Receipt modules
- [Server](/sdk/elixir/server): Plug middleware configuration and usage
- [Full API docs](https://hexdocs.pm/mpp): Complete module reference on HexDocs
146 changes: 146 additions & 0 deletions src/pages/sdk/elixir/server.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
---
imageDescription: "Protect Phoenix and Plug endpoints with payment requirements using MPP middleware"
---

# Server [Plug middleware for payment-gated endpoints]

Mount `MPP.Plug` in any Phoenix or Plug router to protect endpoints with payment. The plug handles the full 402 challenge/credential/receipt flow.

## Quick start

```elixir
plug MPP.Plug,
secret_key: "your-hmac-secret",
realm: "api.example.com",
method: MPP.Methods.Stripe,
method_config: %{stripe_secret_key: "sk_..."},
amount: "1000",
currency: "usd"
```

## Options

### secret_key (required)

- **Type:** `String.t()`

HMAC-SHA256 key for stateless challenge ID binding. The server recomputes the HMAC on verification — no challenge store needed.

### realm (required)

- **Type:** `String.t()`

Server protection space, included in `WWW-Authenticate` headers. Typically your API domain.

### method (required)

- **Type:** module implementing `MPP.Method`

Payment method module for verifying credentials. Built-in: `MPP.Methods.Stripe`.

### amount (required)

- **Type:** `String.t()`

Price in base units (e.g. `"1000"` for $10.00 USD with Stripe).

### currency (required)

- **Type:** `String.t()`

Currency code, normalized to lowercase (e.g. `"usd"`).

### method_config (optional)

- **Type:** `map()`

Server-only configuration passed to `MPP.Method.verify/2` via `charge.method_details`. Use this for secrets like API keys that the payment method needs but clients must not see. Never serialized into challenges.

```elixir
method_config: %{stripe_secret_key: System.get_env("STRIPE_SECRET_KEY")}
```

### recipient (optional)

- **Type:** `String.t()`

Payment recipient identifier included in the charge intent.

### description (optional)

- **Type:** `String.t()`

Human-readable description attached to the challenge.

### expires_in (optional)

- **Type:** `integer()`

Challenge TTL in seconds. Defaults to 300 (5 minutes).

### opaque (optional)

- **Type:** `String.t()`

Base64url-encoded server correlation data included in the challenge.

## Phoenix router example

```elixir
defmodule MyAppWeb.Router do
use MyAppWeb, :router

pipeline :paid do
plug MPP.Plug,
secret_key: Application.compile_env!(:my_app, :mpp_secret_key),
realm: "api.example.com",
method: MPP.Methods.Stripe,
method_config: %{
stripe_secret_key: Application.compile_env!(:my_app, :stripe_secret_key)
},
amount: "1000",
currency: "usd"
end

scope "/api", MyAppWeb do
pipe_through [:api, :paid]
get "/data", DataController, :show
get "/report", ReportController, :show
end
end
```

## Accessing the receipt

After successful payment verification, the receipt is available in `conn.assigns`:

```elixir
def show(conn, _params) do
receipt = conn.assigns[:mpp_receipt]
json(conn, %{data: "paid content", reference: receipt.reference})
end
```

## Custom payment methods

Implement the `MPP.Method` behaviour to add new payment methods:

```elixir
defmodule MyApp.Payments.CustomMethod do
@behaviour MPP.Method

@impl true
def verify(credential, charge) do
# Verify the payment and return a receipt or error
{:ok, %MPP.Receipt{status: "success", reference: "ref_123", ...}}
end
end
```

Then use it in your plug configuration:

```elixir
plug MPP.Plug,
method: MyApp.Payments.CustomMethod,
# ...
```
Loading
Loading