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

# Onboard Customers

> How to onboard and verify personal and business customers

export const KycTutorial = () => {
  const {useState, useEffect} = React;
  const [isSandbox, setIsSandbox] = useState(true);
  const [apiKey, setApiKey] = useState('');
  useEffect(() => {
    const stored = sessionStorage.getItem('etherfuse_api_key');
    if (stored) setApiKey(stored);
  }, []);
  const handleApiKeyChange = value => {
    setApiKey(value);
    if (value) {
      sessionStorage.setItem('etherfuse_api_key', value);
    } else {
      sessionStorage.removeItem('etherfuse_api_key');
    }
  };
  const getOrgIdFromApiKey = () => {
    if (!apiKey) return null;
    const parts = apiKey.split(':');
    if (parts.length >= 3) {
      const orgId = parts[2];
      if ((/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i).test(orgId)) {
        return orgId;
      }
    }
    return null;
  };
  const orgIdFromApiKey = getOrgIdFromApiKey();
  const baseUrl = isSandbox ? 'https://api.sand.etherfuse.com' : 'https://api.etherfuse.com';
  const [currentStep, setCurrentStep] = useState(0);
  const [loading, setLoading] = useState(false);
  const [onboardingResult, setOnboardingResult] = useState(null);
  const [kycSubmitResult, setKycSubmitResult] = useState(null);
  const [documentResult, setDocumentResult] = useState(null);
  const [agreementResults, setAgreementResults] = useState({});
  const [kycStatus, setKycStatus] = useState(null);
  const [stepErrors, setStepErrors] = useState({});
  const [customerId, setCustomerId] = useState('');
  const [bankAccountId, setBankAccountId] = useState('');
  const [publicKey, setPublicKey] = useState('');
  const [blockchain, setBlockchain] = useState('stellar');
  const [givenName, setGivenName] = useState('Juan');
  const [familyName, setFamilyName] = useState('Garcia');
  const [dateOfBirth, setDateOfBirth] = useState('1990-05-15');
  const [street, setStreet] = useState('Av. Reforma 123');
  const [city, setCity] = useState('Mexico City');
  const [region, setRegion] = useState('CDMX');
  const [postalCode, setPostalCode] = useState('06600');
  const [country, setCountry] = useState('MX');
  const [email, setEmail] = useState('juan@example.com');
  const [phoneNumber, setPhoneNumber] = useState('+521234567890');
  const [occupation, setOccupation] = useState('Software Engineer');
  const [idNumber, setIdNumber] = useState('GAJU900515HDFRNN09');
  const [idType, setIdType] = useState('CURP');
  const [selfieData, setSelfieData] = useState('');
  const [idFrontData, setIdFrontData] = useState('');
  const [idBackData, setIdBackData] = useState('');
  const [presignedUrl, setPresignedUrl] = useState('');
  const [orgCustomers, setOrgCustomers] = useState([]);
  const [orgWallets, setOrgWallets] = useState([]);
  const [orgBankAccounts, setOrgBankAccounts] = useState([]);
  const [orgDataLoading, setOrgDataLoading] = useState(false);
  const [orgDataError, setOrgDataError] = useState(null);
  const generateUUID = () => {
    return ('xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx').replace(/[xy]/g, function (c) {
      const r = Math.random() * 16 | 0;
      const v = c === 'x' ? r : r & 0x3 | 0x8;
      return v.toString(16);
    });
  };
  useEffect(() => {
    if (!customerId) setCustomerId(generateUUID());
    if (!bankAccountId) setBankAccountId(generateUUID());
  }, []);
  const [copied, setCopied] = useState(null);
  const copyToClipboard = (text, id) => {
    navigator.clipboard.writeText(text);
    setCopied(id);
    setTimeout(() => setCopied(null), 2000);
  };
  const makeRequest = async (method, endpoint, body) => {
    setLoading(true);
    try {
      const options = {
        method,
        headers: {
          'Content-Type': 'application/json',
          'Authorization': apiKey
        }
      };
      if (body) {
        options.body = JSON.stringify(body);
      }
      const response = await fetch(baseUrl + endpoint, options);
      const responseText = await response.text();
      let data = null;
      try {
        if (responseText) {
          data = JSON.parse(responseText);
        }
      } catch (parseErr) {}
      if (!response.ok) {
        let errorMessage = 'HTTP ' + response.status;
        if (data) {
          errorMessage += ': ' + (data.message || data.error || JSON.stringify(data));
        } else if (responseText) {
          errorMessage += ': ' + responseText.substring(0, 200);
        } else {
          errorMessage += ' (no response body)';
        }
        throw new Error(errorMessage);
      }
      return data;
    } catch (err) {
      const msg = err.message || String(err);
      if (msg.includes('Failed to fetch')) {
        throw new Error('Request failed - likely a 401 Unauthorized (CORS headers not sent on error responses). Verify your API key is correct.');
      }
      throw err;
    } finally {
      setLoading(false);
    }
  };
  const makeSilentRequest = async (method, endpoint, body) => {
    const options = {
      method,
      headers: {
        'Content-Type': 'application/json',
        'Authorization': apiKey
      }
    };
    if (body) {
      options.body = JSON.stringify(body);
    }
    const response = await fetch(baseUrl + endpoint, options);
    if (!response.ok) return null;
    const text = await response.text();
    return text ? JSON.parse(text) : null;
  };
  const fetchOrgData = async () => {
    if (!orgIdFromApiKey) return;
    setOrgDataLoading(true);
    setOrgDataError(null);
    try {
      const customersData = await makeSilentRequest('POST', '/ramp/customers', {
        pageSize: 100,
        pageNumber: 0
      });
      const customers = customersData?.items || [];
      setOrgCustomers(customers);
      const allWallets = [];
      const allBankAccounts = [];
      for (const customer of customers) {
        try {
          const walletsData = await makeSilentRequest('POST', '/ramp/customer/' + customer.customerId + '/wallets', {
            pageSize: 100,
            pageNumber: 0
          });
          if (walletsData?.items) {
            allWallets.push(...walletsData.items.map(w => ({
              ...w,
              customerId: customer.customerId
            })));
          }
          const bankData = await makeSilentRequest('POST', '/ramp/customer/' + customer.customerId + '/bank-accounts', {
            pageSize: 100,
            pageNumber: 0
          });
          if (bankData?.items) {
            allBankAccounts.push(...bankData.items.map(b => ({
              ...b,
              customerId: customer.customerId
            })));
          }
        } catch (err) {}
      }
      setOrgWallets(allWallets);
      setOrgBankAccounts(allBankAccounts);
    } catch (err) {
      setOrgDataError('Failed to load org data');
    } finally {
      setOrgDataLoading(false);
    }
  };
  useEffect(() => {
    if (orgIdFromApiKey) {
      fetchOrgData();
    } else {
      setOrgCustomers([]);
      setOrgWallets([]);
      setOrgBankAccounts([]);
    }
  }, [orgIdFromApiKey, baseUrl]);
  const existingWalletEntries = orgWallets.map(wallet => {
    const customer = orgCustomers.find(c => c.customerId === wallet.customerId);
    const bankAccount = orgBankAccounts.find(b => b.customerId === wallet.customerId);
    return {
      wallet,
      customer,
      bankAccount,
      label: `${wallet.publicKey.substring(0, 8)}...${wallet.publicKey.substring(wallet.publicKey.length - 4)} (${wallet.blockchain})`
    };
  });
  const [selectedWalletMode, setSelectedWalletMode] = useState('new');
  useEffect(() => {
    if (existingWalletEntries.length > 0 && selectedWalletMode === 'new' && !publicKey) {
      const firstEntry = existingWalletEntries[0];
      setSelectedWalletMode(firstEntry.wallet.publicKey);
      setPublicKey(firstEntry.wallet.publicKey);
      setBlockchain(firstEntry.wallet.blockchain);
      setCustomerId(firstEntry.wallet.customerId);
      setBankAccountId(firstEntry.bankAccount ? firstEntry.bankAccount.bankAccountId : generateUUID());
    }
  }, [existingWalletEntries.length]);
  const selectExistingWallet = entry => {
    setSelectedWalletMode(entry.wallet.publicKey);
    setPublicKey(entry.wallet.publicKey);
    setBlockchain(entry.wallet.blockchain);
    setCustomerId(entry.wallet.customerId);
    setBankAccountId(entry.bankAccount ? entry.bankAccount.bankAccountId : generateUUID());
  };
  const selectNewWallet = () => {
    setSelectedWalletMode('new');
    setPublicKey('');
    setCustomerId(generateUUID());
    setBankAccountId(generateUUID());
  };
  const getCurl = (method, endpoint, body) => {
    let cmd = 'curl';
    if (method !== 'GET') cmd += ' -X ' + method;
    cmd += ' "' + baseUrl + endpoint + '"';
    cmd += ' \\\n  -H "Authorization: ' + (apiKey || 'YOUR_API_KEY') + '"';
    if (body) {
      cmd += ' \\\n  -H "Content-Type: application/json"';
      cmd += " \\\n  -d '" + JSON.stringify(body) + "'";
    }
    return cmd;
  };
  const validatePublicKey = (key, chain) => {
    if (!key) return {
      valid: false,
      error: 'Public key is required',
      detectedChain: null
    };
    const chainLower = chain.toLowerCase();
    switch (chainLower) {
      case 'stellar':
        if (!(/^G[A-Z2-7]{55}$/).test(key)) {
          return {
            valid: false,
            error: 'Stellar public keys start with G and are 56 characters',
            detectedChain: null
          };
        }
        break;
      case 'solana':
        if (!(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).test(key)) {
          return {
            valid: false,
            error: 'Solana public keys are 32-44 base58 characters',
            detectedChain: null
          };
        }
        break;
      case 'base':
      case 'polygon':
        if (!(/^0x[a-fA-F0-9]{40}$/).test(key)) {
          return {
            valid: false,
            error: 'EVM addresses start with 0x followed by 40 hex characters',
            detectedChain: null
          };
        }
        break;
      default:
        return {
          valid: true,
          error: null,
          detectedChain: null,
          isCustom: true
        };
    }
    return {
      valid: true,
      error: null,
      detectedChain: null
    };
  };
  const detectBlockchain = key => {
    if (!key) return null;
    if ((/^G[A-Z2-7]{55}$/).test(key)) return 'stellar';
    if ((/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).test(key)) return 'solana';
    if ((/^0x[a-fA-F0-9]{40}$/).test(key)) return 'evm';
    return null;
  };
  useEffect(() => {
    if (!publicKey) return;
    const detected = detectBlockchain(publicKey);
    if (!detected) return;
    const chainLower = blockchain.toLowerCase();
    if (detected === 'evm') {
      if (chainLower !== 'base' && chainLower !== 'polygon') {
        setBlockchain('base');
      }
    } else if (detected !== chainLower) {
      setBlockchain(detected);
    }
  }, [publicKey]);
  const publicKeyValidation = validatePublicKey(publicKey, blockchain);
  const existingWalletMatch = publicKey && orgWallets.find(w => w.publicKey === publicKey);
  const existingCustomerForWallet = existingWalletMatch ? orgCustomers.find(c => c.customerId === existingWalletMatch.customerId) : null;
  const existingBankAccountForWallet = existingWalletMatch ? orgBankAccounts.find(b => b.customerId === existingWalletMatch.customerId) : null;
  const [existingCustomerSuggestion, setExistingCustomerSuggestion] = useState(null);
  const lookupExistingCustomer = async () => {
    try {
      const customersData = await makeRequest('POST', '/ramp/customers', {
        pageSize: 100,
        pageNumber: 0
      });
      if (!customersData?.items?.length) {
        return {
          found: false
        };
      }
      for (const customer of customersData.items) {
        try {
          const walletsData = await makeRequest('POST', '/ramp/customer/' + customer.customerId + '/wallets', {
            pageSize: 100,
            pageNumber: 0
          });
          const matchingWallet = walletsData?.items?.find(w => w.publicKey === publicKey);
          if (matchingWallet) {
            const bankAccountsData = await makeRequest('POST', '/ramp/customer/' + customer.customerId + '/bank-accounts', {
              pageSize: 100,
              pageNumber: 0
            });
            const bankAccount = bankAccountsData?.items?.[0];
            return {
              found: true,
              customerId: customer.customerId,
              bankAccountId: bankAccount?.bankAccountId || null
            };
          }
        } catch (err) {
          continue;
        }
      }
      return {
        found: false
      };
    } catch (err) {
      console.error('Failed to lookup existing customer:', err);
      return {
        found: false
      };
    }
  };
  const useExistingCustomer = async () => {
    if (existingCustomerSuggestion) {
      const newCustomerId = existingCustomerSuggestion.customerId;
      const newBankAccountId = existingCustomerSuggestion.bankAccountId || bankAccountId;
      setCustomerId(newCustomerId);
      if (existingCustomerSuggestion.bankAccountId) {
        setBankAccountId(existingCustomerSuggestion.bankAccountId);
      }
      setExistingCustomerSuggestion(null);
      setStepErrors(prev => ({
        ...prev,
        step1: null
      }));
      await generateOnboardingUrl({
        customerId: newCustomerId,
        bankAccountId: newBankAccountId
      });
    }
  };
  const generateOnboardingUrl = async (overrides = {}) => {
    setStepErrors(prev => ({
      ...prev,
      step1: null
    }));
    setExistingCustomerSuggestion(null);
    if (!publicKeyValidation.valid) {
      setStepErrors(prev => ({
        ...prev,
        step1: publicKeyValidation.error
      }));
      return;
    }
    const effectiveCustomerId = overrides.customerId || customerId;
    const effectiveBankAccountId = overrides.bankAccountId || bankAccountId;
    try {
      const body = {
        customerId: effectiveCustomerId,
        bankAccountId: effectiveBankAccountId,
        publicKey,
        blockchain: blockchain.toLowerCase(),
        ...email && (givenName || familyName) ? {
          userInfo: {
            email,
            displayName: [givenName, familyName].filter(Boolean).join(' ').trim()
          }
        } : {}
      };
      const data = await makeRequest('POST', '/ramp/onboarding-url', body);
      setOnboardingResult(data);
      setPresignedUrl(data.presigned_url);
      setCurrentStep(1);
    } catch (err) {
      const errMsg = err.message || String(err);
      if (errMsg.includes('409') && errMsg.includes('already added user')) {
        const lookup = await lookupExistingCustomer();
        if (lookup.found) {
          setExistingCustomerSuggestion(lookup);
          setStepErrors(prev => ({
            ...prev,
            step1: `This wallet is already registered to customer ${lookup.customerId}${lookup.bankAccountId ? ' with bank account ' + lookup.bankAccountId : ''}.`
          }));
          return;
        }
      }
      if (errMsg.includes('400') && errMsg.includes('Client not linked to this organization')) {
        setStepErrors(prev => ({
          ...prev,
          step1: 'This customer ID belongs to a different organization. Generate a new Customer ID and try again, or use a different API key that owns this customer.'
        }));
        setCustomerId(generateUUID());
        setBankAccountId(generateUUID());
        return;
      }
      setStepErrors(prev => ({
        ...prev,
        step1: errMsg
      }));
    }
  };
  const submitKycData = async () => {
    setStepErrors(prev => ({
      ...prev,
      step2: null
    }));
    try {
      const body = {
        pubkey: publicKey,
        identity: {
          id: publicKey,
          name: {
            givenName,
            familyName
          },
          dateOfBirth,
          address: {
            street,
            city,
            region,
            postalCode,
            country
          },
          idNumbers: [{
            value: idNumber,
            type: idType
          }]
        }
      };
      const data = await makeRequest('POST', '/ramp/customer/' + customerId + '/kyc', body);
      setKycSubmitResult(data);
      setCurrentStep(2);
    } catch (err) {
      setStepErrors(prev => ({
        ...prev,
        step2: err.message || String(err)
      }));
    }
  };
  const uploadDocuments = async (docType, images) => {
    setStepErrors(prev => ({
      ...prev,
      step3: null
    }));
    try {
      const body = {
        pubkey: publicKey,
        documentType: docType,
        images
      };
      const data = await makeRequest('POST', '/ramp/customer/' + customerId + '/kyc/documents', body);
      setDocumentResult(prev => ({
        ...prev,
        [docType]: data
      }));
      return data;
    } catch (err) {
      setStepErrors(prev => ({
        ...prev,
        step3: err.message || String(err)
      }));
      throw err;
    }
  };
  const handleFileUpload = (e, setter) => {
    const file = e.target.files[0];
    if (file) {
      const reader = new FileReader();
      reader.onload = event => {
        setter(event.target.result);
      };
      reader.readAsDataURL(file);
    }
  };
  const uploadAllDocuments = async () => {
    setStepErrors(prev => ({
      ...prev,
      step3: null
    }));
    try {
      if (selfieData) {
        await uploadDocuments('selfie', [{
          label: 'selfie',
          image: selfieData
        }]);
      }
      const idImages = [];
      if (idFrontData) {
        idImages.push({
          label: 'id_front',
          image: idFrontData
        });
      }
      if (idBackData) {
        idImages.push({
          label: 'id_back',
          image: idBackData
        });
      }
      if (idImages.length > 0) {
        await uploadDocuments('document', idImages);
      }
      setCurrentStep(3);
    } catch (err) {}
  };
  const acceptAgreement = async (agreementType, endpoint) => {
    setStepErrors(prev => ({
      ...prev,
      step4: null
    }));
    try {
      const body = {
        presignedUrl
      };
      const data = await makeRequest('POST', endpoint, body);
      setAgreementResults(prev => ({
        ...prev,
        [agreementType]: data
      }));
      return data;
    } catch (err) {
      setStepErrors(prev => ({
        ...prev,
        step4: err.message || String(err)
      }));
      throw err;
    }
  };
  const acceptAllAgreements = async () => {
    setStepErrors(prev => ({
      ...prev,
      step4: null
    }));
    try {
      await acceptAgreement('electronic-signature', '/ramp/agreements/electronic-signature');
      await acceptAgreement('terms-and-conditions', '/ramp/agreements/terms-and-conditions');
      await acceptAgreement('customer-agreement', '/ramp/agreements/customer-agreement');
      setCurrentStep(4);
    } catch (err) {}
  };
  const checkKycStatus = async () => {
    setStepErrors(prev => ({
      ...prev,
      step5: null
    }));
    try {
      const data = await makeRequest('GET', '/ramp/customer/' + customerId + '/kyc/' + publicKey);
      setKycStatus(data);
      setCurrentStep(5);
    } catch (err) {
      setStepErrors(prev => ({
        ...prev,
        step5: err.message || String(err)
      }));
    }
  };
  const tryItButtonStyle = enabled => ({
    display: 'inline-flex',
    alignItems: 'center',
    gap: '6px',
    padding: '8px 16px',
    borderRadius: '20px',
    border: 'none',
    cursor: enabled ? 'pointer' : 'not-allowed',
    backgroundColor: enabled ? '#0d9488' : '#9ca3af',
    color: 'white',
    fontSize: '14px',
    fontWeight: 500,
    opacity: loading ? 0.7 : 1
  });
  const secondaryButtonStyle = {
    padding: '8px 12px',
    margin: '5px',
    borderRadius: '6px',
    border: '1px solid #d1d5db',
    cursor: 'pointer',
    backgroundColor: 'transparent',
    color: '#6b7280',
    fontSize: '13px'
  };
  const codeBlockStyle = {
    width: '100%',
    maxHeight: '300px',
    overflow: 'auto',
    padding: '16px',
    fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
    fontSize: '13px',
    lineHeight: '1.5',
    borderRadius: '8px',
    backgroundColor: '#f8fafc',
    color: '#334155',
    border: '1px solid #e2e8f0',
    margin: 0,
    whiteSpace: 'pre-wrap',
    wordBreak: 'break-word',
    boxSizing: 'border-box'
  };
  const inputStyle = {
    width: '100%',
    padding: '8px 10px',
    borderRadius: '4px',
    border: '1px solid #d1d5db',
    fontSize: '13px',
    fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
    boxSizing: 'border-box'
  };
  const selectStyle = {
    ...inputStyle,
    backgroundColor: 'white',
    cursor: 'pointer',
    appearance: 'auto'
  };
  const labelStyle = {
    display: 'block',
    fontSize: '12px',
    fontWeight: 600,
    color: '#374151',
    marginBottom: '4px'
  };
  const fieldGroupStyle = {
    marginBottom: '12px'
  };
  const stepErrorStyle = {
    padding: '12px',
    backgroundColor: '#fef2f2',
    color: '#dc2626',
    borderRadius: '6px',
    marginTop: '12px',
    fontSize: '13px',
    border: '1px solid #fecaca'
  };
  const renderStepError = stepKey => {
    const err = stepErrors[stepKey];
    if (!err) return null;
    if (stepKey === 'step1' && existingCustomerSuggestion) {
      return <div style={{
        ...stepErrorStyle,
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
        backgroundColor: '#fef9c3',
        borderColor: '#fef08a',
        color: '#854d0e'
      }}>
          <span>{err}</span>
          <button onClick={useExistingCustomer} style={{
        padding: '6px 12px',
        borderRadius: '6px',
        border: 'none',
        backgroundColor: '#0d9488',
        color: 'white',
        fontSize: '13px',
        fontWeight: 500,
        cursor: 'pointer',
        whiteSpace: 'nowrap',
        marginLeft: '12px'
      }}>
            Use it →
          </button>
        </div>;
    }
    return <div style={stepErrorStyle}>
        <strong>Error:</strong> {err}
      </div>;
  };
  const renderRandomIdBox = (label, value, onRegenerate) => <div style={fieldGroupStyle}>
      <label style={labelStyle}>{label}</label>
      <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '8px',
    padding: '8px 10px',
    backgroundColor: '#f0f9ff',
    border: '1px dashed #3b82f6',
    borderRadius: '4px',
    fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
    fontSize: '12px',
    color: '#1e40af'
  }}>
        <span style={{
    flex: 1,
    overflow: 'hidden',
    textOverflow: 'ellipsis'
  }}>{value}</span>
        <button onClick={onRegenerate} title="Generate new random ID" style={{
    background: 'none',
    border: 'none',
    cursor: 'pointer',
    fontSize: '16px',
    padding: '2px 6px',
    borderRadius: '4px'
  }}>
          🎲
        </button>
      </div>
      <div style={{
    fontSize: '10px',
    color: '#6b7280',
    marginTop: '2px'
  }}>
        Auto-generated UUID
      </div>
    </div>;
  const getOnboardingBody = () => ({
    customerId,
    bankAccountId,
    publicKey,
    blockchain: blockchain.toLowerCase(),
    ...email && (givenName || familyName) ? {
      userInfo: {
        email,
        displayName: [givenName, familyName].filter(Boolean).join(' ').trim()
      }
    } : {}
  });
  const getKycBody = () => ({
    pubkey: publicKey,
    identity: {
      id: publicKey,
      email,
      phoneNumber,
      occupation,
      name: {
        givenName,
        familyName
      },
      dateOfBirth,
      address: {
        street,
        city,
        region,
        postalCode,
        country
      },
      idNumbers: [{
        value: idNumber,
        type: idType
      }]
    }
  });
  return <div style={{
    padding: '20px',
    border: '1px solid #e5e7eb',
    borderRadius: '8px',
    marginTop: '20px'
  }}>
      <div style={{
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: '20px'
  }}>
        <h3 style={{
    margin: 0
  }}>Interactive tutorial — Personal customer (KYC)</h3>
        <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '8px'
  }}>
          <span style={{
    fontSize: '14px',
    color: isSandbox ? '#111827' : '#6b7280',
    fontWeight: isSandbox ? 600 : 400
  }}>Sandbox</span>
          <button onClick={() => setIsSandbox(!isSandbox)} style={{
    position: 'relative',
    width: '48px',
    height: '24px',
    borderRadius: '12px',
    border: 'none',
    backgroundColor: isSandbox ? '#22c55e' : '#3b82f6',
    cursor: 'pointer',
    transition: 'background-color 0.2s'
  }}>
            <span style={{
    position: 'absolute',
    top: '2px',
    left: isSandbox ? '2px' : '26px',
    width: '20px',
    height: '20px',
    borderRadius: '50%',
    backgroundColor: 'white',
    transition: 'left 0.2s'
  }} />
          </button>
          <span style={{
    fontSize: '14px',
    color: isSandbox ? '#6b7280' : '#111827',
    fontWeight: isSandbox ? 400 : 600
  }}>Production</span>
        </div>
      </div>

      <div style={{
    marginBottom: '20px',
    padding: '16px',
    backgroundColor: '#f3f4f6',
    borderRadius: '6px'
  }}>
        <div style={{
    marginBottom: '12px'
  }}>
          <span style={{
    fontSize: '12px',
    color: '#6b7280'
  }}>Host: </span>
          <code style={{
    fontSize: '12px'
  }}>{baseUrl}</code>
        </div>
        <div>
          <div style={{
    display: 'flex',
    alignItems: 'baseline',
    gap: '8px',
    marginBottom: '8px'
  }}>
            <label style={{
    fontWeight: 600
  }}>API Key</label>
            <span style={{
    fontSize: '12px',
    color: '#6b7280'
  }}>
              Find it under 'API Keys' at{' '}
              <a href={isSandbox ? 'https://devnet.etherfuse.com/ramp?view=manage' : 'https://app.etherfuse.com/ramp?view=manage'} target="_blank" rel="noopener noreferrer" style={{
    color: '#3b82f6'
  }}>
                {isSandbox ? 'devnet.etherfuse.com/ramp' : 'app.etherfuse.com/ramp'}
              </a>
            </span>
          </div>
          <input type="password" value={apiKey} onChange={e => handleApiKeyChange(e.target.value)} placeholder="Enter your API key" style={{
    width: '100%',
    padding: '10px',
    borderRadius: '4px',
    border: '1px solid #d1d5db',
    fontSize: '14px',
    fontFamily: 'monospace',
    boxSizing: 'border-box'
  }} />
          {orgIdFromApiKey && <div style={{
    marginTop: '8px',
    padding: '8px 12px',
    backgroundColor: '#f0fdf4',
    borderRadius: '4px',
    border: '1px solid #bbf7d0'
  }}>
              <span style={{
    fontSize: '12px',
    color: '#166534'
  }}>
                <strong>Your org →</strong> {orgIdFromApiKey}
              </span>
              {orgDataLoading && <span style={{
    fontSize: '12px',
    color: '#6b7280',
    marginLeft: '12px'
  }}>Loading org data...</span>}
              {!orgDataLoading && orgCustomers.length > 0 && <span style={{
    fontSize: '12px',
    color: '#166534',
    marginLeft: '12px'
  }}>
                  ({orgCustomers.length} customers, {orgWallets.length} wallets, {orgBankAccounts.length} bank accounts)
                </span>}
            </div>}
        </div>
      </div>

      {}
      <div style={{
    marginBottom: '24px',
    border: '1px solid #e5e7eb',
    borderRadius: '8px',
    overflow: 'hidden'
  }}>
        <div style={{
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: '12px 16px',
    backgroundColor: '#f8fafc',
    borderBottom: '1px solid #e5e7eb'
  }}>
          <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '8px'
  }}>
            <span style={{
    padding: '4px 8px',
    backgroundColor: '#6366f1',
    color: 'white',
    borderRadius: '4px',
    fontSize: '12px',
    fontWeight: 600
  }}>Step 1</span>
            <span style={{
    padding: '4px 8px',
    backgroundColor: '#3b82f6',
    color: 'white',
    borderRadius: '4px',
    fontSize: '12px',
    fontWeight: 600
  }}>POST</span>
            <span style={{
    fontFamily: 'monospace',
    fontSize: '14px'
  }}>/ramp/onboarding-url</span>
          </div>
          <button onClick={generateOnboardingUrl} disabled={!apiKey || loading || !publicKeyValidation.valid} style={tryItButtonStyle(apiKey && !loading && publicKeyValidation.valid)}>
            {loading ? 'Loading...' : 'Try it'} {!loading && <span>▶</span>}
          </button>
        </div>
        <div style={{
    padding: '16px'
  }}>
          <p style={{
    color: '#6b7280',
    margin: 0,
    marginBottom: '16px'
  }}>
            Generate a presigned URL for a new or existing customer.{' '}
            <a href="/api-reference/onboarding/generate-onboarding-url" style={{
    color: '#0d9488'
  }}>View API reference →</a>
          </p>

          {}
          {orgIdFromApiKey && existingWalletEntries.length > 0 && <div style={{
    marginBottom: '16px',
    padding: '12px',
    backgroundColor: '#f0f9ff',
    borderRadius: '6px',
    border: '1px solid #bae6fd'
  }}>
              <label style={{
    ...labelStyle,
    marginBottom: '8px',
    display: 'block'
  }}>Select Wallet</label>
              <div style={{
    display: 'flex',
    flexWrap: 'wrap',
    gap: '8px'
  }}>
                <button onClick={selectNewWallet} style={{
    padding: '6px 12px',
    borderRadius: '6px',
    border: selectedWalletMode === 'new' ? '2px solid #2563eb' : '1px solid #d1d5db',
    backgroundColor: selectedWalletMode === 'new' ? '#eff6ff' : 'white',
    cursor: 'pointer',
    fontSize: '12px',
    fontWeight: selectedWalletMode === 'new' ? 600 : 400
  }}>
                  + New Customer
                </button>
                {existingWalletEntries.map(entry => <button key={entry.wallet.publicKey} onClick={() => selectExistingWallet(entry)} style={{
    padding: '6px 12px',
    borderRadius: '6px',
    border: selectedWalletMode === entry.wallet.publicKey ? '2px solid #2563eb' : '1px solid #d1d5db',
    backgroundColor: selectedWalletMode === entry.wallet.publicKey ? '#eff6ff' : 'white',
    cursor: 'pointer',
    fontSize: '12px',
    fontWeight: selectedWalletMode === entry.wallet.publicKey ? 600 : 400,
    fontFamily: 'ui-monospace, monospace'
  }} title={entry.wallet.publicKey}>
                    {entry.label}
                  </button>)}
              </div>
              {orgDataLoading && <div style={{
    fontSize: '11px',
    color: '#6b7280',
    marginTop: '6px'
  }}>Loading wallets...</div>}
            </div>}

          <div style={{
    backgroundColor: '#f9fafb',
    padding: '16px',
    borderRadius: '6px',
    marginBottom: '16px',
    border: '1px solid #e5e7eb'
  }}>
            {}
            {selectedWalletMode === 'new' && <div style={{
    display: 'grid',
    gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
    gap: '12px',
    marginBottom: '16px'
  }}>
                {renderRandomIdBox('Customer ID', customerId, () => setCustomerId(generateUUID()))}
                {renderRandomIdBox('Bank Account ID', bankAccountId, () => setBankAccountId(generateUUID()))}
              </div>}

            {}
            {selectedWalletMode !== 'new' && (() => {
    const selectedEntry = existingWalletEntries.find(e => e.wallet.publicKey === selectedWalletMode);
    const hasBankAccount = selectedEntry?.bankAccount;
    return <div style={{
      marginBottom: '16px'
    }}>
                  <div style={{
      padding: '8px 12px',
      backgroundColor: '#f0fdf4',
      borderRadius: '4px',
      border: '1px solid #bbf7d0',
      marginBottom: hasBankAccount ? 0 : '12px'
    }}>
                    <div style={{
      fontSize: '12px',
      color: '#166534'
    }}>
                      <strong>Customer:</strong> {customerId}
                      {hasBankAccount && <><br /><strong>Bank Account:</strong> {bankAccountId}</>}
                    </div>
                  </div>
                  {!hasBankAccount && renderRandomIdBox('Bank Account ID (new)', bankAccountId, () => setBankAccountId(generateUUID()))}
                </div>;
  })()}

            {}
            {selectedWalletMode === 'new' && <>
            <div style={{
    marginBottom: '16px'
  }}>
              <div style={fieldGroupStyle}>
                <label style={labelStyle}>Public Key *</label>
                <input type="text" value={publicKey} onChange={e => setPublicKey(e.target.value)} placeholder={blockchain === 'stellar' ? 'GABC...XYZ' : blockchain === 'solana' ? 'ABC123...' : '0x...'} style={{
    ...inputStyle,
    borderColor: publicKey && !publicKeyValidation.valid ? '#f87171' : publicKey && publicKeyValidation.valid ? '#22c55e' : '#d1d5db'
  }} />
                {publicKey && !publicKeyValidation.valid && <div style={{
    fontSize: '11px',
    color: '#dc2626',
    marginTop: '4px'
  }}>{publicKeyValidation.error}</div>}
                {publicKey && publicKeyValidation.valid && !publicKeyValidation.isCustom && selectedWalletMode === 'new' && !existingWalletMatch && <div style={{
    fontSize: '11px',
    color: '#22c55e',
    marginTop: '4px'
  }}>✓ Valid {blockchain} address (new wallet)</div>}
                {publicKey && publicKeyValidation.valid && !publicKeyValidation.isCustom && selectedWalletMode !== 'new' && <div style={{
    fontSize: '11px',
    color: '#2563eb',
    marginTop: '4px'
  }}>✓ Existing wallet selected</div>}
                {publicKey && publicKeyValidation.valid && publicKeyValidation.isCustom && <div style={{
    fontSize: '11px',
    color: '#f59e0b',
    marginTop: '4px'
  }}>⚠ Custom blockchain - no validation</div>}
                {existingWalletMatch && selectedWalletMode === 'new' && <div style={{
    fontSize: '11px',
    color: '#2563eb',
    marginTop: '4px',
    padding: '6px 8px',
    backgroundColor: '#eff6ff',
    borderRadius: '4px',
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center'
  }}>
                    <span>⚡ Wallet already exists in your org</span>
                    <button onClick={() => selectExistingWallet(existingWalletEntries.find(e => e.wallet.publicKey === publicKey))} style={{
    fontSize: '11px',
    color: '#2563eb',
    background: 'none',
    border: 'none',
    cursor: 'pointer',
    textDecoration: 'underline'
  }}>
                      Use it →
                    </button>
                  </div>}
              </div>
            </div>

            {}
            <div style={{
    maxWidth: '250px'
  }}>
              <div style={fieldGroupStyle}>
                <label style={labelStyle}>Blockchain</label>
                <input type="text" list="blockchain-options" value={blockchain} onChange={e => setBlockchain(e.target.value)} placeholder="stellar, solana, base, polygon..." style={inputStyle} />
                <datalist id="blockchain-options">
                  <option value="stellar" />
                  <option value="solana" />
                  <option value="base" />
                  <option value="polygon" />
                </datalist>
                <div style={{
    fontSize: '10px',
    color: '#6b7280',
    marginTop: '2px'
  }}>
                  Select or enter custom
                </div>
              </div>
            </div>
            </>}
          </div>

          <div style={{
    display: 'flex',
    gap: '8px'
  }}>
            <button onClick={() => copyToClipboard(getCurl('POST', '/ramp/onboarding-url', getOnboardingBody()), 'curl1')} style={secondaryButtonStyle}>
              {copied === 'curl1' ? '✓ Copied' : 'Copy curl'}
            </button>
          </div>
          {renderStepError('step1')}
          {onboardingResult && <div style={{
    marginTop: '16px'
  }}>
              <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '8px',
    marginBottom: '8px'
  }}>
                <span style={{
    padding: '4px 8px',
    backgroundColor: '#dcfce7',
    color: '#166534',
    borderRadius: '4px',
    fontSize: '12px',
    fontWeight: 500
  }}>200</span>
                <span style={{
    fontSize: '13px',
    color: '#6b7280'
  }}>Success</span>
              </div>
              <pre style={codeBlockStyle}>{JSON.stringify(onboardingResult, null, 2)}</pre>

              {}
              <div style={{
    marginTop: '16px',
    display: 'flex',
    alignItems: 'flex-start',
    gap: '16px'
  }}>
                {}
                <div style={{
    flex: 1
  }}>
                  <div style={{
    fontSize: '12px',
    color: '#6b7280'
  }}>
                    <strong style={{
    color: '#374151'
  }}>Optional:</strong> Visit the UI to enter information manually.
                  </div>
                  <div style={{
    marginTop: '6px',
    fontSize: '11px',
    color: '#9ca3af'
  }}>
                    URL valid for 15 minutes. Also used for Step 4.
                  </div>
                </div>

                {}
                <a href={onboardingResult.presigned_url} target="_blank" rel="noopener noreferrer" style={{
    textDecoration: 'none',
    display: 'block'
  }}>
                  <div style={{
    textAlign: 'center',
    marginBottom: '6px',
    fontSize: '12px',
    color: '#374151',
    fontWeight: 500
  }}>
                    Open UI <span style={{
    fontSize: '10px'
  }}>▶</span>
                  </div>
                  <div style={{
    width: '120px',
    backgroundColor: '#1a1a1a',
    borderRadius: '8px',
    padding: '10px',
    cursor: 'pointer',
    transition: 'box-shadow 0.15s',
    boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
  }}>
                    {}
                    <div style={{
    height: '6px',
    width: '65%',
    backgroundColor: '#374151',
    borderRadius: '3px',
    marginBottom: '5px'
  }} />
                    <div style={{
    height: '4px',
    width: '85%',
    backgroundColor: '#262626',
    borderRadius: '2px',
    marginBottom: '8px'
  }} />
                    {}
                    {[1, 2, 3].map((_, i) => <div key={i} style={{
    height: '16px',
    backgroundColor: '#d4f542',
    borderRadius: '3px',
    marginBottom: '4px',
    display: 'flex',
    alignItems: 'center',
    padding: '0 6px',
    gap: '5px'
  }}>
                        <div style={{
    width: '6px',
    height: '6px',
    backgroundColor: '#1a1a1a',
    borderRadius: '2px',
    opacity: 0.3
  }} />
                        <div style={{
    flex: 1,
    height: '5px',
    backgroundColor: '#1a1a1a',
    borderRadius: '2px',
    opacity: 0.2
  }} />
                      </div>)}
                  </div>
                </a>
              </div>
            </div>}
        </div>
      </div>

      {}
      <div style={{
    marginBottom: '24px',
    border: '1px solid #e5e7eb',
    borderRadius: '8px',
    overflow: 'hidden',
    opacity: selectedWalletMode !== 'new' || currentStep >= 1 ? 1 : 0.5
  }}>
        <div style={{
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: '12px 16px',
    backgroundColor: '#f8fafc',
    borderBottom: '1px solid #e5e7eb'
  }}>
          <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '8px'
  }}>
            <span style={{
    padding: '4px 8px',
    backgroundColor: '#6366f1',
    color: 'white',
    borderRadius: '4px',
    fontSize: '12px',
    fontWeight: 600
  }}>Step 2</span>
            <span style={{
    padding: '4px 8px',
    backgroundColor: '#3b82f6',
    color: 'white',
    borderRadius: '4px',
    fontSize: '12px',
    fontWeight: 600
  }}>POST</span>
            <span style={{
    fontFamily: 'monospace',
    fontSize: '14px'
  }}>/ramp/customer/{'{id}'}/kyc</span>
          </div>
          <button onClick={submitKycData} disabled={!((selectedWalletMode !== 'new' || currentStep >= 1) && apiKey && !loading)} style={tryItButtonStyle((selectedWalletMode !== 'new' || currentStep >= 1) && apiKey && !loading)}>
            {loading ? 'Loading...' : 'Try it'} {!loading && <span>▶</span>}
          </button>
        </div>
        <div style={{
    padding: '16px'
  }}>
          <p style={{
    color: '#6b7280',
    margin: 0,
    marginBottom: '16px'
  }}>
            Submit identity information for the customer.{' '}
            <a href="/api-reference/kyc/submit-kyc-identity-data" style={{
    color: '#0d9488'
  }}>View API reference →</a>
          </p>

          {(selectedWalletMode !== 'new' || currentStep >= 1) && <div style={{
    backgroundColor: '#f9fafb',
    padding: '16px',
    borderRadius: '6px',
    marginBottom: '16px',
    border: '1px solid #e5e7eb'
  }}>
              <div style={{
    display: 'grid',
    gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))',
    gap: '12px'
  }}>
                <div style={fieldGroupStyle}>
                  <label style={labelStyle}>Given Name</label>
                  <input type="text" value={givenName} onChange={e => setGivenName(e.target.value)} style={inputStyle} />
                </div>
                <div style={fieldGroupStyle}>
                  <label style={labelStyle}>Family Name</label>
                  <input type="text" value={familyName} onChange={e => setFamilyName(e.target.value)} style={inputStyle} />
                </div>
                <div style={fieldGroupStyle}>
                  <label style={labelStyle}>Email</label>
                  <input type="email" value={email} onChange={e => setEmail(e.target.value)} placeholder="juan@example.com" style={inputStyle} />
                </div>
                <div style={fieldGroupStyle}>
                  <label style={labelStyle}>Phone Number</label>
                  <input type="text" value={phoneNumber} onChange={e => setPhoneNumber(e.target.value)} placeholder="+521234567890" style={inputStyle} />
                </div>
                <div style={fieldGroupStyle}>
                  <label style={labelStyle}>Occupation</label>
                  <input type="text" value={occupation} onChange={e => setOccupation(e.target.value)} placeholder="Software Engineer" style={inputStyle} />
                </div>
                <div style={fieldGroupStyle}>
                  <label style={labelStyle}>Date of Birth</label>
                  <input type="text" value={dateOfBirth} onChange={e => setDateOfBirth(e.target.value)} placeholder="YYYY-MM-DD" style={inputStyle} />
                </div>
                <div style={fieldGroupStyle}>
                  <label style={labelStyle}>Street</label>
                  <input type="text" value={street} onChange={e => setStreet(e.target.value)} style={inputStyle} />
                </div>
                <div style={fieldGroupStyle}>
                  <label style={labelStyle}>City</label>
                  <input type="text" value={city} onChange={e => setCity(e.target.value)} style={inputStyle} />
                </div>
                <div style={fieldGroupStyle}>
                  <label style={labelStyle}>Region/State</label>
                  <input type="text" value={region} onChange={e => setRegion(e.target.value)} style={inputStyle} />
                </div>
                <div style={fieldGroupStyle}>
                  <label style={labelStyle}>Postal Code</label>
                  <input type="text" value={postalCode} onChange={e => setPostalCode(e.target.value)} style={inputStyle} />
                </div>
                <div style={fieldGroupStyle}>
                  <label style={labelStyle}>Country</label>
                  <input type="text" value={country} onChange={e => setCountry(e.target.value)} placeholder="2-letter code" style={inputStyle} />
                </div>
                <div style={fieldGroupStyle}>
                  <label style={labelStyle}>ID Number</label>
                  <input type="text" value={idNumber} onChange={e => setIdNumber(e.target.value)} style={inputStyle} />
                </div>
                <div style={fieldGroupStyle}>
                  <label style={labelStyle}>ID Type</label>
                  <select value={idType} onChange={e => setIdType(e.target.value)} style={selectStyle}>
                    <option value="CURP">CURP (Mexico)</option>
                    <option value="RFC">RFC (Mexico)</option>
                    <option value="SSN">SSN (USA)</option>
                    <option value="CPF">CPF (Brazil)</option>
                    <option value="PASSPORT">Passport</option>
                    <option value="NATIONAL_ID">National ID</option>
                  </select>
                </div>
              </div>
            </div>}

          <div style={{
    display: 'flex',
    gap: '8px'
  }}>
            <button onClick={() => copyToClipboard(getCurl('POST', '/ramp/customer/' + customerId + '/kyc', getKycBody()), 'curl2')} style={secondaryButtonStyle}>
              {copied === 'curl2' ? '✓ Copied' : 'Copy curl'}
            </button>
          </div>
          {renderStepError('step2')}
          {kycSubmitResult && <div style={{
    marginTop: '16px'
  }}>
              <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '8px',
    marginBottom: '8px'
  }}>
                <span style={{
    padding: '4px 8px',
    backgroundColor: '#dcfce7',
    color: '#166534',
    borderRadius: '4px',
    fontSize: '12px',
    fontWeight: 500
  }}>200</span>
                <span style={{
    fontSize: '13px',
    color: '#6b7280'
  }}>Success</span>
              </div>
              <pre style={codeBlockStyle}>{JSON.stringify(kycSubmitResult, null, 2)}</pre>
            </div>}
        </div>
      </div>

      {}
      <div style={{
    marginBottom: '24px',
    border: '1px solid #e5e7eb',
    borderRadius: '8px',
    overflow: 'hidden',
    opacity: selectedWalletMode !== 'new' || currentStep >= 1 ? 1 : 0.5
  }}>
        <div style={{
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: '12px 16px',
    backgroundColor: '#f8fafc',
    borderBottom: '1px solid #e5e7eb'
  }}>
          <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '8px'
  }}>
            <span style={{
    padding: '4px 8px',
    backgroundColor: '#6366f1',
    color: 'white',
    borderRadius: '4px',
    fontSize: '12px',
    fontWeight: 600
  }}>Step 3</span>
            <span style={{
    padding: '4px 8px',
    backgroundColor: '#3b82f6',
    color: 'white',
    borderRadius: '4px',
    fontSize: '12px',
    fontWeight: 600
  }}>POST</span>
            <span style={{
    fontFamily: 'monospace',
    fontSize: '14px'
  }}>/ramp/customer/{'{id}'}/kyc/documents</span>
          </div>
          <button onClick={uploadAllDocuments} disabled={!((selectedWalletMode !== 'new' || currentStep >= 1) && apiKey && !loading && publicKey && (selfieData || idFrontData))} style={tryItButtonStyle((selectedWalletMode !== 'new' || currentStep >= 1) && apiKey && !loading && publicKey && (selfieData || idFrontData))}>
            {loading ? 'Uploading...' : 'Upload'} {!loading && <span>▶</span>}
          </button>
        </div>
        <div style={{
    padding: '16px'
  }}>
          <p style={{
    color: '#6b7280',
    margin: 0,
    marginBottom: '16px'
  }}>
            Upload identity documents (government ID + selfie).{' '}
            <a href="/api-reference/kyc/upload-kyc-documents" style={{
    color: '#0d9488'
  }}>View API reference →</a>
          </p>

          {(selectedWalletMode !== 'new' || currentStep >= 1) && <div style={{
    backgroundColor: '#f9fafb',
    padding: '16px',
    borderRadius: '6px',
    marginBottom: '16px',
    border: '1px solid #e5e7eb'
  }}>
              <div style={{
    display: 'grid',
    gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
    gap: '16px'
  }}>
                <div style={fieldGroupStyle}>
                  <label style={labelStyle}>Selfie Photo</label>
                  <input type="file" accept="image/jpeg,image/png" onChange={e => handleFileUpload(e, setSelfieData)} style={{
    fontSize: '13px'
  }} />
                  {selfieData && <div style={{
    fontSize: '11px',
    color: '#22c55e',
    marginTop: '4px'
  }}>✓ File loaded</div>}
                </div>
                <div style={fieldGroupStyle}>
                  <label style={labelStyle}>ID Front</label>
                  <input type="file" accept="image/jpeg,image/png" onChange={e => handleFileUpload(e, setIdFrontData)} style={{
    fontSize: '13px'
  }} />
                  {idFrontData && <div style={{
    fontSize: '11px',
    color: '#22c55e',
    marginTop: '4px'
  }}>✓ File loaded</div>}
                </div>
                <div style={fieldGroupStyle}>
                  <label style={labelStyle}>ID Back (if applicable)</label>
                  <input type="file" accept="image/jpeg,image/png" onChange={e => handleFileUpload(e, setIdBackData)} style={{
    fontSize: '13px'
  }} />
                  {idBackData && <div style={{
    fontSize: '11px',
    color: '#22c55e',
    marginTop: '4px'
  }}>✓ File loaded</div>}
                </div>
              </div>
              <div style={{
    fontSize: '12px',
    color: '#6b7280',
    marginTop: '12px'
  }}>
                Formats: JPEG, PNG • Max size: 10MB per image
              </div>
            </div>}

          {renderStepError('step3')}
          {documentResult && Object.keys(documentResult).length > 0 && <div style={{
    marginTop: '16px'
  }}>
              <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '8px',
    marginBottom: '8px'
  }}>
                <span style={{
    padding: '4px 8px',
    backgroundColor: '#dcfce7',
    color: '#166534',
    borderRadius: '4px',
    fontSize: '12px',
    fontWeight: 500
  }}>200</span>
                <span style={{
    fontSize: '13px',
    color: '#6b7280'
  }}>Documents uploaded</span>
              </div>
              <pre style={codeBlockStyle}>{JSON.stringify(documentResult, null, 2)}</pre>
            </div>}
        </div>
      </div>

      {}
      <div style={{
    marginBottom: '24px',
    border: '1px solid #e5e7eb',
    borderRadius: '8px',
    overflow: 'hidden',
    opacity: currentStep >= 3 ? 1 : 0.5
  }}>
        <div style={{
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: '12px 16px',
    backgroundColor: '#f8fafc',
    borderBottom: '1px solid #e5e7eb'
  }}>
          <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '8px'
  }}>
            <span style={{
    padding: '4px 8px',
    backgroundColor: '#6366f1',
    color: 'white',
    borderRadius: '4px',
    fontSize: '12px',
    fontWeight: 600
  }}>Step 4</span>
            <span style={{
    padding: '4px 8px',
    backgroundColor: '#3b82f6',
    color: 'white',
    borderRadius: '4px',
    fontSize: '12px',
    fontWeight: 600
  }}>POST</span>
            <span style={{
    fontFamily: 'monospace',
    fontSize: '14px'
  }}>/ramp/agreements/*</span>
          </div>
          <button onClick={acceptAllAgreements} disabled={currentStep < 3 || !apiKey || loading || !presignedUrl} style={tryItButtonStyle(currentStep >= 3 && apiKey && !loading && presignedUrl)}>
            {loading ? 'Accepting...' : 'Accept All'} {!loading && <span>▶</span>}
          </button>
        </div>
        <div style={{
    padding: '16px'
  }}>
          <p style={{
    color: '#6b7280',
    margin: 0,
    marginBottom: '16px'
  }}>
            Accept required legal agreements using the presigned URL.{' '}
            <a href="/api-reference/agreements/accept-electronic-signature-consent" style={{
    color: '#0d9488'
  }}>View API reference →</a>
          </p>

          {currentStep >= 3 && <div style={{
    backgroundColor: '#fefce8',
    padding: '12px',
    borderRadius: '6px',
    marginBottom: '16px',
    border: '1px solid #fef08a'
  }}>
              <div style={{
    fontSize: '13px',
    color: '#854d0e'
  }}>
                <strong>User Authorization Required:</strong> These agreements create legally binding obligations.
                In production, ensure the actual end user authorizes these requests.
              </div>
            </div>}

          <div style={{
    marginBottom: '12px'
  }}>
            <div style={{
    fontSize: '13px',
    color: '#374151',
    marginBottom: '8px'
  }}>Agreements to accept:</div>
            <ul style={{
    margin: 0,
    paddingLeft: '20px',
    fontSize: '13px',
    color: '#6b7280'
  }}>
              <li style={{
    marginBottom: '4px'
  }}>
                Electronic Signature Consent {agreementResults['electronic-signature'] && <span style={{
    color: '#22c55e'
  }}>✓</span>}
              </li>
              <li style={{
    marginBottom: '4px'
  }}>
                Terms and Conditions {agreementResults['terms-and-conditions'] && <span style={{
    color: '#22c55e'
  }}>✓</span>}
              </li>
              <li>
                Customer Agreement {agreementResults['customer-agreement'] && <span style={{
    color: '#22c55e'
  }}>✓</span>}
              </li>
            </ul>
          </div>

          {renderStepError('step4')}
          {Object.keys(agreementResults).length > 0 && <div style={{
    marginTop: '16px'
  }}>
              <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '8px',
    marginBottom: '8px'
  }}>
                <span style={{
    padding: '4px 8px',
    backgroundColor: '#dcfce7',
    color: '#166534',
    borderRadius: '4px',
    fontSize: '12px',
    fontWeight: 500
  }}>200</span>
                <span style={{
    fontSize: '13px',
    color: '#6b7280'
  }}>{Object.keys(agreementResults).length}/3 agreements accepted</span>
              </div>
            </div>}
        </div>
      </div>

      {}
      <div style={{
    marginBottom: '24px',
    border: '1px solid #e5e7eb',
    borderRadius: '8px',
    overflow: 'hidden',
    opacity: selectedWalletMode !== 'new' || currentStep >= 1 ? 1 : 0.5
  }}>
        <div style={{
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: '12px 16px',
    backgroundColor: '#f8fafc',
    borderBottom: '1px solid #e5e7eb'
  }}>
          <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '8px'
  }}>
            <span style={{
    padding: '4px 8px',
    backgroundColor: '#6366f1',
    color: 'white',
    borderRadius: '4px',
    fontSize: '12px',
    fontWeight: 600
  }}>Step 5</span>
            <span style={{
    padding: '4px 8px',
    backgroundColor: '#22c55e',
    color: 'white',
    borderRadius: '4px',
    fontSize: '12px',
    fontWeight: 600
  }}>GET</span>
            <span style={{
    fontFamily: 'monospace',
    fontSize: '14px'
  }}>/ramp/customer/{'{id}'}/kyc/{'{pubkey}'}</span>
          </div>
          <button onClick={checkKycStatus} disabled={!((selectedWalletMode !== 'new' || currentStep >= 1) && apiKey && !loading && publicKey)} style={tryItButtonStyle((selectedWalletMode !== 'new' || currentStep >= 1) && apiKey && !loading && publicKey)}>
            {loading ? 'Loading...' : 'Check Status'} {!loading && <span>▶</span>}
          </button>
        </div>
        <div style={{
    padding: '16px'
  }}>
          <p style={{
    color: '#6b7280',
    margin: 0,
    marginBottom: '16px'
  }}>
            Check the KYC verification status.{' '}
            <a href="/api-reference/kyc/get-kyc-status" style={{
    color: '#0d9488'
  }}>View API reference →</a>
          </p>

          <div style={{
    display: 'flex',
    gap: '8px'
  }}>
            <button onClick={() => copyToClipboard(getCurl('GET', '/ramp/customer/' + customerId + '/kyc/' + publicKey), 'curl5')} style={secondaryButtonStyle}>
              {copied === 'curl5' ? '✓ Copied' : 'Copy curl'}
            </button>
          </div>

          {renderStepError('step5')}
          {kycStatus && <div style={{
    marginTop: '16px'
  }}>
              <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '8px',
    marginBottom: '8px'
  }}>
                <span style={{
    padding: '4px 8px',
    backgroundColor: '#dcfce7',
    color: '#166534',
    borderRadius: '4px',
    fontSize: '12px',
    fontWeight: 500
  }}>200</span>
                <span style={{
    padding: '4px 8px',
    backgroundColor: kycStatus.status === 'approved' ? '#dcfce7' : kycStatus.status === 'rejected' ? '#fef2f2' : '#fef9c3',
    color: kycStatus.status === 'approved' ? '#166534' : kycStatus.status === 'rejected' ? '#dc2626' : '#854d0e',
    borderRadius: '4px',
    fontSize: '12px',
    fontWeight: 500
  }}>
                  Status: {kycStatus.status}
                </span>
              </div>
              <pre style={codeBlockStyle}>{JSON.stringify(kycStatus, null, 2)}</pre>
              {kycStatus.status === 'proposed' && <div style={{
    padding: '12px',
    backgroundColor: '#eff6ff',
    color: '#1e40af',
    borderRadius: '6px',
    marginTop: '12px',
    fontSize: '13px'
  }}>
                  KYC submitted for review. In sandbox, fake data is accepted — approval will be processed automatically. You can also check status at{' '}
                  <a href="https://devnet.etherfuse.com/ramp" target="_blank" rel="noopener noreferrer" style={{
    color: '#3b82f6'
  }}>
                    devnet.etherfuse.com/ramp
                  </a>
                </div>}
            </div>}
        </div>
      </div>

    </div>;
};

Etherfuse supports two types of customers — **personal** (individual people, verified via KYC) and **business** (legal entities, verified via KYB). The right onboarding flow depends on which type you're onboarding. Both work the same in sandbox as in production.

You declare the customer type via `accountType` (`personal` or `business`) when you create the child organization. See [POST /ramp/organization](/api-reference/organizations/create-a-child-organization) for the field reference.

## Personal customers (KYC)

A personal customer is an individual person. They complete **Know Your Customer** verification — submit identity data, upload a selfie and a government-issued ID, and sign three agreements (electronic signature, terms and conditions, customer agreement).

You have two ways to collect that data:

| Approach                      | Best For                                                                                                               | Guide                                                |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| **Hosted UI (Presigned URL)** | Fastest integration — Etherfuse handles identity collection, document upload, and agreement signing in one hosted page | [Hosted UI Flow](/guides/onboarding-hosted)          |
| **Programmatic**              | White-label experience — collect data in your own UI, submit via API, full control over the customer journey           | [Programmatic Flow](/guides/onboarding-programmatic) |

<Tip>
  **Start with the Hosted UI** if you want the fastest path to testing. You can migrate to Programmatic later without re-onboarding existing customers.

  **Hybrid** is also common — use the programmatic API for identity and documents, then redirect the user to the presigned URL for agreements and compliance verification.
</Tip>

<Warning>
  **KYC Requirements (Production)**

  In production, the following must be submitted for each personal customer:

  1. **Selfie** — A photo of the customer's face
  2. **Government-issued identification** — A valid ID document (passport, driver's license, national ID)
  3. **Proof of address** — Document showing the customer's current address (utility bill, bank statement)

  If the government-issued ID includes the customer's address (e.g., a driver's license), it can satisfy both requirements. After all data is submitted, Etherfuse reviews the information for accuracy before approving the customer.

  In **sandbox**, you can complete KYC using fake data — no real personal information is required.
</Warning>

### Try it live

The tutorial below walks through the personal-customer KYC flow against a real environment using your API key. It demonstrates the **programmatic** approach with a hybrid presigned-URL step for agreements; the same shape applies to the pure hosted flow except the customer completes Steps 2–4 in the Etherfuse UI instead of via API. Toggle Sandbox/Production at the top.

<KycTutorial />

## Business customers (KYB)

A business customer is a legal entity (company, LLC, etc.). There is no per-user KYC — instead, Etherfuse **Know Your Business**-approves the organization as a whole, and once approved every wallet under that org is fully compliant. No selfies, ID documents, agreement signing, or presigned URLs.

Business customers are onboarded **only via the programmatic API**. Create a child organization with `accountType: "business"` and Etherfuse will review and KYB-approve it:

```bash theme={null}
curl -X POST https://api.etherfuse.com/ramp/organization \
  -H "Authorization: <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "<customer_uuid>",
    "displayName": "Acme Corp",
    "accountType": "business",
    "wallets": [
      { "publicKey": "<wallet_public_key>", "blockchain": "solana" }
    ]
  }'
```

See [POST /ramp/organization](/api-reference/organizations/create-a-child-organization) for the full schema. The hosted UI flow is **not available** for business customers — `POST /ramp/onboarding-url` always creates a personal org.

<Info>
  Once Etherfuse KYB-approves the organization, you can skip the KYC, document upload, agreement signing, and presigned URL steps entirely for any wallets registered under it.
</Info>

## Reference

<Expandable title="Checking KYC status">
  Query the current KYC status at any time (works for both approaches) via [GET /ramp/customer/\{id}/kyc/\{pubkey}](/api-reference/kyc/get-kyc-status).

  <CodeGroup>
    ```bash Sandbox theme={null}
    curl -H "Authorization: <api_key>" \
      https://api.sand.etherfuse.com/ramp/customer/<customer_uuid>/kyc/<pubkey>
    ```

    ```bash Production theme={null}
    curl -H "Authorization: <api_key>" \
      https://api.etherfuse.com/ramp/customer/<customer_uuid>/kyc/<pubkey>
    ```
  </CodeGroup>

  | Status                     | Description                                          |
  | -------------------------- | ---------------------------------------------------- |
  | `not_started`              | No KYC data submitted                                |
  | `proposed`                 | Data submitted, awaiting admin review                |
  | `approved`                 | KYC approved                                         |
  | `approved_chain_deploying` | Approved, on-chain marking in progress (Solana only) |
  | `rejected`                 | KYC rejected (see `currentRejectionReason`)          |

  ```json theme={null}
  {
    "customerId": "123e4567-e89b-12d3-a456-426614174000",
    "walletPublicKey": "9Qx7r...",
    "status": "proposed",
    "onChainMarked": false,
    "currentRejectionReason": null,
    "selfies": [...],
    "documents": [...],
    "currentKycInfo": {
      "email": "juan@example.com",
      "phoneNumber": "+521234567890",
      "name": { "givenName": "Juan", "familyName": "Garcia" },
      "address": { "street": "Av. Reforma 123", "city": "Mexico City", "region": "CDMX", "postalCode": "06600", "country": "MX" }
    },
    "approvedAt": null
  }
  ```
</Expandable>

<Expandable title="Handling rejections">
  If KYC is rejected, the `updateReason` in the webhook (or `currentRejectionReason` in the status response) explains why. To resubmit:

  1. Address the rejection reason (e.g., upload a clearer document)
  2. Submit new data via the same endpoints
  3. The new submission creates a fresh review; old rejected data remains for audit purposes
</Expandable>

<Expandable title="Webhooks">
  Register a webhook for `kyc_updated` events to receive status updates. See [POST /ramp/webhook](/api-reference/webhooks/create-webhook) for setup and payload details.

  **Approved:**

  ```json theme={null}
  {
    "kyc_updated": {
      "customerId": "123e4567-e89b-12d3-a456-426614174000",
      "walletPublicKey": "9Qx7r...",
      "approved": true,
      "updateReason": "KYC approved by admin"
    }
  }
  ```

  **Rejected:**

  ```json theme={null}
  {
    "kyc_updated": {
      "customerId": "123e4567-e89b-12d3-a456-426614174000",
      "walletPublicKey": "9Qx7r...",
      "approved": false,
      "updateReason": "ID document is expired. Please submit a valid government-issued ID."
    }
  }
  ```
</Expandable>
