Skip to main content
Check out working demo: Live Demo
Repo: openfort-machines-cards-example
Deploy with Vercel
This guide is for partners already using Openfort embedded wallets. Scope:
  • Partner API integration (/partner/v1) only.
  • Openfort for auth + embedded wallet UX.
  • Your backend holds partner credentials and calls Machines.
  • Simple EVM-first flow for create card, deposits, and withdrawals.

Architecture

  • Frontend uses Openfort for sign-in and embedded wallet creation.
  • Frontend calls your backend only.
  • Your backend calls Machines Partner API.
  • Your backend executes withdrawal transactions with the signer path you control.

Prerequisites

  • Openfort project with:
    • publishable key for frontend
    • secret key for backend routes that call Openfort server APIs
    • embedded wallet (Shield) key configured
    • policy id configured for sponsored transactions
  • Machines partner API key
  • A backend service that can securely store:
    • X-Partner-Key
    • Openfort server credentials

Openfort Embedded Wallet Checklist

Before sending traffic:
  1. Wallet creation
    • New users get an embedded EVM wallet at login.
    • You can read the wallet address after auth.
  2. Stable mapping
    • You persist partner userId + Openfort user id + wallet address.
    • You always send the same mapped user identity to Machines.
  3. Session bootstrap server-side
    • POST /partner/v1/users/resolve and POST /partner/v1/sessions happen on your backend.
    • X-Partner-Key is never exposed to browser code.
  4. Browser request path
    • Browser calls your backend routes, not Machines directly.
    • This avoids CORS failures and secret leakage.
  5. Gas sponsorship
    • Openfort policy id is configured and attached for EVM sends.
    • Use sponsored send options for wallet execution paths where required.
Per user:
  • machinesUserId
  • openfortUserId
  • embeddedWalletAddress
  • linkedAt / lastUsedAt
Per session:
  • sessionId
  • scopes
  • expiresAt
Per withdrawal execution:
  • idempotency key
  • execution path (controller_v1 or coordinator_v2)
  • submitted tx hash
  • confirmed tx hash

End-to-End Partner Flow

1) Set up Openfort providers (frontend)

"use client";

import { AuthProvider, OpenfortProvider } from "@openfort/react";

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <OpenfortProvider
      publishableKey={process.env.NEXT_PUBLIC_OPENFORT_PUBLISHABLE_KEY!}
      walletConfig={{
        shieldPublishableKey:
          process.env.NEXT_PUBLIC_OPENFORT_SHIELD_PUBLISHABLE_KEY!,
        ethereumProviderPolicyId:
          process.env.NEXT_PUBLIC_OPENFORT_POLICY_ID,
      }}
      uiConfig={{ authProviders: [AuthProvider.EMAIL_OTP] }}
    >
      {children}
    </OpenfortProvider>
  );
}

2) Resolve user + create partner session (backend)

import { NextRequest, NextResponse } from "next/server";

const MACHINES_BASE_URL =
  process.env.MACHINES_PARTNER_BASE_URL ||
  "https://dev-api.machines.cash/partner/v1";

export async function POST(req: NextRequest) {
  const { externalUserId, walletAddress } = await req.json();

  const resolveRes = await fetch(`${MACHINES_BASE_URL}/users/resolve`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Partner-Key": process.env.MACHINES_PARTNER_API_KEY!,
    },
    body: JSON.stringify({
      userId: externalUserId,
      wallet: { chain: "evm", address: walletAddress },
      walletLabel: "Openfort Embedded Wallet",
    }),
  });

  if (!resolveRes.ok) {
    return NextResponse.json({ ok: false, error: "resolve_failed" }, { status: 502 });
  }

  const sessionRes = await fetch(`${MACHINES_BASE_URL}/sessions`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Partner-Key": process.env.MACHINES_PARTNER_API_KEY!,
    },
    body: JSON.stringify({
      userId: externalUserId,
      wallet: { chain: "evm", address: walletAddress },
      scopes: [
        "kyc.read",
        "kyc.write",
        "agreements.read",
        "agreements.write",
        "cards.read",
        "cards.write",
        "deposits.read",
        "deposits.write",
        "withdrawals.read",
        "withdrawals.write",
        "transactions.read",
      ],
      ttlSeconds: 900,
    }),
  });

  const payload = await sessionRes.json();
  if (!sessionRes.ok || !payload?.ok) {
    return NextResponse.json({ ok: false, error: "session_failed", payload }, { status: 502 });
  }

  return NextResponse.json({
    ok: true,
    sessionToken: payload.data.sessionToken,
    sessionId: payload.data.sessionId,
    expiresAt: payload.data.expiresAt,
  });
}

