FP-001
Send a FaucetPay payout with the legacy v1 API
The v1 form-POST payout, kept for installs that still run it. New builds should use the v2 scoped-key API, which adds idempotency keys.
**This is the legacy API.** v2 replaced the single faucet api_key with scoped keys, Bearer auth and required idempotency keys on send. If you are building something now, use v2 — see the v2 payout entries in this section. This page stays because plenty of running faucets still speak v1 and will for a long time.
FaucetPay's send endpoint is a plain form POST. There is no JSON body, no bearer token, and no SDK to install. What trips people up is the amount field.
FaucetPay pays in the coin's smallest unit. For most coins that is the satoshi, meaning 100,000,000 units to one coin. If your site keeps balances in USD you have to convert before you call this, and if you get the conversion backwards you will send a hundred million times too much.
The function below returns the decoded response array on success and throws on transport failure, so your payout queue can tell the difference between "FaucetPay said no" and "the request never arrived". That distinction matters: the first is final, the second should be retried.
function faucetpay_send(string $apiKey, string $to, string $currency, int $units, ?string $refId = null): array
{
$post = [
'api_key' => $apiKey,
'amount' => $units, // smallest unit (satoshi), NOT USD
'to' => $to,
'currency' => strtoupper($currency),
];
if ($refId !== null) {
$post['ref_id'] = $refId; // your own id, useful for reconciliation
}
$ch = curl_init('https://faucetpay.io/api/v1/send');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($post),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
CURLOPT_CONNECTTIMEOUT => 8,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_FOLLOWLOCATION => false,
]);
$body = curl_exec($ch);
$err = curl_error($ch);
$http = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($body === false) {
// Transport failure. The payout may or may not have happened: retry, do not
// mark it failed, and never credit the user's balance back on this branch.
throw new RuntimeException('faucetpay transport error: ' . $err);
}
$data = json_decode((string) $body, true);
if (!is_array($data)) {
throw new RuntimeException('faucetpay returned non-json (http ' . $http . ')');
}
return $data; // ['status' => 200, 'message' => ..., 'payout_id' => ...]
}
Using it
Call it with units, never dollars:
$res = faucetpay_send($key, $wallet, 'LTC', 12000);
A successful response carries status 200. Anything else is a refusal with a message explaining why: unknown wallet, below minimum, insufficient balance, bad API key.
Wrap the call so a thrown exception leaves the withdrawal in a held state rather than a failed one. Held withdrawals can be retried by hand. Failed ones tend to get refunded twice.
What bites people
FaucetPay does not pay in USD. There is no USD currency code. If your internal ledger is in dollars you must convert to the coin's units yourself, and you should store the rate you used on the payout row.
Sending to an address that is not a registered FaucetPay user fails. Check the address first if you want a clean error message for the user.
Do not retry automatically on a timeout without an idempotency strategy. A ref_id you generate and store lets you check afterwards whether the payout landed.
This one touches real money. Point it at a throwaway wallet and watch a full cycle before you trust it with a live balance.