> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blankfx.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Read/validate a single payment link



## OpenAPI

````yaml /api-reference/openapi.json get /payment-links/{id}
openapi: 3.1.0
info:
  title: BlankFX Public API
  version: 1.0.0
  description: >-
    Programmatic, **non-custodial** access to BlankFX: read your account, move
    money, and trade FX

    across the AMM (gasless Permit2 relay) and RFQ (MM-backed escrow) rails.
    BlankFX never holds your

    keys — every value operation returns EIP-712 typed-data that **you** sign
    with your registered

    wallet.


    ## Base URL


    All endpoints are under `https://api.blankfx.org/v1` (phase-1 `bfx_test_`
    keys run against the

    Sepolia sandbox). Errors share one shape: `{ "error": { "code": "<slug>",
    "message": "..." } }`.


    ## Authentication — Ed25519 request signing


    There is **no shared secret**. You generate an Ed25519 keypair, register the
    *public* key, and

    sign every request with the *private* key (which BlankFX never sees).


    1. **Create a key** in the dashboard (Settings → API keys). Your Ed25519
    keypair is generated in
       the browser; the **private key (PKCS8 PEM) is shown once** — store it securely. Only the public
       key is sent to BlankFX. You choose the key's scopes (below).
    2. **Sign each request.** Build the canonical string (LF-separated, no
    trailing newline):
       ```
       ${timestamp}\n${METHOD}\n${requestTarget}\n${sha256_hex(body)}
       ```
       where `timestamp` is unix **seconds**, `METHOD` is upper-case, `requestTarget` is the path
       **including any query string** (e.g. `/public/v1/fx/orders?limit=10`), and `sha256_hex(body)` is
       the hex SHA-256 of the raw request body (`sha256_hex("")` for GET/no-body). Then send three
       headers:
       - `BFX-API-KEY`: your `key_id`
       - `BFX-TIMESTAMP`: the same unix-seconds timestamp
       - `BFX-SIGNATURE`: `base64( Ed25519_sign(privateKey, canonicalString) )`
    3. **Freshness + replay.** The timestamp must be within **±30s** of server
    time, and each signature
       is single-use — re-sending an identical signed request is rejected (`replayed`). Always sign
       retries with a **fresh timestamp** (even for idempotent calls).

    ### Node signing example

    ```js

    import crypto from 'node:crypto';

    function signedHeaders({ keyId, privateKeyPem, method, requestTarget, body =
    '' }) {
      const ts = Math.floor(Date.now() / 1000).toString();
      const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
      const canonical = `${ts}\n${method.toUpperCase()}\n${requestTarget}\n${bodyHash}`;
      const sig = crypto.sign(null, Buffer.from(canonical), crypto.createPrivateKey(privateKeyPem));
      return { 'BFX-API-KEY': keyId, 'BFX-TIMESTAMP': ts, 'BFX-SIGNATURE': sig.toString('base64') };
    }

    ```


    ## Scopes


    Each key carries a subset of scopes; a call missing the required scope
    returns `403

    insufficient_scope`.

    - `read` — account, balances, wallets, rates, transactions, payment-link
    reads, exports

    - `trade` — FX quotes, accept, order submit/read (`/fx/*`)

    - `pay` — transfers, payment-link create/cancel, contacts (`/transfers*`,
    `/payment-links`)

    - `wallets` — programmatic signer-wallet register/manage (`/wallets*`)

    - `webhooks` — webhook endpoint management


    ## Idempotency


    Write calls (`POST`/`PATCH`) honour an optional `Idempotency-Key` header:
    replaying the same key on

    the same route within 24h returns the original response (with
    `Idempotency-Replayed: true`) instead

    of acting twice.


    ## Rate limits


    Per-key sliding window: **600/min** read, **120/min** write, **30/min** on
    the heaviest trade

    operations. Over-limit returns `429`.


    ## Non-custodial value flow (FX)


    1. `POST /fx/quotes` — get indicative AMM + firm RFQ quotes for a pair.

    2. `POST /fx/quotes/{id}/accept` — locks the quote, creates an order, and
    returns **EIP-712
       typed-data** to sign (AMM → a Permit2-witness order; RFQ → an escrow-funding order).
    3. Sign the typed-data with your registered wallet.

    4. `POST /fx/orders/{id}/submit` — send `{ signature }` (AMM: the relayer
    broadcasts your signed
       Permit2 order) or `{ tx_hash }` (RFQ: you self-broadcast the escrow-funding tx; the backend
       verifies the on-chain event). Poll `GET /fx/orders/{id}` for `settled`.

    ## Transfers


    - **Cross-currency** (`from_currency ≠ currency`): one atomic swap delivered
    to the payee — `accept`
       returns a Permit2-witness order to sign; `submit { signature }` relays it.
    - **Same-currency** (same token): non-custodial self-broadcast — you sign
    and broadcast a standard
       ERC-20 transfer, then `submit { tx_hash }` and the backend verifies the on-chain `Transfer`.

    ## Webhooks


    Register endpoints (scope `webhooks`) to receive `payment_link.paid`,
    `fx.order.settled|failed|

    refunded`, and `transfer.confirmed|failed`. Each delivery is HMAC-signed
    with a per-endpoint secret

    shown once at registration; verify it before trusting the payload.
  contact:
    name: BlankFX
    url: https://blankfx.org
servers:
  - url: https://blankfx-contacts.fly.dev/public/v1
    description: Sandbox — Sepolia testnet, bfx_test_ keys (live today)
  - url: https://api.blankfx.org/v1
    description: Stable public host (provisioned in Phase 2)
security:
  - bfxEd25519: []
tags:
  - name: Meta
    description: Health + the machine-readable contract (unauthenticated).
  - name: Account
    description: Read account, balances, wallets, rates, transactions (scope `read`).
  - name: FX
    description: Non-custodial FX trading across AMM + RFQ (scope `trade`).
  - name: Transfers
    description: >-
      Cross-currency (atomic swap) + same-token (self-broadcast) transfers
      (scope `pay`).
  - name: Payment links
    description: Create/read/cancel payment links (scope `pay`/`read`).
  - name: Wallets
    description: Programmatic signer-wallet register/manage via SIWE (scope `wallets`).
  - name: Contacts
    description: Saved payout recipients (scope `pay`).
  - name: Webhooks
    description: Event delivery endpoints (scope `webhooks`).
paths:
  /payment-links/{id}:
    get:
      summary: Read/validate a single payment link
      responses:
        '200':
          description: Payment link
        '401':
          description: Auth failed
        '403':
          description: Insufficient scope
        '404':
          description: Not found
components:
  securitySchemes:
    bfxEd25519:
      type: apiKey
      in: header
      name: BFX-API-KEY
      description: >-
        Ed25519 request signing. Send three headers: BFX-API-KEY (your key_id),
        BFX-TIMESTAMP (unix seconds; rejected if more than 30s from server
        time), and BFX-SIGNATURE = base64(Ed25519_sign(privateKey,
        `${timestamp}\n${METHOD}\n${requestTarget}\n${sha256(body)}`)) where
        requestTarget is the path including any query string. The server
        verifies the signature against your registered public key; no shared
        secret exists.

````