Skip to content

Instantly share code, notes, and snippets.

@arnm
Created September 23, 2025 04:34
Show Gist options
  • Select an option

  • Save arnm/9ffb8e18b0c454e3eb5ae1dd277701b2 to your computer and use it in GitHub Desktop.

Select an option

Save arnm/9ffb8e18b0c454e3eb5ae1dd277701b2 to your computer and use it in GitHub Desktop.
wise-tht-2025
const axios = require("axios");
const { v4: uuidv4 } = require("uuid");
const BASE_URL = "https://api.sandbox.transferwise.tech";
const TOKEN = process.env.WISE_API_TOKEN;
class WiseApiError extends Error {
constructor(message, { status, traceId, code, details, originalError } = {}) {
super(message);
this.name = "WiseApiError";
this.status = status;
this.traceId = traceId;
this.code = code;
this.details = details;
this.originalError = originalError;
}
static fromAxios(err, context = "Wise API") {
if (err.response) {
// Server responded with error status (2xx range)
const status = err.response.status;
const traceId = err.response.headers?.["x-trace-id"];
const data = err.response.data;
const code = data?.errors?.[0]?.code || data?.code;
const message = `${context} failed with status ${status}`;
return new WiseApiError(message, { status, traceId, code, details: data, originalError: err });
} else if (err.request) {
// Request was made but no response received (network issues, timeouts)
const message = `${context} failed - no response received`;
return new WiseApiError(message, { code: err.code, details: { request: "No response received" }, originalError: err });
} else {
// Error in request setup
const message = `${context} failed - request setup error: ${err.message}`;
return new WiseApiError(message, { details: { setup: err.message }, originalError: err });
}
}
}
class WiseClient {
constructor({ token = TOKEN, baseURL = BASE_URL } = {}) {
if (!token) throw new Error("Missing API token. Set WISE_API_TOKEN.");
this.http = axios.create({
baseURL,
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
timeout: 30000,
});
}
handleError(context, error) {
if (axios.isAxiosError(error)) {
throw WiseApiError.fromAxios(error, context);
}
throw new WiseApiError(`${context} encountered a non-HTTP error`, { details: { message: error?.message } });
}
async listProfiles() {
try {
const res = await this.http.get("/v2/profiles");
return res.data;
} catch (error) {
this.handleError("List Profiles", error);
}
}
async createQuote(profileId, { sourceCurrency, targetCurrency, sourceAmount } = {}) {
try {
const res = await this.http.post(`/v3/profiles/${profileId}/quotes`, {
sourceCurrency,
targetCurrency,
sourceAmount,
});
return res.data;
} catch (error) {
this.handleError("Create Quote", error);
}
}
async createRecipient(profileId, { currency, type, accountHolderName, details } = {}) {
try {
const res = await this.http.post("/v1/accounts", {
currency,
type,
profile: profileId,
accountHolderName,
details,
});
return res.data;
} catch (error) {
this.handleError("Create Recipient", error);
}
}
async createTransfer(quoteUuid, targetAccount, { reference, customerTransactionId } = {}) {
try {
const res = await this.http.post("/v1/transfers", {
targetAccount,
quoteUuid,
customerTransactionId: customerTransactionId || uuidv4(),
details: { reference },
});
return res.data;
} catch (error) {
this.handleError("Create Transfer", error);
}
}
}
function findPersonalProfileId(profiles) {
if (!Array.isArray(profiles) || profiles.length === 0) return null;
const personal = profiles.find((p) => p.type === "PERSONAL");
return (personal ?? profiles[0]).id;
}
function selectBankTransferOption(quote) {
const opts = quote?.paymentOptions || [];
return opts.find(
(o) => o.payIn === "BANK_TRANSFER" && o.payOut === "BANK_TRANSFER"
) || null;
}
function getTargetAmount(quote, bankOption) {
return bankOption?.targetAmount ?? quote?.targetAmount ?? null;
}
function getTargetCurrency(quote, bankOption) {
return bankOption?.targetCurrency ?? quote?.targetCurrency ?? "GBP";
}
function getExchangeRateStr(quote) {
const rate = quote?.rate;
return rate !== undefined && rate !== null && !Number.isNaN(parseFloat(rate))
? Number(rate).toFixed(4)
: "N/A";
}
function getFeesDisplay(quote, bankOption) {
const feeAmount = bankOption?.fee?.total;
const feeCurrency = bankOption?.price?.total?.value?.currency || quote?.sourceCurrency;
return feeAmount != null ? `${feeAmount} ${feeCurrency || ""}`.trim() : "N/A";
}
function getDeliveryEstimate(bankOption) {
return bankOption?.estimatedDelivery
? new Date(bankOption.estimatedDelivery).toLocaleString()
: (bankOption?.formattedEstimatedDelivery || "N/A");
}
const runLogic = async () => {
const client = new WiseClient({});
// Task 1: Find out the Personal Profile ID of the user.
const profiles = await client.listProfiles();
const profileId = findPersonalProfileId(profiles);
if (!profileId) throw new Error("No profile found on account");
console.log(`Profile ID: ${profileId}`); // Example Console Log
// Create Quote
const quote = await client.createQuote(profileId, {
sourceCurrency: "SGD",
targetCurrency: "GBP",
sourceAmount: 1000,
});
// [IMP] Select BANK_TRANSFER option for both payin and payout
// Make sure you are selecting the correct payin and payout options to get the correct transfer fee.
// Task 2: Console Log the Quote ID
// Task 3: Console Log the Amount the recipient will receive, including the currency (e.g. "12.34 GBP")
// Task 4: Console Log the Exchange Rate (4 decimal places, e.g. "1.2345")
// Task 5: Console Log the Fees (total fee)
// Task 6: Console Log the Delivery Estimates (human readable format)
const bankOption = selectBankTransferOption(quote);
if (!bankOption) {
throw new Error("BANK_TRANSFER payment option not found in quote.");
}
console.log(`Quote ID: ${quote.id}`);
const targetAmount = getTargetAmount(quote, bankOption);
const targetCurrency = getTargetCurrency(quote, bankOption);
console.log(`Amount (recipient receives): ${targetAmount} ${targetCurrency}`);
const rateStr = getExchangeRateStr(quote);
console.log(`Exchange Rate: ${rateStr}`);
const feesDisplay = getFeesDisplay(quote, bankOption);
console.log(`Fees: ${feesDisplay}`);
const deliveryHuman = getDeliveryEstimate(bankOption);
console.log(`Delivery Estimate: ${deliveryHuman}`);
// Create Recipient (GBP Sort Code)
const recipient = await client.createRecipient(profileId, {
currency: "GBP",
type: "sort_code",
accountHolderName: "GBP Person Name",
details: {
legalType: "PRIVATE",
sortCode: "04-00-04",
accountNumber: "12345678",
},
});
// Task 7: Console Log the Recipient ID
console.log(`Recipient ID: ${recipient.id}`);
// Create Transfer
const transfer = await client.createTransfer(quote.id, recipient.id, { reference: "Payment reference" });
// Task 8: Console Log the Transfer ID
console.log(`Transfer ID: ${transfer.id}`);
// Task 9: Console Log the Transfer Status
console.log(`Transfer Status: ${transfer.status}`);
// Remember to copy all the console logs to a text file for submission.
console.log("All tasks completed successfully.");
};
Promise.resolve()
.then(() => runLogic())
.catch((error) => {
if (error instanceof WiseApiError) {
console.error("An error occurred");
if (error.status) console.error(`Status ${error.status}`);
if (error.traceId) console.error(`Trace ID: ${error.traceId}`);
if (error.code) console.error(`Code: ${error.code}`);
if (error.details) console.error(error.details);
} else {
console.error("An unexpected error occurred");
console.error(error?.message || error);
}
});
@arnm
Copy link
Author

arnm commented Sep 23, 2025

Output

Profile ID: 28894851
Quote ID: ec0d7cb8-f928-494f-9f9c-2d712776a6fc
Amount (recipient receives): 575.08 GBP
Exchange Rate: 0.5772
Fees: 3.61 SGD
Delivery Estimate: 9/22/2025, 11:35:13 PM
Recipient ID: 701588325
Transfer ID: 55466348
Transfer Status: incoming_payment_waiting
All tasks completed successfully.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment