# Quickstart: your first sandbox payment

This quickstart takes you from an API key to a live transaction in the GrailPay sandbox. You authenticate, onboard the two sides of a payment, register a webhook, create a transaction, and follow it as it moves. Nothing you send here reaches a real financial institution, so you can run the full flow safely.

By the end you will have:

- Confirmed your API key with an authenticated call.
- Onboarded a **merchant** and a **person**, and used them as the two sides of a payment. Here the person pays the merchant; the same two records work the other way around.
- Registered a webhook endpoint to receive event updates.
- Created a transaction and watched it move through its first states.

This walkthrough uses one common shape, a person paying a merchant, but the rule behind it is broader: every payment except a person-to-person payment needs a KYB-verified merchant on at least one side. The merchant can be the payor or the payee, and the other side can be a person, a business, or another merchant. The two UUIDs you send in Step 5 are all that decide who pays whom. For the full model behind each step, see the [Payments Platform Overview](/docs/technical/overview).

:::note[Who can be on each side]
The merchant anchors the payment, not a fixed role. Any of these combinations work once the merchant's compliance status is approved. The ACH entry type follows from the party on each leg: a business or merchant produces a CCD entry, and a person produces a WEB entry when debited and a PPD entry when paid.

| Payor    | Payee    | Debit leg | Payout leg | Example                                                      |
|----------|----------|-----------|------------|--------------------------------------------------------------|
| Person   | Merchant | WEB       | CCD        | A customer pays a business (this quickstart)                 |
| Business | Merchant | CCD       | CCD        | A company pays a business, with no KYB on the paying company |
| Merchant | Merchant | CCD       | CCD        | Two onboarded businesses settle with each other              |
| Merchant | Person   | CCD       | PPD        | A business pays a customer, contractor or claimant           |

