# `X402.Extensions.PaymentIdentifier`
[🔗](https://github.com/cardotrejos/x402/blob/v0.9.0/lib/x402/extensions/payment_identifier.ex#L1)

The x402 `payment-identifier` extension: client-generated idempotency ids.

Implements the
[payment-identifier extension](https://github.com/x402-foundation/x402/blob/main/specs/extensions/payment_identifier.md).
A server advertises the extension in `PaymentRequired.extensions` under
the `"payment-identifier"` key (`extension/1`); the client echoes the
advertisement and adds its id under `info.id` (`enricher/1`):

    # server (PaymentRequired.extensions)
    %{"payment-identifier" => %{"info" => %{"required" => true}, "schema" => %{...}}}

    # client (PaymentPayload.extensions)
    %{"payment-identifier" => %{"info" => %{"required" => true, "id" => "..."}, "schema" => %{...}}}

A valid id is 16 to 128 characters drawn from `[A-Za-z0-9_-]`
(`valid_id?/1`); `generate_id/0` produces one. Servers extract the echoed
id with `extract_id/1` and bind it to the request through `fingerprint/2`:
the same id reused for a different request is a conflict.

## Legacy `paymentIdentifier` format (deprecated)

Releases before 0.7.0 used a `"paymentIdentifier"` key whose value was a
Base64 JSON `{"paymentId": ...}` string, a bare `%{"paymentId" => id}`
map, or an `%{"info" => ...}` envelope around either. `extract_id/1`
still understands that format — reporting it as `{:legacy, id}` — and
`encode/1`, `decode/1`, and `fetch_payment_id/1` still produce and
consume it. Every legacy function is deprecated and will be removed in
1.0.0; servers emit a `[:x402, :payment_identifier, :legacy]` telemetry
event (and a one-time warning log) when they see it.

# `decode_error`

```elixir
@type decode_error() ::
  :invalid_base64 | :invalid_json | :missing_payment_id | :invalid_payment_id
```

# `encode_error`

```elixir
@type encode_error() :: :invalid_payment_id | :invalid_json
```

# `extract_error`

```elixir
@type extract_error() :: :invalid_payment_id | {:legacy, decode_error()}
```

Errors returned by `extract_id/1`.

# `extracted`

```elixir
@type extracted() :: {:spec, payment_id()} | {:legacy, payment_id()}
```

An extracted id, tagged with the wire format it arrived in.

# `fingerprint_context`

```elixir
@type fingerprint_context() :: %{
  optional(:method) =&gt; atom() | String.t(),
  optional(:path) =&gt; String.t(),
  optional(:tool) =&gt; String.t()
}
```

Request context hashed into `fingerprint/2`.

# `legacy_source`

```elixir
@type legacy_source() :: :gate | :mcp
```

Where a legacy-format identifier was observed.

# `payment_id`

```elixir
@type payment_id() :: String.t()
```

Payment identifier value used for idempotency.

# `decode`
*since 0.1.0* 

> This function is deprecated. Use the spec `payment-identifier` format (extension/1, extract_id/1); removed in 1.0.0.

```elixir
@spec decode(String.t()) :: {:ok, payment_id()} | {:error, decode_error()}
```

Decodes a legacy Base64 JSON payload and returns the payment identifier.

    {:ok, encoded} = X402.Extensions.PaymentIdentifier.encode("payment-123")
    X402.Extensions.PaymentIdentifier.decode(encoded)
    #=> {:ok, "payment-123"}

    X402.Extensions.PaymentIdentifier.decode("not-base64")
    #=> {:error, :invalid_base64}

# `encode`
*since 0.1.0* 

> This function is deprecated. Use the spec `payment-identifier` format (extension/1, extract_id/1); removed in 1.0.0.

```elixir
@spec encode(payment_id()) :: {:ok, String.t()} | {:error, encode_error()}
```

Encodes a legacy payment identifier to a Base64 JSON payload.

    {:ok, encoded} = X402.Extensions.PaymentIdentifier.encode("payment-123")
    {:ok, "payment-123"} = X402.Extensions.PaymentIdentifier.decode(encoded)

# `enricher`
*since 0.9.0* 

```elixir
@spec enricher(keyword()) :: (map(), map() | nil -&gt;
                          {:ok, map()} | {:error, :invalid_payment_id})
```

Builds a client-side enricher for `X402.Client.build_payment/3`'s
`:extensions` option.

When the server advertised `payment-identifier` (or `always: true`), the
returned function attaches the spec-format extension to the payload:
the advertised `info` and `schema` are echoed unchanged and `info.id` is
added. Otherwise the payload passes through untouched. Without an
advertisement the `always: true` form attaches `%{"info" => %{"id" => id}}`.

The returned function yields `{:error, :invalid_payment_id}` when an
explicit `:id` fails `valid_id?/1`.

## Options

* `:id` - Explicit id to attach. When omitted a fresh `generate_id/0` value is
  produced on every invocation of the returned function. The default value is `nil`.

* `:always` (`t:boolean/0`) - Attach the extension even when the server did not advertise
  `payment-identifier`. Defaults to `false`: the enricher is a no-op
  for servers that do not support the extension. The default value is `false`.

## Examples

    iex> enricher = X402.Extensions.PaymentIdentifier.enricher(id: "abcdefghijklmnop")
    iex> advertised = %{"payment-identifier" => X402.Extensions.PaymentIdentifier.extension(required: true)}
    iex> payment_required = %{"extensions" => advertised}
    iex> payload = %{"extensions" => advertised}
    iex> {:ok, enriched} = enricher.(payload, payment_required)
    iex> enriched["extensions"]["payment-identifier"]["info"]
    %{"required" => true, "id" => "abcdefghijklmnop"}

    iex> enricher = X402.Extensions.PaymentIdentifier.enricher()
    iex> enricher.(%{"extensions" => %{}}, %{"extensions" => %{}})
    {:ok, %{"extensions" => %{}}}

# `extension`
*since 0.9.0* 

```elixir
@spec extension(keyword()) :: map()
```

Builds the server-side advertisement for `PaymentRequired.extensions`.

## Options

* `:required` (`t:boolean/0`) - Whether clients must supply an id (`info.required`). The default value is `false`.

## Examples

    iex> X402.Extensions.PaymentIdentifier.extension()["info"]
    %{"required" => false}

    iex> X402.Extensions.PaymentIdentifier.extension(required: true)["info"]
    %{"required" => true}

    iex> extension = X402.Extensions.PaymentIdentifier.extension()
    iex> extension["schema"] == X402.Extensions.PaymentIdentifier.schema()
    true

# `extension_key`
*since 0.9.0* 

```elixir
@spec extension_key() :: String.t()
```

Returns the extension key on the wire.

## Examples

    iex> X402.Extensions.PaymentIdentifier.extension_key()
    "payment-identifier"

# `extract_id`
*since 0.9.0* 

```elixir
@spec extract_id(term()) :: {:ok, extracted() | nil} | {:error, extract_error()}
```

Extracts the client's payment id from a `PaymentPayload.extensions` map.

Returns `{:ok, {:spec, id}}` for the `"payment-identifier"` format,
`{:ok, {:legacy, id}}` for the deprecated `"paymentIdentifier"` format,
and `{:ok, nil}` when no id is present (including when the spec extension
is echoed without an `info.id`). The spec key takes precedence: the legacy
key is only consulted when the spec key carries no id.

Errors are `:invalid_payment_id` for a spec-format id that fails
`valid_id?/1` (or a non-map spec value), and `{:legacy, reason}` for a
malformed legacy value. Legacy ids are not subject to the spec's length
and character rules — any non-empty string is accepted.

## Examples

    iex> extensions = %{"payment-identifier" => %{"info" => %{"id" => "abcdefghijklmnop", "required" => false}}}
    iex> X402.Extensions.PaymentIdentifier.extract_id(extensions)
    {:ok, {:spec, "abcdefghijklmnop"}}

    iex> X402.Extensions.PaymentIdentifier.extract_id(%{"payment-identifier" => %{"info" => %{"id" => "short"}}})
    {:error, :invalid_payment_id}

    iex> X402.Extensions.PaymentIdentifier.extract_id(%{"paymentIdentifier" => %{"paymentId" => "pay-1"}})
    {:ok, {:legacy, "pay-1"}}

    iex> X402.Extensions.PaymentIdentifier.extract_id(%{"paymentIdentifier" => %{}})
    {:error, {:legacy, :missing_payment_id}}

    iex> X402.Extensions.PaymentIdentifier.extract_id(%{})
    {:ok, nil}

    iex> X402.Extensions.PaymentIdentifier.extract_id(nil)
    {:ok, nil}

# `fetch_payment_id`
*since 0.1.0* 

> This function is deprecated. Use the spec `payment-identifier` format (extension/1, extract_id/1); removed in 1.0.0.

```elixir
@spec fetch_payment_id(term()) ::
  {:ok, payment_id()} | {:error, :missing_payment_id | :invalid_payment_id}
```

Extracts and validates `"paymentId"` from a decoded legacy payload map.

    X402.Extensions.PaymentIdentifier.fetch_payment_id(%{"paymentId" => "pay-1"})
    #=> {:ok, "pay-1"}

    X402.Extensions.PaymentIdentifier.fetch_payment_id(%{})
    #=> {:error, :missing_payment_id}

# `fingerprint`
*since 0.9.0* 

```elixir
@spec fingerprint(map(), fingerprint_context()) :: String.t()
```

Computes the request fingerprint a payment id is bound to.

The fingerprint is the lowercase hex SHA-256 of the newline-joined
sequence `scheme`, `network`, `asset`, `amount`, `payTo` (read from
`requirements`, string or atom keys) followed by the context's `:method`,
`:path`, and `:tool`, in that order. Absent values contribute an empty
string; atoms are stringified (`:get` becomes `"get"`). Two requests with
the same fingerprint are the same request for idempotency purposes; a
reused id with a different fingerprint is a conflict.

## Examples

    iex> requirements = %{"scheme" => "exact", "network" => "eip155:8453", "asset" => "0xusdc", "amount" => "1000", "payTo" => "0xabc"}
    iex> a = X402.Extensions.PaymentIdentifier.fingerprint(requirements, %{method: :get, path: "/api"})
    iex> b = X402.Extensions.PaymentIdentifier.fingerprint(requirements, %{method: :get, path: "/api"})
    iex> a == b
    true
    iex> String.length(a)
    64

    iex> requirements = %{"scheme" => "exact", "network" => "eip155:8453", "asset" => "0xusdc", "amount" => "1000", "payTo" => "0xabc"}
    iex> a = X402.Extensions.PaymentIdentifier.fingerprint(requirements, %{tool: "search"})
    iex> b = X402.Extensions.PaymentIdentifier.fingerprint(requirements, %{tool: "other"})
    iex> a == b
    false

# `generate_id`
*since 0.9.0* 

```elixir
@spec generate_id() :: payment_id()
```

Generates a random 32-character identifier (24 random
bytes, Base64url without padding).

## Examples

    iex> id = X402.Extensions.PaymentIdentifier.generate_id()
    iex> String.length(id)
    32
    iex> X402.Extensions.PaymentIdentifier.valid_id?(id)
    true

# `legacy_extension_key`
*since 0.9.0* 

```elixir
@spec legacy_extension_key() :: String.t()
```

Returns the deprecated pre-0.7.0 extension key.

## Examples

    iex> X402.Extensions.PaymentIdentifier.legacy_extension_key()
    "paymentIdentifier"

# `required?`
*since 0.9.0* 

```elixir
@spec required?(term()) :: boolean()
```

Returns whether an advertised extensions map marks the id as required.

Accepts the `PaymentRequired.extensions` map (string or atom keys).

## Examples

    iex> extensions = %{"payment-identifier" => X402.Extensions.PaymentIdentifier.extension(required: true)}
    iex> X402.Extensions.PaymentIdentifier.required?(extensions)
    true

    iex> X402.Extensions.PaymentIdentifier.required?(%{"payment-identifier" => %{"info" => %{}}})
    false

    iex> X402.Extensions.PaymentIdentifier.required?(%{})
    false

    iex> X402.Extensions.PaymentIdentifier.required?(nil)
    false

# `schema`
*since 0.9.0* 

```elixir
@spec schema() :: map()
```

Returns the JSON schema the server advertises for the extension.

## Examples

    iex> schema = X402.Extensions.PaymentIdentifier.schema()
    iex> schema["properties"]["id"]["pattern"]
    "^[A-Za-z0-9_-]+$"

# `valid_id?`
*since 0.9.0* 

```elixir
@spec valid_id?(term()) :: boolean()
```

Checks whether `id` is a valid spec-format identifier.

Valid ids are 16 to 128 characters long and
contain only `A-Z`, `a-z`, `0-9`, `_`, and `-`.

## Examples

    iex> X402.Extensions.PaymentIdentifier.valid_id?("abcdefghijklmnop")
    true

    iex> X402.Extensions.PaymentIdentifier.valid_id?("too-short")
    false

    iex> X402.Extensions.PaymentIdentifier.valid_id?("has spaces in the id")
    false

    iex> X402.Extensions.PaymentIdentifier.valid_id?(nil)
    false

---

*Consult [api-reference.md](api-reference.md) for complete listing*