3) Call Machines through your backend proxy

Use the partner session token from step 2. At minimum, support these routes:
  • GET /partner/v1/kyc/values
  • POST /partner/v1/kyc/applications
  • GET /partner/v1/kyc/status
  • GET /partner/v1/agreements
  • POST /partner/v1/agreements
  • GET /partner/v1/cards
  • POST /partner/v1/cards
  • GET /partner/v1/deposits/assets
  • POST /partner/v1/deposits/range
  • POST /partner/v1/deposits/estimate
  • POST /partner/v1/deposits
  • GET /partner/v1/transactions

4) Create withdrawals

Call sequence:
  1. GET /partner/v1/withdrawals/assets
  2. POST /partner/v1/withdrawals/range
  3. POST /partner/v1/withdrawals/estimate
  4. POST /partner/v1/withdrawals
Create request example:
curl --request POST \
  --url https://api.machines.cash/partner/v1/withdrawals \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer <PARTNER_SESSION_TOKEN>' \
  --header 'Idempotency-Key: wdrl-001' \
  --data '{
    "amountCents": 2500,
    "source": {
      "contractId": "optional-uuid"
    },
    "destination": {
      "currency": "hbar",
      "network": "hbar",
      "address": "0.0.123456",
      "extraId": "optional-tag"
    },
    "adminAddress": "0xabc..."
  }'

5) Execute withdrawal transaction

Use response fields:
  • execution.callTarget
  • execution.callPath
  • parameters
Rules:
  • send tx to execution.callTarget
  • sender must match adminAddress used in create request
  • if status is pending, retry create with the same idempotency key

Execution Model and Contract Call Paths

Machines withdrawal create response includes:
  • execution.callTarget
  • execution.callPath (controller_v1 or coordinator_v2)
  • parameters (7-arg withdrawal payload)
Send to execution.callTarget using the signer you control.
  • controller_v1:
    • call withdrawAsset(address,address,uint256,address,uint256,bytes32,bytes)
  • coordinator_v2:
    • build admin typed-data signature
    • call 10-arg withdrawAsset(...) with admin signature arrays

Environment Variables

Frontend:
  • NEXT_PUBLIC_OPENFORT_PUBLISHABLE_KEY
  • NEXT_PUBLIC_OPENFORT_SHIELD_PUBLISHABLE_KEY
  • NEXT_PUBLIC_OPENFORT_POLICY_ID
  • NEXT_PUBLIC_OPENFORT_DEFAULT_CHAIN
Backend:
  • MACHINES_PARTNER_BASE_URL
  • MACHINES_PARTNER_API_KEY
  • MACHINES_PARTNER_DEFAULT_SCOPES
  • MACHINES_PARTNER_EXTERNAL_USER_PREFIX
  • Openfort server credentials for your execution routes

API Routes Used

User/session:
  • POST /partner/v1/users/resolve
  • POST /partner/v1/sessions
KYC:
  • GET /partner/v1/kyc/values
  • POST /partner/v1/kyc/applications
  • GET /partner/v1/kyc/status
Agreements:
  • GET /partner/v1/agreements
  • POST /partner/v1/agreements
Cards:
  • GET /partner/v1/cards
  • POST /partner/v1/cards
  • POST /partner/v1/cards/secrets/session
  • POST /partner/v1/cards/{cardId}/secrets
Deposits:
  • GET /partner/v1/deposits/assets
  • POST /partner/v1/deposits/range
  • POST /partner/v1/deposits/estimate
  • POST /partner/v1/deposits
Withdrawals:
  • GET /partner/v1/withdrawals/assets
  • POST /partner/v1/withdrawals/range
  • POST /partner/v1/withdrawals/estimate
  • POST /partner/v1/withdrawals
Transactions:
  • GET /partner/v1/transactions
  • GET /partner/v1/transactions/{transactionId}

Notes

  • Keep all partner calls server-side.
  • Keep idempotency keys stable on retries.
  • Keep one wallet mapping per user to avoid identity drift.