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

# Create Vendor

> Creates a vendor with a single payout method.
The bank account fields required in `payoutMethod` depend on the method's currency, bank country and type.

export const PayoutSchemaExplorer = () => {
  const LOCAL_RAILS = [{
    currencies: "USD",
    countries: "US",
    types: "ACH,Wire",
    fields: "accountNumber,routingCode(9)"
  }, {
    currencies: "CAD",
    countries: "CA",
    types: "EFT",
    fields: "accountNumber,bankCode(4),branchCode(5)"
  }, {
    currencies: "EUR",
    countries: "EU,GB",
    types: "SEPA",
    fields: "bicSwift,iban"
  }, {
    currencies: "GBP",
    countries: "GB",
    types: "FPS",
    fields: "accountNumber,sortCode(6)"
  }, {
    currencies: "AED",
    countries: "AE",
    types: "UAEFTS",
    fields: "iban(AE),bicSwift"
  }, {
    currencies: "CHF",
    countries: "CH",
    types: "SIC",
    fields: "iban(CH),bicSwift"
  }, {
    currencies: "CZK",
    countries: "CZ",
    types: "CERTIS",
    fields: "iban(CZ),bicSwift"
  }, {
    currencies: "DKK",
    countries: "DK",
    types: "DKSMC",
    fields: "iban(DK),bicSwift"
  }, {
    currencies: "HKD",
    countries: "HK",
    types: "CCASS",
    fields: "accountNumber,bankCode(3),branchCode(3)"
  }, {
    currencies: "NOK",
    countries: "NO",
    types: "NICS",
    fields: "iban(NO),bicSwift"
  }, {
    currencies: "PLN",
    countries: "PL",
    types: "SORBNET",
    fields: "iban(PL),bicSwift"
  }, {
    currencies: "SEK",
    countries: "SE",
    types: "RIX",
    fields: "iban(SE),bicSwift"
  }, {
    currencies: "ZAR",
    countries: "ZA",
    types: "SAMOS",
    fields: "accountNumber,bicSwift"
  }, {
    currencies: "ILS",
    countries: "IL",
    types: "ZAHAV",
    fields: "iban(IL),bicSwift"
  }, {
    currencies: "AUD",
    countries: "AU",
    types: "NPP",
    fields: "accountNumber,bsbCode(6-9)"
  }, {
    currencies: "SGD",
    countries: "SG",
    types: "IBG",
    fields: "accountNumber,bankCode(4),branchCode(3)"
  }, {
    currencies: "NZD",
    countries: "NZ",
    types: "BECS",
    fields: "accountNumber,bsbCode(6)"
  }, {
    currencies: "MXN",
    countries: "MX",
    types: "SPEI",
    fields: "accountNumber,bankCode(3),branchCode(3)"
  }];
  const SWIFT_ROWS = [{
    currencies: "USD,CAD,GBP,AED,CHF,CZK,DKK,HKD,NOK,PLN,SEK,ZAR,ILS,AUD,NZD",
    countries: "EU",
    types: "Swift",
    fields: "bicSwift,iban"
  }, {
    currencies: "USD,AED,CHF,CZK,DKK,HKD,NOK,PLN,SEK,ZAR,ILS,AUD,NZD",
    countries: "GB",
    types: "Swift",
    fields: "bicSwift,iban"
  }, {
    currencies: "USD,EUR,AED,CHF,CZK,DKK,HKD,NOK,PLN,SEK,ZAR,ILS,AUD,NZD",
    countries: "AE",
    types: "Swift",
    fields: "bicSwift,iban"
  }, {
    currencies: "USD,AED,CHF,CZK,DKK,HKD,NOK,PLN,SEK,ZAR,ILS",
    countries: "EG,QA,SA",
    types: "Swift",
    fields: "bicSwift(8-11),iban"
  }, {
    currencies: "EUR,GBP",
    countries: "TR",
    types: "Swift",
    fields: "bicSwift,iban"
  }, {
    currencies: "EUR",
    countries: "SC,CH",
    types: "Swift",
    fields: "bicSwift,iban"
  }, {
    currencies: "AUD",
    countries: "DK",
    types: "Swift",
    fields: "bicSwift,iban"
  }, {
    currencies: "USD,AED,CHF,CZK,DKK,HKD,NOK,PLN,SEK,ZAR,ILS",
    countries: "CA,MY,MV,MX,TH,PA,UY",
    types: "Swift",
    fields: "accountNumber,bicSwift"
  }, {
    currencies: "USD,EUR,AED,CHF,CZK,DKK,HKD,NOK,PLN,SEK,ZAR,ILS",
    countries: "SG,ID",
    types: "Swift",
    fields: "accountNumber,bicSwift"
  }, {
    currencies: "USD,CAD,EUR,GBP,AED,CHF,CZK,DKK,NOK,PLN,SEK,ZAR,ILS",
    countries: "HK",
    types: "Swift",
    fields: "accountNumber,bicSwift"
  }, {
    currencies: "EUR",
    countries: "ZA",
    types: "Swift",
    fields: "accountNumber,bicSwift"
  }, {
    currencies: "CAD,AED,CHF,CZK,DKK,HKD,NOK,PLN,SEK,ZAR,ILS",
    countries: "US",
    types: "Swift",
    fields: "accountNumber,bicSwift,routingCode(9)"
  }, {
    currencies: "USD,AED,CHF,CZK,DKK,HKD,NOK,PLN,SEK,ZAR,ILS",
    countries: "IN",
    types: "Swift",
    fields: "bankAddress,accountNumber,bicSwift"
  }, {
    currencies: "USD,AED,CHF,CZK,DKK,HKD,NOK,PLN,SEK,ZAR,ILS",
    countries: "CO",
    types: "Swift",
    fields: "accountNumber,bicSwift",
    note: "Contact phone and tax ID are also required at the contact level for CO."
  }];
  const ACCOUNT_NUMBER_RULES = {
    US: "1–17 chars, alphanumeric",
    CA: "7–12 chars, alphanumeric",
    GB: "exactly 8 digits",
    AU: "1–9 digits",
    SG: "6–21 chars, alphanumeric",
    HK: "6–21 chars, alphanumeric",
    MY: "4–19 digits",
    MV: "10–17 digits",
    IN: "4–20 digits",
    ID: "9–20 chars, alphanumeric",
    MX: "exactly 18 digits (CLABE)",
    TH: "8–15 digits",
    CO: "8–20 digits",
    ZA: "9–17 digits",
    NZ: "10–16 digits",
    PA: "5–20 digits",
    UY: "4–16 digits"
  };
  const BANK_STATE_COUNTRIES = ["US", "CA", "NZ", "AU"];
  const CURRENCY_NAMES = {
    USD: "United States Dollar",
    CAD: "Canadian Dollar",
    EUR: "Euro",
    GBP: "British Pound",
    AED: "United Arab Emirates Dirham",
    CHF: "Swiss Franc",
    CZK: "Czech Koruna",
    DKK: "Danish Krone",
    HKD: "Hong Kong Dollar",
    NOK: "Norwegian Krone",
    PLN: "Polish Zloty",
    SEK: "Swedish Krona",
    ZAR: "South African Rand",
    ILS: "Israeli New Shekel",
    AUD: "Australian Dollar",
    SGD: "Singapore Dollar",
    NZD: "New Zealand Dollar",
    MXN: "Mexican Peso"
  };
  const COUNTRY_NAMES = {
    AD: "Andorra",
    AE: "United Arab Emirates",
    AT: "Austria",
    AU: "Australia",
    AX: "Åland Islands",
    BE: "Belgium",
    BG: "Bulgaria",
    BL: "Saint Barthélemy",
    CA: "Canada",
    CH: "Switzerland",
    CO: "Colombia",
    CY: "Cyprus",
    CZ: "Czech Republic",
    DE: "Germany",
    DK: "Denmark",
    EE: "Estonia",
    EG: "Egypt",
    ES: "Spain",
    FI: "Finland",
    FR: "France",
    GB: "United Kingdom",
    GF: "French Guiana",
    GP: "Guadeloupe",
    GR: "Greece",
    HK: "Hong Kong",
    HR: "Croatia",
    HU: "Hungary",
    ID: "Indonesia",
    IE: "Ireland",
    IL: "Israel",
    IN: "India",
    IS: "Iceland",
    IT: "Italy",
    LI: "Liechtenstein",
    LT: "Lithuania",
    LU: "Luxembourg",
    LV: "Latvia",
    MC: "Monaco",
    MF: "Saint Martin (French part)",
    MQ: "Martinique",
    MT: "Malta",
    MV: "Maldives",
    MX: "Mexico",
    MY: "Malaysia",
    NL: "Netherlands",
    NO: "Norway",
    NZ: "New Zealand",
    PA: "Panama",
    PL: "Poland",
    PM: "Saint Pierre and Miquelon",
    PT: "Portugal",
    QA: "Qatar",
    RE: "Reunion",
    RO: "Romania",
    SA: "Saudi Arabia",
    SC: "Seychelles",
    SE: "Sweden",
    SG: "Singapore",
    SI: "Slovenia",
    SK: "Slovakia",
    SM: "San Marino",
    TH: "Thailand",
    TR: "Türkiye",
    US: "United States of America",
    UY: "Uruguay",
    VA: "Holy See (Vatican City State)",
    YT: "Mayotte",
    ZA: "South Africa"
  };
  const EU_COUNTRIES = ["AD", "AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "AX", "FR", "GF", "GP", "MQ", "YT", "RE", "DE", "GR", "HU", "IS", "IE", "IT", "LV", "LT", "LU", "MT", "MC", "NL", "NO", "PL", "PT", "RO", "SK", "SI", "ES", "SE", "LI", "BL", "MF", "PM", "SM", "VA"];
  const currencyLabel = code => CURRENCY_NAMES[code] ? code + " - " + CURRENCY_NAMES[code] : code;
  const countryLabel = code => COUNTRY_NAMES[code] ? code + " - " + COUNTRY_NAMES[code] : code;
  const STATE_DATA = {
    US: [{
      code: "AK",
      name: "Alaska"
    }, {
      code: "AL",
      name: "Alabama"
    }, {
      code: "AR",
      name: "Arkansas"
    }, {
      code: "AZ",
      name: "Arizona"
    }, {
      code: "CA",
      name: "California"
    }, {
      code: "CO",
      name: "Colorado"
    }, {
      code: "CT",
      name: "Connecticut"
    }, {
      code: "DC",
      name: "District of Columbia"
    }, {
      code: "DE",
      name: "Delaware"
    }, {
      code: "FL",
      name: "Florida"
    }, {
      code: "GA",
      name: "Georgia"
    }, {
      code: "HI",
      name: "Hawaii"
    }, {
      code: "IA",
      name: "Iowa"
    }, {
      code: "ID",
      name: "Idaho"
    }, {
      code: "IL",
      name: "Illinois"
    }, {
      code: "IN",
      name: "Indiana"
    }, {
      code: "KS",
      name: "Kansas"
    }, {
      code: "KY",
      name: "Kentucky"
    }, {
      code: "LA",
      name: "Louisiana"
    }, {
      code: "MA",
      name: "Massachusetts"
    }, {
      code: "MD",
      name: "Maryland"
    }, {
      code: "ME",
      name: "Maine"
    }, {
      code: "MI",
      name: "Michigan"
    }, {
      code: "MN",
      name: "Minnesota"
    }, {
      code: "MO",
      name: "Missouri"
    }, {
      code: "MS",
      name: "Mississippi"
    }, {
      code: "MT",
      name: "Montana"
    }, {
      code: "NC",
      name: "North Carolina"
    }, {
      code: "ND",
      name: "North Dakota"
    }, {
      code: "NE",
      name: "Nebraska"
    }, {
      code: "NH",
      name: "New Hampshire"
    }, {
      code: "NJ",
      name: "New Jersey"
    }, {
      code: "NM",
      name: "New Mexico"
    }, {
      code: "NV",
      name: "Nevada"
    }, {
      code: "NY",
      name: "New York"
    }, {
      code: "OH",
      name: "Ohio"
    }, {
      code: "OK",
      name: "Oklahoma"
    }, {
      code: "OR",
      name: "Oregon"
    }, {
      code: "PA",
      name: "Pennsylvania"
    }, {
      code: "RI",
      name: "Rhode Island"
    }, {
      code: "SC",
      name: "South Carolina"
    }, {
      code: "SD",
      name: "South Dakota"
    }, {
      code: "TN",
      name: "Tennessee"
    }, {
      code: "TX",
      name: "Texas"
    }, {
      code: "UT",
      name: "Utah"
    }, {
      code: "VA",
      name: "Virginia"
    }, {
      code: "VT",
      name: "Vermont"
    }, {
      code: "WA",
      name: "Washington"
    }, {
      code: "WI",
      name: "Wisconsin"
    }, {
      code: "WV",
      name: "West Virginia"
    }, {
      code: "WY",
      name: "Wyoming"
    }],
    CA: [{
      code: "AB",
      name: "Alberta"
    }, {
      code: "BC",
      name: "British Columbia"
    }, {
      code: "MB",
      name: "Manitoba"
    }, {
      code: "NB",
      name: "New Brunswick"
    }, {
      code: "NL",
      name: "Newfoundland and Labrador"
    }, {
      code: "NS",
      name: "Nova Scotia"
    }, {
      code: "NT",
      name: "Northwest Territories"
    }, {
      code: "NU",
      name: "Nunavut"
    }, {
      code: "ON",
      name: "Ontario"
    }, {
      code: "PE",
      name: "Prince Edward Island"
    }, {
      code: "QC",
      name: "Quebec"
    }, {
      code: "SK",
      name: "Saskatchewan"
    }, {
      code: "YT",
      name: "Yukon"
    }],
    NZ: [{
      code: "AUK",
      name: "Auckland Region"
    }, {
      code: "BOP",
      name: "Bay of Plenty Region"
    }, {
      code: "CAN",
      name: "Canterbury Region"
    }, {
      code: "CIT",
      name: "Chatham Islands"
    }, {
      code: "GIS",
      name: "Gisborne District"
    }, {
      code: "HKB",
      name: "Hawke's Bay Region"
    }, {
      code: "MBH",
      name: "Marlborough Region"
    }, {
      code: "MWT",
      name: "Manawatu-Wanganui Region"
    }, {
      code: "NSN",
      name: "Nelson Region"
    }, {
      code: "NTL",
      name: "Northland Region"
    }, {
      code: "OTA",
      name: "Otago Region"
    }, {
      code: "STL",
      name: "Southland Region"
    }, {
      code: "TAS",
      name: "Tasman District"
    }, {
      code: "TKI",
      name: "Taranaki Region"
    }, {
      code: "WGN",
      name: "Wellington Region"
    }, {
      code: "WKO",
      name: "Waikato Region"
    }, {
      code: "WTC",
      name: "West Coast Region"
    }],
    AU: [{
      code: "ACT",
      name: "Australian Capital Territory"
    }, {
      code: "NSW",
      name: "New South Wales"
    }, {
      code: "NT",
      name: "Northern Territory"
    }, {
      code: "QLD",
      name: "Queensland"
    }, {
      code: "SA",
      name: "South Australia"
    }, {
      code: "TAS",
      name: "Tasmania"
    }, {
      code: "VIC",
      name: "Victoria"
    }, {
      code: "WA",
      name: "Western Australia"
    }]
  };
  const expand = rows => {
    const out = [];
    rows.forEach(row => {
      row.currencies.split(",").forEach(cur => {
        row.countries.split(",").forEach(ctry => {
          row.types.split(",").forEach(t => {
            out.push({
              currency: cur,
              country: ctry,
              type: t,
              fieldsRaw: row.fields,
              note: row.note
            });
          });
        });
      });
    });
    return out;
  };
  const ALL_ROWS = useMemo(() => expand(LOCAL_RAILS).concat(expand(SWIFT_ROWS)), []);
  const ALL_CURRENCIES = useMemo(() => Array.from(new Set(ALL_ROWS.map(r => r.currency))).sort(), [ALL_ROWS]);
  const countriesForCurrency = cur => {
    const set = new Set();
    let hasEuBucket = false;
    ALL_ROWS.forEach(r => {
      if (r.currency !== cur) return;
      if (r.country === "EU") {
        hasEuBucket = true;
        return;
      }
      set.add(r.country);
    });
    if (hasEuBucket) EU_COUNTRIES.forEach(c => set.add(c));
    return Array.from(set).sort();
  };
  const rowsFor = (cur, ctry) => {
    const exact = ALL_ROWS.filter(r => r.currency === cur && r.country === ctry);
    if (exact.length > 0) return exact;
    if (EU_COUNTRIES.indexOf(ctry) > -1) {
      return ALL_ROWS.filter(r => r.currency === cur && r.country === "EU");
    }
    return [];
  };
  const fieldRule = (spec, ctry, railType) => {
    const m = spec.match(/^([a-zA-Z]+)(?:\(([^)]+)\))?$/);
    const name = m[1];
    const arg = m[2];
    switch (name) {
      case "accountNumber":
        return {
          name,
          rule: ACCOUNT_NUMBER_RULES[ctry] || "Length and format vary by bank country"
        };
      case "iban":
        return arg ? {
          name,
          rule: "Valid IBAN, must be prefixed " + arg,
          sub: "This rail requires an " + arg + "-prefixed IBAN specifically."
        } : {
          name,
          rule: "Valid IBAN (any country accepted)",
          sub: railType ? railType + " destinations accept any valid IBAN." : "Any valid IBAN is accepted."
        };
      case "bicSwift":
        return {
          name,
          rule: arg ? arg.replace("-", " or ") + " characters" : "8 or 11 characters"
        };
      case "routingCode":
        return {
          name,
          rule: arg + " digits",
          sub: "US ABA / Fedwire routing number."
        };
      case "sortCode":
        return {
          name,
          rule: "Exactly " + arg + " digits",
          sub: "GB bank sort code."
        };
      case "bankCode":
        return {
          name,
          rule: arg + " digits",
          sub: "Meaning varies by rail — not comparable across currencies."
        };
      case "branchCode":
        return {
          name,
          rule: arg + " digits",
          sub: "Meaning varies by rail — not comparable across currencies."
        };
      case "bsbCode":
        return {
          name,
          rule: arg.indexOf("-") > -1 ? arg.replace("-", "–") + " digits" : "Exactly " + arg + " digits"
        };
      case "bankAddress":
        return {
          name,
          rule: "Required for this destination",
          sub: "No format constraint beyond being present."
        };
      default:
        return {
          name,
          rule: ""
        };
    }
  };
  const exampleValue = (fieldName, ctry) => {
    switch (fieldName) {
      case "accountNumber":
        return "10029384756";
      case "routingCode":
        return "114000093";
      case "sortCode":
        return "203045";
      case "bicSwift":
        return "EXAMPLEX";
      case "iban":
        return (ctry.length === 2 ? ctry : "GB") + "00EXAMPLE0000000000";
      case "bankCode":
        return "0001";
      case "branchCode":
        return "00001";
      case "bsbCode":
        return "000000";
      case "bankAddress":
        return "100 Example Street, City";
      default:
        return "";
    }
  };
  const [currency, setCurrency] = useState("USD");
  const [country, setCountry] = useState("US");
  const [type, setType] = useState("ACH");
  const [stateByCountry, setStateByCountry] = useState({
    US: "AK",
    CA: "AB",
    NZ: "AUK",
    AU: "ACT"
  });
  const [copied, setCopied] = useState(false);
  const [copyFailed, setCopyFailed] = useState(false);
  const countries = countriesForCurrency(currency);
  const matches = rowsFor(currency, country);
  const types = matches.map(m => m.type);
  const activeType = types.indexOf(type) > -1 ? type : types[0];
  const row = matches.find(m => m.type === activeType) || matches[0];
  const shapesDiffer = new Set(matches.map(m => m.fieldsRaw)).size > 1;
  const onCurrencyChange = e => {
    const cur = e.target.value;
    setCurrency(cur);
    const ctrys = countriesForCurrency(cur);
    if (ctrys.indexOf(country) === -1) setCountry(ctrys[0]);
  };
  if (!row) return null;
  const needsState = BANK_STATE_COUNTRIES.indexOf(country) > -1;
  const currentState = needsState ? stateByCountry[country] || STATE_DATA[country][0].code : null;
  const fieldInfos = row.fieldsRaw.split(",").map(spec => fieldRule(spec, country, row.type));
  const payoutFields = {};
  fieldInfos.forEach(info => {
    payoutFields[info.name] = exampleValue(info.name, country);
  });
  const ordered = {
    type: row.type,
    currency: currency,
    bankName: "Example Bank",
    bankCountry: country
  };
  if (needsState) ordered.bankState = currentState;
  Object.keys(payoutFields).forEach(name => {
    ordered[name] = payoutFields[name];
  });
  const json = JSON.stringify(ordered, null, 2);
  const highlighted = json.replace(/"([^"]+)":/g, '<span class="tok-key">"$1"</span>:').replace(/: "([^"]*)"/g, ': <span class="tok-str">"$1"</span>');
  const onCopied = () => {
    setCopied(true);
    setTimeout(() => setCopied(false), 1600);
  };
  const onCopyFailed = () => {
    setCopyFailed(true);
    setTimeout(() => setCopyFailed(false), 2200);
  };
  const fallbackCopy = () => {
    try {
      const textarea = document.createElement("textarea");
      textarea.value = json;
      textarea.style.position = "fixed";
      textarea.style.opacity = "0";
      document.body.appendChild(textarea);
      textarea.focus();
      textarea.select();
      const ok = document.execCommand("copy");
      document.body.removeChild(textarea);
      if (ok) onCopied(); else onCopyFailed();
    } catch (e) {
      onCopyFailed();
    }
  };
  const copyJson = () => {
    if (navigator.clipboard && navigator.clipboard.writeText) {
      navigator.clipboard.writeText(json).then(onCopied).catch(fallbackCopy);
    } else {
      fallbackCopy();
    }
  };
  return <div className="pse-wrap">
    <div className="pse">
      <style>{`
        .pse {
          --pse-surface: #ffffff;
          --pse-surface-2: #f4f6fa;
          --pse-ink: #0c0e11;
          --pse-ink-muted: #515357;
          --pse-ink-faint: #717377;
          --pse-accent: #2563eb;
          --pse-accent-ink: #1d4ed8;
          --pse-accent-soft: rgba(37, 99, 235, 0.1);
          --pse-amber: #b45309;
          --pse-amber-soft: #fef3e0;
          --pse-green: #15803d;
          --pse-green-soft: #e9f9ee;
          --pse-border: #e0e2e6;
          --pse-code-bg: #f4f6fa;
          --pse-shadow: 0 1px 2px rgba(12, 14, 17, 0.04), 0 8px 24px rgba(12, 14, 17, 0.06);
          color: var(--pse-ink);
          font-variant-numeric: tabular-nums;
          display: grid;
          grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
          align-items: stretch;
          column-gap: 0;
          row-gap: 16px;
          margin-top: 1.5rem;
        }
        .pse-wrap {
          container-type: inline-size;
          container-name: pse;
        }
        .dark .pse {
          --pse-surface: #14161a;
          --pse-surface-2: #181a1e;
          --pse-ink: #f4f6fa;
          --pse-ink-muted: #a0a2a6;
          --pse-ink-faint: #717377;
          --pse-accent: #3d82f7;
          --pse-accent-ink: #6aa0f9;
          --pse-accent-soft: rgba(96, 165, 250, 0.14);
          --pse-amber: #f5a524;
          --pse-amber-soft: rgba(245, 165, 36, 0.12);
          --pse-green: #4ade80;
          --pse-green-soft: rgba(74, 222, 128, 0.14);
          --pse-border: #27292c;
          --pse-code-bg: #0e0f12;
          --pse-shadow: 0 1px 2px rgba(0, 0, 0, 0.35), 0 8px 24px rgba(0, 0, 0, 0.4);
        }
        .pse * { box-sizing: border-box; }
        @media (prefers-reduced-motion: reduce) {
          .pse * { transition: none !important; animation: none !important; }
        }
        .pse .pse-panel {
          background: var(--pse-surface);
          border: 1px solid var(--pse-border);
          border-radius: 12px;
          box-shadow: var(--pse-shadow);
        }
        /* The controls sit in the narrow left column now, so they run as a
           single column — splitting them in two there would squeeze the
           select labels, which already truncate at this width. */
        .pse .pse-controls {
          padding: 16px 18px;
          display: grid;
          grid-template-columns: 1fr;
          gap: 12px;
          align-items: start;
        }
        .pse .pse-field-stack { display: flex; flex-direction: column; gap: 12px; }
        .pse .pse-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
        .pse .pse-field label {
          font-size: 11px;
          font-weight: 600;
          letter-spacing: 0.07em;
          text-transform: uppercase;
          color: var(--pse-ink-muted);
        }
        .pse select {
          appearance: none;
          font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
          font-size: 12px;
          font-weight: 500;
          background: var(--pse-surface-2) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%234b5768' stroke-width='2'><path d='M6 9l6 6 6-6'/></svg>") no-repeat right 12px center;
          color: var(--pse-ink);
          border: 1px solid var(--pse-border);
          border-radius: 8px;
          padding: 8px 30px 8px 10px;
          cursor: pointer;
          width: 100%;
        }
        .pse select:disabled { opacity: 0.5; cursor: not-allowed; }
        .pse select:focus-visible { outline: 2px solid var(--pse-accent); outline-offset: 1px; }
        .pse select:disabled {
          opacity: 1;
          background-color: var(--pse-surface);
          color: var(--pse-ink-muted);
          cursor: not-allowed;
        }
        .pse .pse-method-note {
          display: flex;
          align-items: center;
          gap: 6px;
          font-size: 12px;
          color: var(--pse-amber);
          min-height: 16px;
        }
        .pse .pse-method-note.pse-single {
          color: var(--pse-ink-faint);
        }
        .pse .pse-method-note .pse-dot {
          width: 6px; height: 6px; border-radius: 50%;
          background: var(--pse-amber);
          flex-shrink: 0;
        }
        /* Controls and Required fields read as one merged panel across row 1:
           the column gap is zero, the row stretches so both are the same
           height, and the controls' right border becomes the internal
           divider. Example request spans the full width below.
           display:contents promotes the two result cards onto the .pse grid
           so the markup stays as authored. */
        .pse > style { display: none; }
        .pse .pse-results { display: contents; }
        .pse .pse-controls {
          grid-column: 1;
          grid-row: 1;
          border-radius: 12px 0 0 12px;
        }
        .pse .pse-results > .pse-card:first-child {
          grid-column: 2;
          grid-row: 1;
          border-left: none;
          border-radius: 0 12px 12px 0;
        }
        .pse .pse-results > .pse-card:last-child {
          grid-column: 1 / span 2;
          grid-row: 2;
        }
        /* Too narrow to sit side by side — one column, each panel closed off
           on its own again. */
        @container pse (max-width: 560px) {
          .pse { grid-template-columns: 1fr; }
          .pse .pse-controls,
          .pse .pse-results > .pse-card:first-child,
          .pse .pse-results > .pse-card:last-child {
            grid-column: 1;
            grid-row: auto;
            border: 1px solid var(--pse-border);
            border-radius: 12px;
          }
        }
        .pse .pse-card { padding: 16px 18px 20px; }
        .pse .pse-card h2 {
          font-size: 13px;
          font-weight: 600;
          letter-spacing: 0.06em;
          text-transform: uppercase;
          color: var(--pse-ink-muted);
          margin: 0 0 12px;
        }
        .pse .pse-field-list { display: flex; flex-direction: column; gap: 0; margin: 0; }
        .pse .pse-field-row {
          display: flex;
          flex-direction: column;
          gap: 4px;
          padding: 9px 0;
          margin: 0;
          border-top: 1px solid var(--pse-border);
        }
        .pse .pse-field-row:first-child { border-top: none; padding-top: 0; }
        .pse code.pse-code {
          font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
          font-size: 13.5px;
          background: var(--pse-code-bg);
          border: 1px solid var(--pse-border);
          border-radius: 4px;
          padding: 2px 6px;
        }
        /* This site's theme decorates every <code> with backtick characters
           via ::before/::after. Our chips already signal "code" with their
           own background/border, so drop the extra backticks to avoid
           double-signaling the same thing. */
        .pse code.pse-code::before,
        .pse code.pse-code::after {
          content: none !important;
        }
        .pse .pse-field-row .pse-rule {
          font-size: 13.5px;
          color: var(--pse-ink);
          line-height: 1.5;
        }
        .pse .pse-field-row .pse-rule .pse-sub {
          display: block;
          font-size: 12px;
          color: var(--pse-ink-faint);
          margin-top: 2px;
        }
        .pse .pse-field-row .pse-rule code.pse-code { font-size: 12.5px; padding: 1px 5px; }
        .pse p { margin: 0; }
        .pse .pse-case-note {
          margin: 14px 0 0;
          padding: 10px 14px;
          background: var(--pse-amber-soft);
          border-left: 3px solid var(--pse-amber);
          border-radius: 0 6px 6px 0;
          font-size: 13px;
          color: var(--pse-ink);
        }
        .pse .pse-example-head {
          display: flex;
          align-items: center;
          justify-content: space-between;
          margin-bottom: 16px;
        }
        .pse .pse-example-head h2 { margin: 0; }
        .pse .pse-copy-btn {
          appearance: none;
          border: 1px solid var(--pse-border);
          background: var(--pse-surface-2);
          color: var(--pse-ink);
          font-size: 12.5px;
          font-weight: 600;
          padding: 6px 12px;
          border-radius: 7px;
          cursor: pointer;
        }
        .pse .pse-copy-btn:hover { background: var(--pse-code-bg); }
        .pse .pse-copy-btn:focus-visible { outline: 2px solid var(--pse-accent); outline-offset: 1px; }
        .pse .pse-copy-btn.done { background: var(--pse-green-soft); border-color: var(--pse-green); color: var(--pse-green); }
        .pse .pse-copy-btn.failed { background: var(--pse-amber-soft); border-color: var(--pse-amber); color: var(--pse-amber); }
        .pse .pse-json {
          display: block;
          margin: 0;
          font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
          font-size: 13px;
          line-height: 1.6;
          background: var(--pse-code-bg);
          border: 1px solid var(--pse-border);
          border-radius: 10px;
          padding: 16px 18px;
          overflow-x: auto !important;
          white-space: pre !important;
          word-break: normal !important;
          color: var(--pse-ink);
        }
        .pse .tok-key { color: var(--pse-accent-ink); }
        .pse .tok-str { color: var(--pse-green); }
      `}</style>
      <div className="pse-panel pse-controls">
        <div className="pse-field">
          <label htmlFor="pse-sel-currency">Currency</label>
          <select id="pse-sel-currency" value={currency} onChange={onCurrencyChange}>
            {ALL_CURRENCIES.map(c => <option key={c} value={c}>
                {currencyLabel(c)}
              </option>)}
          </select>
        </div>
        <div className="pse-field-stack">
          <div className="pse-field">
            <label htmlFor="pse-sel-country">Bank Country</label>
            <select id="pse-sel-country" value={country} onChange={e => setCountry(e.target.value)}>
              {countries.map(c => <option key={c} value={c}>
                  {countryLabel(c)}
                </option>)}
            </select>
          </div>
          {needsState && <div className="pse-field">
              <label htmlFor="pse-sel-state">State / Province</label>
              <select id="pse-sel-state" value={currentState} onChange={e => setStateByCountry({
    ...stateByCountry,
    [country]: e.target.value
  })}>
                {STATE_DATA[country].map(s => <option key={s.code} value={s.code}>
                    {s.code} — {s.name}
                  </option>)}
              </select>
            </div>}
        </div>
        <div className="pse-field">
          <label htmlFor="pse-sel-type">Payout Type</label>
          <select id="pse-sel-type" value={activeType} onChange={e => setType(e.target.value)} disabled={types.length <= 1}>
            {types.map(t => <option key={t} value={t}>
                {t}
              </option>)}
          </select>
          <div className={"pse-method-note" + (!shapesDiffer ? " pse-single" : "")}>
            {types.length > 1 ? shapesDiffer ? <>
                  <span className="pse-dot"></span> {types.length} payout
                  types available for this pair — shapes differ, choose
                  carefully.
                </> : `${types.length} payout types available — identical field requirements either way.` : "Only payout type for this pair."}
          </div>
        </div>
      </div>
      <div className="pse-results">
        <div className="pse-panel pse-card">
          <h2>Required fields</h2>
          <div className="pse-field-list">
            {needsState && <div className="pse-field-row">
                <div>
                  <code className="pse-code">bankState</code>
                </div>
                <div className="pse-rule">
                  ISO state/province code — required because{" "}
                  <code className="pse-code">bankCountry</code> is {country}
                  <span className="pse-sub">
                    Selected above: {currentState}.
                  </span>
                </div>
              </div>}
            {fieldInfos.map((info, i) => <div className="pse-field-row" key={info.name + i}>
                <div>
                  <code className="pse-code">{info.name}</code>
                </div>
                <div className="pse-rule">
                  {info.rule}
                  {info.sub && <span className="pse-sub">{info.sub}</span>}
                </div>
              </div>)}
          </div>
          {row.note && <div className="pse-case-note">{row.note}</div>}
        </div>
        <div className="pse-panel pse-card">
          <div className="pse-example-head">
            <h2>Example request</h2>
            <button type="button" className={"pse-copy-btn" + (copied ? " done" : "") + (copyFailed ? " failed" : "")} onClick={copyJson}>
              {copied ? "Copied" : copyFailed ? "Couldn't copy — select below" : "Copy JSON"}
            </button>
          </div>
          <div className="pse-json" dangerouslySetInnerHTML={{
    __html: highlighted
  }} />
        </div>
      </div>
    </div>
    </div>;
};

