Goal: Accept your first payment in FLY end-to-end.
Prep time: ~15 minutes
Prerequisites
export API_BASE_URL="https://api.staging.blackbird.xyz/flynet/v1"
export ACCESS_TOKEN="..." # OAuth access token (from /oauth/token)
export MERCHANT_ID="..." # from your onboarding email; required
export CUSTOMER_USER_ID="..." # the member paying; their UUID, which equals the token's sub claim / the id from /users/me
If you don’t have a MERCHANT_ID, your partner record isn’t payments-enabled. Ask via Support. See Payments for the credential model.
Tasting note - customer_user_id is the paying member’s UUID. For a member acting on their own behalf, that’s the sub claim on their access token: the same value /users/me returns as id. You don’t read it from a /users/{id} path; resolve it from the token.
The customer must hold enough FLY in their SPENDING wallet for confirm to succeed.
What you will use
POST /flynet/v1/payment_intents
POST /flynet/v1/payment_intents/{id}/confirm
GET /flynet/v1/payment_intents/{id}
- Optionally:
POST /flynet/v1/payment_intents/{id}/refund
Step 1: Create the intent
const res = await fetch(`${process.env.API_BASE_URL}/payment_intents`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ACCESS_TOKEN!}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
flynet_merchant_id: process.env.MERCHANT_ID,
customer_user_id: process.env.CUSTOMER_USER_ID,
amount: { value: "1000000000000000000", currency: "FLY" },
description: "First payment recipe",
idempotency_key: `recipe-first-payment-${Date.now()}`,
}),
});
const intent = await res.json();
console.log(intent.id, intent.status);
curl -X POST "$API_BASE_URL/payment_intents" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"flynet_merchant_id\": \"$MERCHANT_ID\",
\"customer_user_id\": \"$CUSTOMER_USER_ID\",
\"amount\": { \"value\": \"1000000000000000000\", \"currency\": \"FLY\" },
\"description\": \"First payment recipe\",
\"idempotency_key\": \"recipe-first-payment-$(date +%s)\"
}"
Response:
{
"id": "{uuid}",
"status": "pending",
"amount": { "value": "1000000000000000000", "currency": "FLY" }
}
Save the id. You need it in the next step.
Tasting note - Use your own order ID as the idempotency key in production so network retries land on the same intent. Here we use a timestamp so re-running the recipe creates fresh intents. See Idempotency.
Chef’s warning - If create returns 404 resource_not_found "Flynet merchant not found", your MERCHANT_ID isn’t scoped to your client_id or your environment. Double-check the value in your onboarding email; if it matches and still 404s, route via Support.
Step 2: Confirm
const intentId = intent.id; // from Step 1
const res = await fetch(
`${process.env.API_BASE_URL}/payment_intents/${intentId}/confirm`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ACCESS_TOKEN!}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ user_id: process.env.CUSTOMER_USER_ID }),
},
);
const confirmed = await res.json();
console.log(confirmed.status, confirmed.paid_at);
export INTENT_ID="{the_id_from_step_1}"
curl -X POST "$API_BASE_URL/payment_intents/$INTENT_ID/confirm" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d "{ \"user_id\": \"$CUSTOMER_USER_ID\" }"
Response:
{
"id": "{uuid}",
"status": "paid",
"paid_at": "2026-05-11T20:01:00Z"
}
The customer’s SPENDING wallet decreases by 1 FLY. The merchant’s wallet increases by 1 FLY. The customer gets a receipt email from flynet@blackbird.xyz.
Chef’s warning - If the customer does not hold enough FLY, this call returns 400 payment0030. Pre-check the customer’s SPENDING wallet balance for a smoother UX.
Step 3: Verify
const res = await fetch(
`${process.env.API_BASE_URL}/payment_intents/${intentId}`,
{ headers: { Authorization: `Bearer ${process.env.ACCESS_TOKEN!}` } },
);
const verified = await res.json();
console.log(verified.status, verified.paid_at);
curl "$API_BASE_URL/payment_intents/$INTENT_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN"
You should see status: "paid" and a populated paid_at.
Step 4: Refund
const res = await fetch(
`${process.env.API_BASE_URL}/payment_intents/${intentId}/refund`,
{
method: "POST",
headers: { Authorization: `Bearer ${process.env.ACCESS_TOKEN!}` },
},
);
const refunded = await res.json();
console.log(refunded.status); // "refunded"
curl -X POST "$API_BASE_URL/payment_intents/$INTENT_ID/refund" \
-H "Authorization: Bearer $ACCESS_TOKEN"
The 1 FLY returns to the customer. Status becomes refunded. Refunds are full-only in v1.
What’s next
- Read Payments for the idempotency contract.
- Handle insufficient FLY in your UI before calling
confirm; see payment0030.
- List merchant intents with
GET /flynet/v1/payment_intents?payee_account_balance_id={uuid}.