The only shape with no merchant is a payment between two people, which needs an approved payee and a feature enabled on your account. See [Person to Person Payments](/docs/technical/transactions/create-a-transaction#person-to-person-payments).
:::

## Prerequisites

Before you start, you need:

- A **sandbox API key**. If you do not have one, contact [support@grailpay.com](mailto:support@grailpay.com).
- A tool for sending HTTPS requests. This guide uses [cURL](https://curl.se/); any HTTP client works.
- A webhook endpoint that accepts HTTPS POST requests. For a first run, a temporary endpoint from a service such as [webhook.site](https://webhook.site/) is enough to see deliveries arrive.

All requests in this guide use the sandbox base URL `https://api-sandbox.grailpay.com/api/v3`. Every call must be made over HTTPS — calls over HTTP fail. For the base URLs, API keys, and shared request conventions, see [Environments](/docs/technical/environments).

:::caution
Use your sandbox API key only against the sandbox base URL. Sandbox and production have separate keys, and nothing created in one exists in the other.
:::

## Step 1: Authenticate

The Payments API uses Bearer authentication. Send your API key in the `Authorization` header on every request:

```txt title="Bearer Authentication"
Authorization: Bearer {YOUR_API_KEY}
```

Your key has the form `ID|TOKEN_VALUE`, for example `12345|abcdefg`.

Confirm the key works by fetching the authenticated account with `GET /api/v3/me`:

```bash title="cURL"
curl --location 'https://api-sandbox.grailpay.com/api/v3/me' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {YOUR_API_KEY}'
```

**Verify:** A `200` response returns your account's `entity.type`, `entity.uuid`, and `entity.name`. If you receive a `401`, check the header format against [Authentication](/docs/technical/authentication) — the response `message` names the problem, and `request_id` identifies the call if you contact support.

## Step 2: Onboard the merchant

The merchant is the KYB-verified anchor of the flow, and in this walkthrough it is the payee. Create one with `POST /api/v3/merchants`. Onboarding collects a point of contact, the business details, its beneficial owners, and a bank account. For the complete request body, see [Onboard a Merchant](/docs/technical/users/onboard-a-merchant) and the [API reference](/api/payments/#tag/users/POST/api/v3/merchants).

When you add a bank account manually, use a sandbox test routing number so the account validates without reaching a real institution. Pair routing number `226078036` with any 12-digit account number:

```json title="Merchant bank account (manual)"
{
    "bank_account": {
        "account_name": "Acme Operating",
        "account_number": "123456789012",
        "routing_number": "226078036",
        "account_type": "checking"
    }
}
```

Any beneficial-owner SSN you send in the sandbox must be a valid test value: it cannot begin with `000` or a value in the `900–999` range, cannot have `00` as its middle two digits, and cannot end in `0000`.

**Verify:** A successful response returns the merchant's `uuid`. Save it for use as the `payee_uuid` in Step 5.

:::note
KYB verification runs asynchronously. A new merchant starts in a **Pending** compliance status and must reach **Approved** before it can transact. Track its progress with the `compliance_status.*` webhook events you register in Step 4, or see [Compliance Status Overview](/docs/resources/compliance-status-overview).
:::

## Step 3: Onboard the person

The person is the other side of the payment, and in this walkthrough it is the payor whose bank account is debited. Create one with `POST /api/v3/people`. You must provide a bank account when creating a person; onboarding validates the account in real time with [Account Intelligence](/docs/risk/bank-accounts/account-intelligence).

Reuse the sandbox test routing number `226078036` with a 12-digit account number:

```bash title="cURL"
curl --location 'https://api-sandbox.grailpay.com/api/v3/people' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: afc24e55-8321-4172-8903-67e7ed80f07b' \
--header 'Authorization: Bearer {YOUR_API_KEY}' \
--data '{
    "person": {
        "first_name": "Will",
        "last_name": "Clance",
        "address": {
            "line_1": "681 Avenue C",
            "city": "Bayonne",
            "state": "NJ",
            "zip": "07002"
        }
    },
    "bank_account": {
        "account_name": "Will Clance Checking",
        "account_number": "210987654321",
        "routing_number": "226078036",
        "account_type": "checking"
    },
    "actions": {
        "account_intelligence": {
            "version": "v3",
            "name_match": true
        }
    }
}'
```

The `Idempotency-Key` header makes the request safe to retry without creating a duplicate person. Generate a fresh V4 UUID for each new request. See [Idempotency](/docs/technical/idempotency) for the full behavior. For the complete field list, see [Onboard a Person](/docs/technical/users/onboard-a-person) and the [API reference](/api/payments/#tag/users/POST/api/v3/people).

**Verify:** A successful response returns the person's `uuid` and an `actions.account_intelligence` object confirming the validation that ran (shown abridged — the full object also includes a risk score and decisioning insights):

```json title="Account Intelligence result (abridged)"
{
    "actions": {
        "account_intelligence": {
            "version": "v3",
            "name_match": true
        }
    }
}
```

Save the person's `uuid` — it becomes the `payor_uuid` in Step 5. For the full result, see [Account Intelligence](/docs/risk/bank-accounts/account-intelligence).

:::tip
To see how different validation outcomes behave, swap the routing number for one of the reserved test values on [Validate Bank Accounts](/docs/technical/bank-accounts/validate-bank-accounts#testing).
:::

## Step 4: Register a webhook

Webhooks deliver event updates — compliance decisions, transaction progress, and payouts — to your endpoint in real time. Register one URL with `POST /api/v3/webhooks`. Every registered URL receives every event; your handler routes on the `event` field in the payload.

```bash title="cURL"
curl --location 'https://api-sandbox.grailpay.com/api/v3/webhooks' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer {YOUR_API_KEY}' \
--data '{
    "url": "https://your-endpoint.example.com/grailpay/webhooks"
}'
```

**Verify:** A successful response returns the registration's `uuid`, the `url` you registered, and a `secret`. Store the `secret` — GrailPay signs every delivery with it, and you use it to verify the `Signature` header on incoming events. See [Manage Webhooks](/docs/technical/webhooks/manage-webhooks) for signature verification and delivery rules.

## Step 5: Create a transaction

With both parties onboarded and the merchant approved, create the payment with `POST /api/v3/transactions`. The transaction debits the payer and credits the payee. Amounts are in cents, so `1000` is $10.00.

```bash title="cURL"
curl --location 'https://api-sandbox.grailpay.com/api/v3/transactions' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: 9f1d2c7a-6b54-4e21-9a3c-7c2f1b0e44d8' \
--header 'Authorization: Bearer {YOUR_API_KEY}' \
--data '{
    "payor_uuid": "{PERSON_UUID}",
    "payee_uuid": "{MERCHANT_UUID}",
    "amount": 1000
}'
```

Replace `{PERSON_UUID}` with the payer from Step 3 and `{MERCHANT_UUID}` with the payee from Step 2. For optional fields such as `description`, `company_name`, and payout `modality`, see [Create a Transaction](/docs/technical/transactions/create-a-transaction) and the [API reference](/api/payments/#tag/transactions/POST/api/v3/transactions).

To send the payment the other way, swap the two values: the merchant's `uuid` as `payor_uuid` and the person's as `payee_uuid`. Nothing else in the request changes; the merchant remains the KYB-verified anchor on either side.

**Verify:** A `201` response returns the new transaction with its `uuid` and an initial `status` of `QUEUED`. A repeated request with the same `Idempotency-Key` returns `409` instead of creating a second transaction.

:::note
The merchant must be in an **Approved** compliance status for the transaction to process, whichever side it is on. If onboarding is still pending, wait for the `compliance_status` event from Step 4 before you create the transaction.
:::

## Step 6: Follow the transaction

Track the transaction as the ACH debit and payout move. You have two options:

- **Webhooks** — your endpoint from Step 4 receives `transaction.*` and `payout.*` events as the state changes. This is the recommended approach for production integrations.
- **Polling** — fetch the current state with `GET /api/v3/transactions/{uuid}`.

```bash title="cURL"
curl --location 'https://api-sandbox.grailpay.com/api/v3/transactions/{TRANSACTION_UUID}' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {YOUR_API_KEY}'
```

**Verify:** The transaction advances from `QUEUED` through capture states such as `CAPTURE_PENDING` and `CAPTURE_ACH_PENDING`. For the full lifecycle and the events fired at each stage, see [Transaction States & Events](/docs/resources/transaction-states).

## Summary

You have moved a payment end to end in the sandbox. You authenticated your API key, onboarded a merchant and a person with validated bank accounts and used them as the two sides of a payment, registered a webhook, created a transaction, and followed it through its first states — the same sequence a production integration follows, without touching a real financial institution.

## Next steps

- **Understand the money movement** — read the [Payments Platform Overview](/docs/technical/overview) for ACH entry types, payment speeds, returns, and settlement timing.
- **Handle every event** — map the events your integration must react to in [Webhook Events](/docs/technical/webhooks/events).
- **Score risk before you pay** — add [Account Intelligence](/docs/risk/bank-accounts/account-intelligence) and [Transaction Intelligence](/docs/risk/transactions/transaction-intelligence) to screen accounts and transactions.
- **Go live** — when your integration is tested, switch to your production API key and base URL. Review [Environments](/docs/technical/environments) first.

## Questions?

If you run into any issues, contact [support@grailpay.com](mailto:support@grailpay.com).