Select a currency, bank country, and payout type below to see exactly which `payoutMethod` fields are required and their format rules, plus a matching example request.

<PayoutSchemaExplorer />


## OpenAPI

````yaml POST /vendors
openapi: 3.1.0
info:
  title: Grain API
  version: 1.0.0
  description: >-
    Grain's API provides a comprehensive set of endpoints for managing hedging,
    currency conversions, pricing, and fund movements - enabling seamless
    integration of FX workflows into your platform. Each API call follows REST
    conventions, uses secure authentication, and returns standardized responses
    for consistency across environments.
  license:
    name: Creative Commons Attribution 3.0
  contact:
    name: Grain Finance
    url: https://docs.grainfinance.co
    email: support@grainfinance.co
  termsOfService: https://grainfinance.co/terms-of-service/
  x-apiClientRegistration:
    url: https://grainfinance.co/partners
servers:
  - url: https://api.grainfinance.co/v1
    description: Grain API
security: []
tags:
  - name: Hedges
    description: >-
      Hedge objects represent transactions that lock exchange rates for future
      dates. A single hedge locks an exchange rate for a future transaction
      between two currencies.
  - name: Dual Hedges
    description: >-
      A dual hedge mitigates FX risk by automatically generating two linked
      hedge legs - a customer leg and a supplier leg - both routed through the
      customer's functional currency. This structure reduces exposure to
      currency fluctuations for both cash flow and accounting purposes, while
      allowing independent tracking and reporting of each hedge leg.
  - name: Customers
    description: >-
      Customer objects represent your end customers on the Grain platform. A
      customer must be created before a hedge or conversion can be initiated.
  - name: Pricing
    description: >-
      Pricing endpoints provide access to FX rates for currency pairs that are
      not being hedged. These endpoints can be used for live rate display or
      bulk retrieval to support high-volume quoting workflows.
  - name: Conversions
    description: >-
      Conversion objects represent currency conversions executed for immediate
      or near-term settlement.
  - name: Wallets
    description: Endpoints to view balances, manage funding, and perform transfers.
  - name: Vendors
    description: >-
      Vendor objects represent payees that payouts can be sent to. A vendor
      holds contact details and one or more payout methods — the bank accounts
      payouts are sent to.
