Issuance is the mint path: Forja builds the transaction, your custody layer signs it, Forja tracks the receipt.
unsigned → signed/submitted → confirmed
Forja never holds issuer keys. You sign mint(to, amount) with the token minter wallet.

Sequence

Flow diagram

1. Create an issuance

POST /v1/tokens/{tokenId}/issuances
Idempotency-Key: <client-request-id>
{
  "amount": "1000.00",
  "destinationAddress": "0xRecipientWallet",
  "reference": "mint-request-001",
  "metadata": { "asset": "STABLE" }
}
Forja returns status: "unsigned" and an unsignedTransaction payload (chain ID, to, data, gas fields, nonce).

2. Sign the mint transaction

Sign unsignedTransaction with the authorized minter wallet for that token (your MPC or custody provider in production). You may broadcast yourself or let Forja broadcast in the next step.

3. Report signed transaction

POST /v1/issuances/{issuanceId}/signed
{
  "txHash": "0x..."
}
Or send rawSignedTransaction if Forja should broadcast. Forja moves the issuance to submitted and watches the chain for confirmation.

4. Poll status

GET /v1/issuances/{issuanceId}
Final status: confirmed, failed, or expired.

Required API scopes

  • issuances.read
  • issuances.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..."; // issuer minter key (sandbox or custody-backed)

const minter = new ethers.Wallet(MINTER_PRIVATE_KEY);
const destinationAddress = "0xRecipientWallet";
const amount = "100.00";

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 issuance
const idempotencyKey = crypto.randomUUID();
const issuance = await forja(
  "POST",
  `/v1/tokens/${TOKEN_ID}/issuances`,
  {
    amount,
    destinationAddress,
    reference: idempotencyKey,
    metadata: { network: "polygon-amoy" },
  },
  { "Idempotency-Key": idempotencyKey }
);
console.log("created", issuance.id, issuance.status);

// 2. Sign mint transaction
const rawSignedTransaction = await signUnsignedTx(issuance.unsignedTransaction);

// 3. Submit signed transaction (Forja broadcasts)
const submitted = await forja("POST", `/v1/issuances/${issuance.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/issuances/${issuance.id}`);
  console.log("poll", current.status, current.txHash);
}
See Quickstart for HMAC header details. For the reverse lifecycle, see Redemption flow.