Redemptions mirror minting in reverse:
requested → receipt confirmed → unsigned burn tx → signed/submitted → confirmed

Sequence

Flow diagram

1. Create a redemption

POST /v1/tokens/{tokenId}/redemptions
Idempotency-Key: <client-request-id>
{
  "amount": "100.00",
  "clientReference": "settlement-001",
  "redeemerAddress": "0xUserWallet",
  "metadata": { "asset": "STABLE" }
}
Forja returns status: "requested". The burn transaction is not built until tokens are confirmed in the treasury wallet.

2. Confirm receipt

After the user returns tokens to the treasury (or your operator confirms the inbound transfer):
POST /v1/redemptions/{redemptionId}/receipt
{
  "receiptTxHash": "0x..."
}
Forja moves the redemption to unsigned and returns an ERC-20 burn transaction for the treasury minter to sign.

3. Sign and submit

Sign unsignedTransaction with the same minter wallet used for mint, then report:
POST /v1/redemptions/{redemptionId}/signed
{
  "txHash": "0x..."
}
Or send rawSignedTransaction if Forja should broadcast.

4. Poll status

GET /v1/redemptions/{redemptionId}
Final status: confirmed, failed, or expired.

Required API scopes

  • redemptions.read
  • redemptions.create

Example (Node.js)

Install dependencies: npm install ethers (Node 20+ includes fetch and crypto).
import crypto from "crypto";
import { ethers } from "ethers";

const API_URL = "https://api.sandbox.forja.capital";
const API_KEY = "ak_sandbox_...";
const API_SECRET = "sk_sandbox_...";
const TOKEN_ID = "your-token-id";
const MINTER_PRIVATE_KEY = "0x..."; // treasury burn signer

const minter = new ethers.Wallet(MINTER_PRIVATE_KEY);
const amount = "100.00";
const clientReference = crypto.randomUUID();
const receiptTxHash = "0x..."; // on-chain transfer of tokens to treasury

function signRequest(method, path, body = "") {
  const timestamp = String(Math.floor(Date.now() / 1000));
  const nonce = crypto.randomUUID();
  const signingString = [method.toUpperCase(), path, body, timestamp, nonce].join("\n");
  const signature = crypto.createHmac("sha256", API_SECRET).update(signingString).digest("hex");
  return {
    ...(body ? { "Content-Type": "application/json" } : {}),
    "x-api-key": API_KEY,
    "x-timestamp": timestamp,
    "x-nonce": nonce,
    "x-signature": signature,
  };
}

async function forja(method, path, payload, extraHeaders = {}) {
  const body = payload === undefined ? "" : JSON.stringify(payload);
  const response = await fetch(`${API_URL}${path}`, {
    method,
    headers: { ...signRequest(method, path, body), ...extraHeaders },
    body: body || undefined,
  });
  const data = await response.json();
  if (!response.ok) throw new Error(`${method} ${path} ${response.status}: ${JSON.stringify(data)}`);
  return data;
}

async function signUnsignedTx(unsigned) {
  return minter.signTransaction({
    type: 2,
    chainId: unsigned.chainId,
    nonce: unsigned.nonce,
    to: unsigned.to,
    data: unsigned.data,
    value: BigInt(unsigned.value),
    gasLimit: BigInt(unsigned.gasLimit),
    maxFeePerGas: BigInt(unsigned.maxFeePerGas),
    maxPriorityFeePerGas: BigInt(unsigned.maxPriorityFeePerGas),
  });
}

// 1. Create redemption
const redemption = await forja(
  "POST",
  `/v1/tokens/${TOKEN_ID}/redemptions`,
  {
    amount,
    clientReference,
    redeemerAddress: "0xUserWallet",
    metadata: { network: "polygon-amoy" },
  },
  { "Idempotency-Key": clientReference }
);
console.log("created", redemption.id, redemption.status);

// 2. Confirm treasury received tokens
const withBurn = await forja("POST", `/v1/redemptions/${redemption.id}/receipt`, {
  receiptTxHash,
});
console.log("receipt confirmed", withBurn.status);

// 3. Sign burn and submit
const rawSignedTransaction = await signUnsignedTx(withBurn.unsignedTransaction);
const submitted = await forja("POST", `/v1/redemptions/${redemption.id}/signed`, {
  rawSignedTransaction,
});
console.log("submitted", submitted.status, submitted.txHash);

// 4. Poll until terminal status
let current = submitted;
while (!["confirmed", "failed", "expired"].includes(current.status)) {
  await new Promise((r) => setTimeout(r, 5000));
  current = await forja("GET", `/v1/redemptions/${redemption.id}`);
  console.log("poll", current.status, current.txHash);
}
See Quickstart for HMAC header details. For minting, see Issuance flow.