Migrating to API v3
If you’ve integrated with GrailPay over the past few years, you’ve probably noticed that our API grew in layers. Version 1 came first. Version 2 reshaped parts of it but left others in place, so a typical “v2 integration” still reaches back to v1 to create a transaction or register a webhook. Some endpoints live under 3p/, others under processor/. It works, but it asks you to know more about our history than you should have to.
API v3 is the version we’d have built from the start if we’d known then what we know now. It’s one API, under one base path, with a consistent shape for every request and response. This guide walks through what changed, when the earlier versions stop working, and how each endpoint and webhook event you use today maps to its v3 replacement. All three versions of the API reference are on this site — v3, and v2 and v1 marked as deprecated — so you can compare an old endpoint and its replacement side by side.
Timeline
Section titled “Timeline”| Date | Milestone | What it means for you |
|---|---|---|
| Now | v3 available | Build anything new on v3, and start planning the move for what you already have. |
| April 1, 2027 | v1 and v2 deprecated | Your existing calls keep working. No new features, fixes, or changes land on these versions. |
| October 1, 2027 | v1 and v2 sunset | v1 and v2 endpoints stop responding. Legacy webhook events stop being delivered. |
There’s a full year between now and the sunset date, and six months of formal deprecation before that. We set it up that way so nobody has to rush, but we’d encourage you to start early: the entity model changed in ways that are easier to absorb when you’re not working against a deadline.
What changed
Section titled “What changed”You no longer need to know our internal user UUIDs. This is the change most integrators will feel first. In v1 and v2, creating a transaction for a business or merchant meant knowing the user UUID we’d assigned behind that entity, which was never a concept you should have had to carry. In v3, you hand us any entity UUID — a person, a business, a merchant — and we work out the rest. The same is true everywhere an entity is referenced: the UUID you already have is the one you use.
Requests and responses are consistent. Every v3 endpoint returns the same entity in the same shape. The transaction object you get back from POST /api/v3/transactions is the one you get from GET /api/v3/transactions/{uuid}, and it’s the one that arrives inside a transaction.completed webhook. In v1 and v2, each endpoint and each event had its own field set, and reconciling them was your problem. Field names are consistent across entities too: timestamps, bank_identifiers, modality, and the like mean the same thing wherever they appear.
One API, one base path. Everything lives under /api/v3/. There’s no 3p/ and processor/ split; the caller’s role comes from authentication, not the URL. A v3 integration doesn’t need any v1 or v2 calls alongside it.
People, businesses, and merchants are separate resources. v2 addressed all three through a single users resource, which meant the shape of a response depended on what you’d happened to fetch. v3 gives each its own resource under /people, /businesses, and /merchants, so onboarding, lookups, and updates are explicit about what they’re acting on.
Bank accounts stand on their own. A bank account is no longer nested under a user and addressed by aggregator type. Each one has a UUID and lives at /api/v3/bank-accounts/{uuid}, whichever entity it belongs to and however it was linked.
Payouts are one resource, including processor payouts. The v2 batch payout endpoints, split four ways by entity type with a separate set for processors, are replaced by a single /api/v3/payouts resource. Processor payouts are part of it: filter the list endpoint to see them, or fetch one directly by UUID. Lifecycle events for them still arrive as processor_payout.* webhooks.
Billing is split in two. The single v2 billing endpoint becomes /api/v3/billing/summary, which gives you totals grouped by billable event, and /api/v3/billing/items, which gives you the individual events behind those totals.
Webhook events carry an envelope. Every v3 event shares the same top-level fields — event, event_uuid, event_occurred_at, event_version, vendor_uuid, and data — and event names are namespaced as group.action (transaction.created) rather than PascalCase (TransactionStarted). The entity inside data is the same object the API returns. The Webhook event changes section below goes into detail.
Webhooks are managed in the API, and subscription is simpler. Registering, listing, and removing webhooks happens through /api/v3/webhooks, replacing the v1 webhook endpoints and their processor-side twins. The subscription model changed as well: in v1 and v2 you subscribed to each event individually and received only those. In v3 you register one URL and receive every event. Your handler decides which ones to act on. Verification changes too: legacy deliveries carried X-Caller-Auth, a hash of your API key, while a v3 delivery carries a Signature header, an HMAC-SHA256 of the body keyed with the secret returned when you registered the webhook.
Endpoint changes: v2 to v3
Section titled “Endpoint changes: v2 to v3”Every v2 path starts with /3p/api/v2/; every v3 path starts with /api/v3/. Beyond that, the tables below show where each endpoint went. Where v3 has something v2 didn’t, it’s listed with a dash in the v2 column so you can see what’s newly available to you.
A reminder before you start: if your integration is “on v2,” check it against the v1 tables too. v2 never had endpoints for creating transactions or managing webhooks, so those calls are almost certainly v1.
Bank accounts
Section titled “Bank accounts”| v2 | v3 | Notes |
|---|---|---|
POST /3p/api/v2/bank-accounts/validate | POST /api/v3/bank-accounts/validate | |
GET /3p/api/v2/users/{uuid}/bank-accounts | GET /api/v3/bank-accounts | Filter by the owning entity. Confirm filter parameter. |
POST /3p/api/v2/users/{uuid}/bank-accounts | POST /api/v3/bank-accounts or POST /api/v3/people/{uuid}/bank-accounts | The entity is identified in the request rather than the path. Confirm which to use for businesses and merchants. |
GET /3p/api/v2/users/{uuid}/bank-accounts/default | GET /api/v3/bank-accounts | No dedicated endpoint; the default account is marked is_default in the list. Confirm. |
DELETE /3p/api/v2/bank-accounts/{aggregator_type}/{account_uuid} | DELETE /api/v3/bank-accounts/{uuid} | Aggregator type is no longer part of the path. |
GET /3p/api/v2/users/{uuid}/bank-accounts/{account_uuid}/balance | GET /api/v3/bank-accounts/{uuid}/balance | |
GET /3p/api/v2/bank-accounts/{aggregator_type}/{account_uuid}/history | GET /api/v3/bank-accounts/{uuid}/history | |
| — | PUT /api/v3/bank-accounts/{uuid}/default | New in v3, restoring the v1 switch-default capability that v2 dropped. |
| — | GET /api/v3/bank-accounts/{uuid}/owners | New in v3. |
Billing
Section titled “Billing”| v2 | v3 | Notes |
|---|---|---|
GET /3p/api/v2/merchants/{uuid}/billing | GET /api/v3/billing/summary | Totals grouped by billable event. The merchant is a query parameter rather than a path segment. Confirm parameter name. |
| — | GET /api/v3/billing/items | New in v3: the individual billing events behind the summary. |
Payouts
Section titled “Payouts”| v2 | v3 | Notes |
|---|---|---|
GET /3p/api/v2/batch-payouts | GET /api/v3/payouts | One list for all payouts. Confirm how a v2 batch maps to v3 payouts. |
GET /3p/api/v2/batch-payouts/{batch_payout_uuid} | GET /api/v3/payouts/{uuid} | Confirm. |
GET /3p/api/v2/batch-payouts/business/{business_user_uuid} | GET /api/v3/payouts | Filter by payee. Confirm filter parameter. |
GET /3p/api/v2/batch-payouts/business/{business_user_uuid}/{batch_payout_uuid} | GET /api/v3/payouts/{uuid} | Confirm. |
GET /3p/api/v2/batch-payouts/merchant/{merchant_user_uuid} | GET /api/v3/payouts | Filter by payee. Confirm. |
GET /3p/api/v2/batch-payouts/merchant/{merchant_user_uuid}/{batch_payout_uuid} | GET /api/v3/payouts/{uuid} | Confirm. |
GET /3p/api/v2/batch-payouts/person/{user_uuid} | GET /api/v3/payouts | Filter by payee. Confirm. |
GET /3p/api/v2/batch-payouts/person/{user_uuid}/{batch_payout_uuid} | GET /api/v3/payouts/{uuid} | Confirm. |
GET /3p/api/v2/batch-payouts/processor | GET /api/v3/payouts | Filter for processor payouts. Confirm filter parameter. |
GET /3p/api/v2/batch-payouts/processor/{batch_payout_uuid} | GET /api/v3/payouts/{uuid} | |
GET /3p/api/v2/batch-merchant-payouts/{batch_merchant_payout_uuid} | GET /api/v3/payouts/{uuid} | Confirm. |
| — | POST /api/v3/payouts/standalone | New in v3: create a payout that isn’t tied to a transaction. |
Refunds
Section titled “Refunds”| v2 | v3 | Notes |
|---|---|---|
GET /3p/api/v2/refunds | GET /api/v3/refunds | |
GET /3p/api/v2/refunds/{uuid} | GET /api/v3/refunds/{uuid} | |
GET /3p/api/v2/batch-refunds | GET /api/v3/refunds | Batch refund detail is folded into the refund resource. |
GET /3p/api/v2/batch-refunds/{batch_refund_uuid} | GET /api/v3/refunds/{uuid} | As above. |
| — | POST /api/v3/refunds | v2 had no refund creation endpoint; v2 integrations used POST /3p/api/v1/transactions/{uuid}/refund. |
Transactions
Section titled “Transactions”| v2 | v3 | Notes |
|---|---|---|
GET /3p/api/v2/transactions | GET /api/v3/transactions | |
GET /3p/api/v2/transactions/{uuid} | GET /api/v3/transactions/{uuid} | |
DELETE /3p/api/v2/transactions/{uuid} | DELETE /api/v3/transactions/{uuid}/cancel | |
| — | POST /api/v3/transactions | v2 had no create endpoint; v2 integrations used POST /3p/api/v1/transaction. Any entity UUID identifies the payor and payee. |
| — | POST /api/v3/transactions/{uuid}/pause | New in v3. |
| — | POST /api/v3/transactions/{uuid}/resume | New in v3. |
Users, people, businesses, and merchants
Section titled “Users, people, businesses, and merchants”| v2 | v3 | Notes |
|---|---|---|
POST /3p/api/v2/businesses | POST /api/v3/businesses or POST /api/v3/merchants | One v2 endpoint onboarded both; v3 separates them. Use /merchants for entities that undergo KYB. |
PUT /3p/api/v2/users/{uuid}/kyb | PATCH /api/v3/merchants/{uuid} | KYB details are updated on the merchant. Confirm. |
GET /3p/api/v2/users | GET /api/v3/people, GET /api/v3/businesses, GET /api/v3/merchants | Split by entity type. |
POST /3p/api/v2/users | POST /api/v3/people | |
GET /3p/api/v2/users/{uuid} | GET /api/v3/people/{uuid}, GET /api/v3/businesses/{uuid}, GET /api/v3/merchants/{uuid} | Split by entity type. |
DELETE /3p/api/v2/users/{uuid} | DELETE /api/v3/people/{uuid} | For merchants, use POST /api/v3/merchants/{uuid}/deactivate. Businesses: Confirm. |
| — | PATCH /api/v3/people/{uuid}, PATCH /api/v3/businesses/{uuid} | New in v3. |
| — | POST /api/v3/merchants/{uuid}/activate, POST /api/v3/merchants/{uuid}/deactivate | New in v3. |
| — | POST /api/v3/people/kyc | New in v3: standalone KYC verification for a person. |
Webhooks
Section titled “Webhooks”| v2 | v3 | Notes |
|---|---|---|
GET /3p/api/v2/webhook-events | — | No v3 equivalent; the event catalog is documented at Webhook Events. Confirm. |
| — | GET /api/v3/webhooks, POST /api/v3/webhooks, GET /api/v3/webhooks/{uuid}, DELETE /api/v3/webhooks/{uuid} | v2 had no webhook management; v2 integrations used the v1 webhook endpoints (see below). |
New in v3 with no v2 counterpart
Section titled “New in v3 with no v2 counterpart”| v3 | Purpose |
|---|---|
GET /api/v3/me | Fetch the authenticated account. |
GET /api/v3/clawbacks, GET /api/v3/clawbacks/{uuid} | Read clawbacks. |
GET /api/v3/returns | Read ACH returns. |
GET /api/v3/reverse-payouts, GET /api/v3/reverse-payouts/{uuid} | Read reverse payouts. |
GET /api/v3/vendors/{uuid} | Fetch vendor details. |
GET /api/v3/vendors/{uuid}/fbo, POST /api/v3/vendors/{uuid}/fbo/funding, DELETE /api/v3/vendors/{uuid}/fbo/funding/{funding_uuid} | Prefunded FBO account and funding. |
GET /api/v3/vendors/{uuid}/bank-accounts, POST …/bank-accounts, PUT …/bank-accounts/{bank_account_uuid}/default, DELETE …/bank-accounts/{bank_account_uuid} | Vendor funding bank accounts. |
Endpoint changes: v1 to v3
Section titled “Endpoint changes: v1 to v3”v1 had two parallel surfaces: /3p/api/v1/ for vendors and /processor/api/v1/ for processors, with many endpoints duplicated across them. v3 has one surface, and who you are is settled by how you authenticate rather than which prefix you call. In the tables below, both v1 surfaces map onto the same v3 endpoints.
Bank accounts
Section titled “Bank accounts”| v1 | v3 | Notes |
|---|---|---|
GET /3p/api/v1/bank-account/{user_uuid} | GET /api/v3/bank-accounts/{uuid} | Addressed by bank account UUID, not user. |
POST /3p/api/v1/bank-account/{user_uuid} | POST /api/v3/bank-accounts or POST /api/v3/people/{uuid}/bank-accounts | Confirm which to use for businesses and merchants. |
GET /3p/api/v1/bank-account/list/{user_uuid} | GET /api/v3/bank-accounts | Filter by entity. Confirm parameter. |
GET /3p/api/v1/bank-account/balance/{user_uuid} | GET /api/v3/bank-accounts/{uuid}/balance | |
PUT /3p/api/v1/bank-account/switch/default/{user_uuid} | PUT /api/v3/bank-accounts/{uuid}/default | |
GET /3p/api/v1/cashflow/prediction/{user_uuid} | — | Not in the v3 Payments API. Confirm whether this moved to the Risk Intelligence API. |
POST /3p/api/v1/bank-account/user | — | No v3 equivalent. Confirm. |
GET /processor/api/v1/bank-account/{user_uuid} | GET /api/v3/bank-accounts/{uuid} | |
GET /processor/api/v1/bank-account/list/{user_uuid} | GET /api/v3/bank-accounts | |
GET /processor/api/v1/bank-account/balance/{user_uuid} | GET /api/v3/bank-accounts/{uuid}/balance | |
GET /processor/api/v1/cashflow-prediction/{user_uuid} | — | As above. Confirm. |
Payouts
Section titled “Payouts”| v1 | v3 | Notes |
|---|---|---|
GET /3p/api/v1/batchpayouts | GET /api/v3/payouts | Confirm how a v1 batch maps to v3 payouts. |
GET /3p/api/v1/batchpayout/{payout_uuid} | GET /api/v3/payouts/{uuid} | Confirm. |
GET /processor/api/v1/batchpayout/{payout_uuid} | GET /api/v3/payouts/{uuid} | Processor payouts are fetched by UUID like any other payout. |
Refunds
Section titled “Refunds”| v1 | v3 | Notes |
|---|---|---|
POST /3p/api/v1/transactions/{uuid}/refund | POST /api/v3/refunds | The transaction is identified in the request body. |
GET /3p/api/v1/transactions/{transaction_uuid}/refunds | GET /api/v3/refunds | Filter by transaction. Confirm parameter. |
GET /3p/api/v1/refunds/{refund_uuid} | GET /api/v3/refunds/{uuid} | |
GET /3p/api/v1/batchrefunds | GET /api/v3/refunds | Batch refund detail is folded into the refund resource. |
GET /3p/api/v1/batchrefunds/{batch_refund_uuid} | GET /api/v3/refunds/{uuid} | As above. |
Transactions
Section titled “Transactions”| v1 | v3 | Notes |
|---|---|---|
POST /3p/api/v1/transaction | POST /api/v3/transactions | Any entity UUID identifies the payor and payee; no user UUID lookup needed. |
GET /3p/api/v1/transaction/{uuid} | GET /api/v3/transactions/{uuid} | |
GET /3p/api/v1/transaction/list | GET /api/v3/transactions | |
DELETE /3p/api/v1/transaction/{uuid} | DELETE /api/v3/transactions/{uuid}/cancel |
Users, people, businesses, and merchants
Section titled “Users, people, businesses, and merchants”| v1 | v3 | Notes |
|---|---|---|
POST /3p/api/v1/register/business | POST /api/v3/businesses or POST /api/v3/merchants | Use /merchants for entities that undergo KYB. |
POST /3p/api/v1/register/person | POST /api/v3/people | |
POST /3p/api/v1/user/kyb/{uuid} | PATCH /api/v3/merchants/{uuid} | Confirm. |
GET /3p/api/v1/user/{uuid} | GET /api/v3/people/{uuid}, GET /api/v3/businesses/{uuid}, GET /api/v3/merchants/{uuid} | Split by entity type. |
GET /3p/api/v1/users | GET /api/v3/people, GET /api/v3/businesses, GET /api/v3/merchants | Split by entity type. |
DELETE /3p/api/v1/users/{uuid} | DELETE /api/v3/people/{uuid} | For merchants, use POST /api/v3/merchants/{uuid}/deactivate. Businesses: Confirm. |
Webhooks
Section titled “Webhooks”| v1 | v3 | Notes |
|---|---|---|
GET /3p/api/v1/webhook | GET /api/v3/webhooks | |
POST /3p/api/v1/webhook | POST /api/v3/webhooks | |
DELETE /3p/api/v1/webhook | DELETE /api/v3/webhooks/{uuid} | Addressed by webhook UUID. |
GET /processor/api/v1/webhook | GET /api/v3/webhooks | One endpoint for all callers. |
POST /processor/api/v1/webhook | POST /api/v3/webhooks | |
DELETE /processor/api/v1/webhook | DELETE /api/v3/webhooks/{uuid} |
Webhook event changes
Section titled “Webhook event changes”Legacy webhook events stop being delivered on October 1, 2027, the same day the v1 and v2 endpoints go dark. Until then, a webhook registered through v1 keeps receiving the legacy events it subscribed to, and a webhook registered through /api/v3/webhooks receives every v3 event. There is no crossover: a v1 registration never receives v3 events, and a v3 registration never receives legacy events.
If you’ve only ever consumed legacy events, the v3 shape will feel different at first, but it’s different in a way that makes your handler simpler.
The envelope
Section titled “The envelope”A legacy payload put every field at the top level, named the event in PascalCase, and gave you no way to tell a retried delivery from a new one. A v3 payload wraps the entity under data and surrounds it with a small, predictable envelope:
{ "event": "TransactionStarted", "uuid": "b5c337d8-d886-11ed-afa1-0242ac120002", "status": "CAPTURE_PENDING", "amount": 12000}{ "event": "transaction.created", "event_uuid": "019d072d-1203-7695-842b-a15567fbb0d8", "event_occurred_at": "2026-04-05 14:28:33", "event_version": 3, "vendor_uuid": "019e2711-5a92-735d-a7d5-669aec76b54f", "data": { "transaction": { } }}Four things follow from this:
- You get every event. A v3 registration receives all events, not a subscribed subset. Filter on
eventin your handler rather than at registration time. - Route on
event, de-duplicate onevent_uuid. Every delivery, including retries, carries the sameevent_uuid, so an idempotent handler is a one-line check. vendor_uuidis always there. Legacy events included it only on processor-bound payloads. In v3 it’s part of every envelope.- The entity inside
datais the API’s entity. Thetransactionobject intransaction.createdhas the same shape as the one intransaction.completed, and the same shapeGET /api/v3/transactions/{uuid}returns. Legacy events each had their own field set; v3 events don’t.
Full payloads for every event are on the Webhook Events pages.
Event mapping
Section titled “Event mapping”Most legacy events have a direct v3 replacement with a new name. A few were split, a few were removed because the concept they described is gone, and v3 adds events for things the legacy set never covered.
| Legacy event | v3 event | Notes |
|---|---|---|
BankAccountRemoved | bank_account.removed | |
BankLinkFailed | bank_account.link_failed | |
BankLinkedSuccessfully | bank_account.link_success | |
BatchPayoutBusiness | — | Removed. Payouts are reported individually through payout.* events. |
BatchPayoutMerchant | — | Removed. As above. |
BatchPayoutPerson | — | Removed. As above. |
BatchPayout | — | Removed. Processor payouts are reported through processor_payout.* events. |
BatchRefund | batch_refund.created | |
BusinessCreated | business.created | |
BusinessUpdated | business.updated | |
ClawbackStarted | clawback.created | Confirm. |
ClawbackCaptureStarted | clawback.processing | Confirm. |
ClawbackFailed | clawback.failed | |
ClawbackCompleted | clawback.completed | |
ComplianceStatusChanged | compliance_status.processing, compliance_status.in_review, compliance_status.approved, compliance_status.rejected | One event per status instead of one event with a status field. |
MerchantCreated | merchant.created | |
MerchantUpdated | merchant.updated | |
NocReceived | noc.received | |
NocProcessed | noc.processed | |
PayoutCreated | payout.created | |
| — | payout.processing | New in v3. |
PayoutFailed | payout.failed | |
PayoutCompleted | payout.completed | |
PersonCreated | person.created | |
PersonUpdated | person.updated | |
PersonStatusChanged | person.status_changed | |
ProcessorPayoutCreated | processor_payout.created | |
| — | processor_payout.processing | New in v3. |
ProcessorPayoutFailed | processor_payout.failed | |
ProcessorPayoutCompleted | processor_payout.completed | |
RefundPending | refund.created | |
RefundCancelled | refund.cancelled | |
RefundCaptureStarted | refund.capture_processing | |
RefundCaptureFailed | refund.capture_failed | |
RefundCaptureCompleted | refund.capture_completed | |
RefundPayoutPending | refund.payout_processing | |
RefundPayoutFailed | refund.payout_failed | |
RefundPayoutCompleted | refund.payout_completed | |
| — | reverse_payout.created | New in v3. Confirm. |
| — | reverse_payout.cancelled | New in v3. |
ReversePayoutStarted | reverse_payout.processing | Confirm. |
ReversePayoutFailed | reverse_payout.failed | |
ReversePayoutCompleted | reverse_payout.completed | |
TransactionStarted | transaction.created | |
TransactionCaptureStarted | transaction.processing | |
TransactionAwaitingCancellation | transaction.awaiting_cancellation | |
TransactionPaused | transaction.paused | |
TransactionResumed | transaction.resumed | |
TransactionCanceled | transaction.cancelled | Note the spelling change. |
TransactionDeclined | transaction.declined | |
TransactionFailed | transaction.failed | |
TransactionCompleted | transaction.completed | |
| — | fbo.funding_created, fbo.funding_processing, fbo.funding_completed, fbo.funding_failed, fbo.funding_canceled | New in v3. |
| — | fbo.account_snapshot | New in v3. |
A suggested order of work
Section titled “A suggested order of work”Every integration is different, but this sequence has worked well for teams we’ve talked to.
- Take inventory. List every v1 and v2 endpoint your integration calls, and every legacy event your handler consumes. If you think you’re on v2, check for v1 calls too; transaction creation and webhook registration almost always are.
- Map each call using the tables above. If a row you depend on is marked for confirmation, or an endpoint you use isn’t listed, ask us at support@grailpay.com before you build against your guess.
- Rework entity handling. Replace
userscalls withpeople,businesses, ormerchants. Stop looking up user UUIDs to create transactions; pass the entity UUID you have. Store bank account UUIDs, since the aggregator-type path segment is gone. - Rework webhook handling. Register one webhook through
POST /api/v3/webhooks; there is no event list to maintain. Change your handler to readeventanddata, route oneventand drop the events you don’t need, and useevent_uuidto de-duplicate. - Test in Sandbox. v3 is available in the sandbox environment. The Technical Overview covers how to get set up there.
- Cut over, then clean up. Once v3 is live for you, remove the v1 and v2 calls rather than leaving them dormant. After October 1, 2027 they’ll fail anyway, and it’s better to find any you missed on your own schedule.
Reference
Section titled “Reference”- Payments API v3 reference
- Payments API v2 reference (deprecated)
- Payments API v1 reference (deprecated)
- Webhook Events
- Legacy Webhook Events
Questions?
Section titled “Questions?”If something in your integration doesn’t fit the tables above, or you’d like a second pair of eyes on your migration plan, reach out to support@grailpay.com. We’d rather hear from you early than have you discover a gap in September 2027.
Maintained by EkLine