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

# First payment

> Create, confirm, and verify a Payment Intent end-to-end. About 15 minutes.

**Goal:** Accept your first payment in FLY end-to-end.\
**Prep time:** \~15 minutes

## Prerequisites

```bash theme={null}
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](/resources/support). See [Payments](/concepts/payments#the-merchant-id-is-your-third-credential) for the credential model.

<Note>
  **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.
</Note>

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

```typescript theme={null}
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);
```

```bash theme={null}
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:

```json theme={null}
{
  "id": "{uuid}",
  "status": "pending",
  "amount": { "value": "1000000000000000000", "currency": "FLY" }
}
```

Save the `id`. You need it in the next step.

<Note>
  **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](/concepts/payments#idempotency).
</Note>

<Warning>
  **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](/resources/support).
</Warning>

## Step 2: Confirm

```typescript theme={null}
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);
```

```bash theme={null}
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:

```json theme={null}
{
  "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`.

<Warning>
  **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.
</Warning>

## Step 3: Verify

```typescript theme={null}
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);
```

```bash theme={null}
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

```typescript theme={null}
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"
```

```bash theme={null}
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](/concepts/payments#idempotency) for the idempotency contract.
* Handle insufficient FLY in your UI before calling `confirm`; see [`payment0030`](/resources/debugging#payment0030).
* List merchant intents with `GET /flynet/v1/payment_intents?payee_account_balance_id={uuid}`.