paths:
  /vendors:
    post:
      tags:
        - Vendors
      description: >-
        Creates a vendor with a single payout method.

        The bank account fields required in `payoutMethod` depend on the
        method's currency, bank country and type.
      operationId: Create
      parameters: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateVendorRequest'
      responses:
        '201':
          description: Vendor created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomerVendor'
        '403':
          description: The request failed because the caller has insufficient permissions.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: The request failed because a business rule constraint was violated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: >-
            The request failed because it is either semantically incorrect or
            has failed business validation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '502':
          description: >-
            The request failed because an upstream provider was unavailable or
            returned an unexpected response.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - api-key: []
components:
  schemas:
    CreateVendorRequest:
      description: >-
        Request body for `POST /v1/vendors`. Creates a vendor with a single
        payout method. The conditional

        contact fields (`contactState`, `contactTaxId`, `contactPhoneNumber`,
        `companyRegisterNumber`) are

        required for some countries.
      properties:
        externalVendorId:
          $ref: '#/components/schemas/ExternalVendorId'
          description: The vendor id, as identified within your system.
          example: vendor_001
        contactFullName:
          type: string
          description: The full name of the vendor's contact person or business.
          example: Acme Supplies Ltd
        contactCountry:
          $ref: '#/components/schemas/CountryCode'
          description: The alpha-2 ISO 3166 country code of the vendor.
          example: US
        contactCity:
          type: string
          description: The city of the vendor's address.
          example: New York
        contactEmail:
          type: string
          description: The email address of the vendor.
          example: billing@acmesupplies.com
        contactStreetName:
          type: string
          description: The street name of the vendor's address.
          example: Madison Avenue
        contactBuildingName:
          type: string
          description: The building name or number of the vendor's address.
          example: '383'
        contactPostalCode:
          type: string
          description: The postal code of the vendor's address.
          example: '10017'
        contactState:
          type: string
          description: >-
            The ISO state code of the vendor's address. Required for vendors in
            US, CA, NZ and AU.
          example: NY
        contactTaxId:
          type: string
          description: The tax id of the vendor. Required for vendors in some countries.
          example: '901234567'
        contactPhoneNumber:
          type: string
          description: >-
            The phone number of the vendor's contact. Required for vendors in
            some countries.
          example: '+13212312321'
        companyRegisterNumber:
          type: string
          description: >-
            The company register number of the vendor. Required for vendors in
            some countries.
          example: '514455544'
        payoutMethod:
          $ref: '#/components/schemas/VendorPayoutMethod'
          description: The payout method to register for the vendor.
      required:
        - externalVendorId
        - contactFullName
        - contactCountry
        - contactCity
        - contactEmail
        - contactStreetName
        - contactBuildingName
        - contactPostalCode
        - payoutMethod
      type: object
      additionalProperties: false
    CustomerVendor:
      description: >-
        A Vendor represents a payee that payouts can be sent to. Returned by
        `POST /v1/vendors`,

        `GET /v1/vendors/{vendorId}`, and `GET /v1/vendors`.
      properties:
        vendorId:
          $ref: '#/components/schemas/UUID'
          description: The id of the vendor within the Grain platform.
          example: 1b0ada1e-fa1b-4cfe-a553-01b340c5650e
        externalVendorId:
          $ref: '#/components/schemas/ExternalVendorId'
          description: The vendor id, as identified within your system.
          example: vendor_001
        contact:
          $ref: '#/components/schemas/VendorContact'
          description: The contact details of the vendor.
        payoutMethods:
          items:
            $ref: '#/components/schemas/VendorPayoutMethod'
          type: array
          description: The payout methods registered for the vendor.
        createdAt:
          $ref: '#/components/schemas/IsoDateTime'
          description: When the vendor was created, as an ISO-8601 date-time in UTC.
          example: '2026-05-15T09:12:28.000Z'
        updatedAt:
          $ref: '#/components/schemas/IsoDateTime'
          description: When the vendor was last updated, as an ISO-8601 date-time in UTC.
          example: '2026-05-15T09:12:28.000Z'
      required:
        - vendorId
        - externalVendorId
        - contact
        - payoutMethods
        - createdAt
        - updatedAt
      type: object
      additionalProperties: false
    ErrorResponse:
      description: An API Error
      properties:
        message:
          type: string
          description: A short explanation of the error.
          example: Can't perform this action
        reason:
          type: string
          description: A detailed description of the reason for the failure.
          example: >-
            Supplied object is in *Cancelled* state, which prevents performing
            this action
      required:
        - message
      type: object
      additionalProperties: false
    ExternalVendorId:
      type: string
      example: vendor_001
      description: Partner's own reference ID for the vendor.
      pattern: ^[A-Za-z0-9_.\-]{1,64}$
    CountryCode:
      type: string
      example: GB
      description: The alpha-2 ISO 3166 country code of the entity's country.
      pattern: ^[A-Za-z]{2}$
    VendorPayoutMethod:
      description: >-
        A payout method registered for the vendor — the bank account payouts in
        `currency` are sent to.

        Bank account fields are corridor-specific: only the fields relevant to
        the method's currency,

        country, and type are returned.
      properties:
        type:
          $ref: '#/components/schemas/DisplayPaymentMethod'
          description: The payment method used to send payouts to this bank account.
          example: SEPA
        currency:
          $ref: '#/components/schemas/CurrencyCode'
          description: The alpha-3 ISO 4217 currency code of payouts sent with this method.
          example: EUR
        bankName:
          type: string
          description: The name of the bank holding the account.
          example: JPMorgan Chase Bank, N.A.
        bankCountry:
          $ref: '#/components/schemas/CountryCode'
          description: The alpha-2 ISO 3166 country code of the bank.
          example: US
        bankState:
          type: string
          description: The ISO state code of the bank's location, when applicable.
          example: NY
        accountNumber:
          type: string
          description: The account number, when applicable.
          example: '8290041523'
        iban:
          type: string
          description: The IBAN of the account, when applicable.
          example: DE89370400440532013000
          pattern: ^[A-Z0-9]{15,34}$
        bicSwift:
          type: string
          description: The SWIFT/BIC code of the bank, when applicable.
          example: CHASUS33XXX
          pattern: ^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$
        routingCode:
          type: string
          description: >-
            Country-specific routing code, when applicable — the nine-digit ABA
            routing number for USD payouts.
          example: '021000021'
        bankCode:
          type: string
          description: >-
            Country-specific bank code, when applicable — CAD (EFT), HKD
            (CCASS), SGD (IBG) and MXN (SPEI) payouts.
          example: '004'
        branchCode:
          type: string
          description: >-
            Country-specific branch code, when applicable — CAD (EFT), HKD
            (CCASS), SGD (IBG) and MXN (SPEI) payouts.
          example: '081'
        sortCode:
          type: string
          description: UK sort code, when applicable. Six digits, no hyphens.
          example: '560036'
          pattern: ^\d{6}$
        bsbCode:
          type: string
          description: Australian BSB code, when applicable. Six digits, no hyphens.
          example: '082902'
          pattern: ^\d{6}$
        bankAddress:
          type: string
          description: The full address of the bank, when provided.
          example: 383 Madison Avenue, New York, NY 10017
      required:
        - type
        - currency
        - bankName
        - bankCountry
      type: object
      additionalProperties: false
    UUID:
      type: string
      format: uuid
      pattern: >-
        [0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-4[0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}
    VendorContact:
      description: >-
        The contact details of the vendor. Conditional fields (`state`, `taxId`,
        `phoneNumber`,

        `companyRegisterNumber`) are returned only when they were provided.
      properties:
        fullName:
          type: string
          description: The full name of the vendor's contact person or business.
          example: Acme Supplies Ltd
        country:
          $ref: '#/components/schemas/CountryCode'
          description: The alpha-2 ISO 3166 country code of the vendor.
          example: US
        city:
          type: string
          description: The city of the vendor's address.
          example: New York
        email:
          type: string
          description: The email address of the vendor, when provided.
          example: billing@acmesupplies.com
        streetName:
          type: string
          description: The street name of the vendor's address, when provided.
          example: Madison Avenue
        buildingName:
          type: string
          description: The building name or number of the vendor's address, when provided.
          example: '383'
        postalCode:
          type: string
          description: The postal code of the vendor's address, when provided.
          example: '10017'
        state:
          type: string
          description: >-
            The ISO state code of the vendor's address, when provided. Required
            for vendors in US, CA, NZ and AU.
          example: NY
        taxId:
          type: string
          description: The tax id of the vendor, when provided.
          example: '901234567'
        phoneNumber:
          type: string
          description: The phone number of the vendor's contact, when provided.
          example: '+13212312321'
        companyRegisterNumber:
          type: string
          description: The company register number of the vendor, when provided.
          example: '514455544'
      required:
        - fullName
        - country
        - city
      type: object
      additionalProperties: false
    IsoDateTime:
      type: string
      example: '2026-05-15T09:12:28.000Z'
      format: date-time
      description: An ISO-8601 date-time string in UTC.
    DisplayPaymentMethod:
      enum:
        - SWIFT
        - SEPA
        - WIRE
        - ACH
        - FPS
        - CA ACSS
        - NPP
        - IBG
        - BECS
        - CCASS
        - TEF
        - UAEFTS
        - SIC
        - CERTIS
        - DKSMC
        - NICS
        - SORBNET
        - RIX
        - SAMOS
        - ZAHAV
        - SARIE
        - QPS
      type: string
    CurrencyCode:
      type: string
      example: EUR
      description: The alpha-3 ISO 4217 currency code of the related entity, in upper case.
      pattern: ^[A-Z]{3}$
  securitySchemes:
    api-key:
      type: http
      scheme: basic
      description: >-
        Basic authentication using the partner API keys from
        https://console.grainfinance.co/keys

````