Skip to content
Select theme

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.

DateMilestoneWhat it means for you
Nowv3 availableBuild anything new on v3, and start planning the move for what you already have.
April 1, 2027v1 and v2 deprecatedYour existing calls keep working. No new features, fixes, or changes land on these versions.
October 1, 2027v1 and v2 sunsetv1 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.

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.

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.

v2v3Notes
POST /3p/api/v2/bank-accounts/validatePOST /api/v3/bank-accounts/validate
GET /3p/api/v2/users/{uuid}/bank-accountsGET /api/v3/bank-accountsFilter by the owning entity. Confirm filter parameter.
POST /3p/api/v2/users/{uuid}/bank-accountsPOST /api/v3/bank-accounts or POST /api/v3/people/{uuid}/bank-accountsThe 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/defaultGET /api/v3/bank-accountsNo 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}/balanceGET /api/v3/bank-accounts/{uuid}/balance
GET /3p/api/v2/bank-accounts/{aggregator_type}/{account_uuid}/historyGET /api/v3/bank-accounts/{uuid}/history
—PUT /api/v3/bank-accounts/{uuid}/defaultNew in v3, restoring the v1 switch-default capability that v2 dropped.
—GET /api/v3/bank-accounts/{uuid}/ownersNew in v3.
v2v3Notes
GET /3p/api/v2/merchants/{uuid}/billingGET /api/v3/billing/summaryTotals grouped by billable event. The merchant is a query parameter rather than a path segment. Confirm parameter name.
—GET /api/v3/billing/itemsNew in v3: the individual billing events behind the summary.
v2v3Notes
GET /3p/api/v2/batch-payoutsGET /api/v3/payoutsOne 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/payoutsFilter 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/payoutsFilter 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/payoutsFilter 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/processorGET /api/v3/payoutsFilter 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/standaloneNew in v3: create a payout that isn’t tied to a transaction.
v2v3Notes
GET /3p/api/v2/refundsGET /api/v3/refunds
GET /3p/api/v2/refunds/{uuid}GET /api/v3/refunds/{uuid}
GET /3p/api/v2/batch-refundsGET /api/v3/refundsBatch 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/refundsv2 had no refund creation endpoint; v2 integrations used POST /3p/api/v1/transactions/{uuid}/refund.
v2v3Notes
GET /3p/api/v2/transactionsGET /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/transactionsv2 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}/pauseNew in v3.
—POST /api/v3/transactions/{uuid}/resumeNew in v3.
v2v3Notes
POST /3p/api/v2/businessesPOST /api/v3/businesses or POST /api/v3/merchantsOne v2 endpoint onboarded both; v3 separates them. Use /merchants for entities that undergo KYB.
PUT /3p/api/v2/users/{uuid}/kybPATCH /api/v3/merchants/{uuid}KYB details are updated on the merchant. Confirm.
GET /3p/api/v2/usersGET /api/v3/people, GET /api/v3/businesses, GET /api/v3/merchantsSplit by entity type.
POST /3p/api/v2/usersPOST /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}/deactivateNew in v3.
—POST /api/v3/people/kycNew in v3: standalone KYC verification for a person.
v2v3Notes
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).
v3Purpose
GET /api/v3/meFetch the authenticated account.
GET /api/v3/clawbacks, GET /api/v3/clawbacks/{uuid}Read clawbacks.
GET /api/v3/returnsRead 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.

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.

v1v3Notes
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-accountsConfirm which to use for businesses and merchants.
GET /3p/api/v1/bank-account/list/{user_uuid}GET /api/v3/bank-accountsFilter 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.
v1v3Notes
GET /3p/api/v1/batchpayoutsGET /api/v3/payoutsConfirm 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.
v1v3Notes
POST /3p/api/v1/transactions/{uuid}/refundPOST /api/v3/refundsThe transaction is identified in the request body.
GET /3p/api/v1/transactions/{transaction_uuid}/refundsGET /api/v3/refundsFilter by transaction. Confirm parameter.
GET /3p/api/v1/refunds/{refund_uuid}GET /api/v3/refunds/{uuid}
GET /3p/api/v1/batchrefundsGET /api/v3/refundsBatch refund detail is folded into the refund resource.
GET /3p/api/v1/batchrefunds/{batch_refund_uuid}GET /api/v3/refunds/{uuid}As above.
v1v3Notes
POST /3p/api/v1/transactionPOST /api/v3/transactionsAny 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/listGET /api/v3/transactions
DELETE /3p/api/v1/transaction/{uuid}DELETE /api/v3/transactions/{uuid}/cancel
v1v3Notes
POST /3p/api/v1/register/businessPOST /api/v3/businesses or POST /api/v3/merchantsUse /merchants for entities that undergo KYB.
POST /3p/api/v1/register/personPOST /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/usersGET /api/v3/people, GET /api/v3/businesses, GET /api/v3/merchantsSplit 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.
v1v3Notes
GET /3p/api/v1/webhookGET /api/v3/webhooks
POST /3p/api/v1/webhookPOST /api/v3/webhooks
DELETE /3p/api/v1/webhookDELETE /api/v3/webhooks/{uuid}Addressed by webhook UUID.
GET /processor/api/v1/webhookGET /api/v3/webhooksOne endpoint for all callers.
POST /processor/api/v1/webhookPOST /api/v3/webhooks
DELETE /processor/api/v1/webhookDELETE /api/v3/webhooks/{uuid}

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.

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:

Legacy
{
"event": "TransactionStarted",
"uuid": "b5c337d8-d886-11ed-afa1-0242ac120002",
"status": "CAPTURE_PENDING",
"amount": 12000
}
v3
{
"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 event in your handler rather than at registration time.
  • Route on event, de-duplicate on event_uuid. Every delivery, including retries, carries the same event_uuid, so an idempotent handler is a one-line check.
  • vendor_uuid is always there. Legacy events included it only on processor-bound payloads. In v3 it’s part of every envelope.
  • The entity inside data is the API’s entity. The transaction object in transaction.created has the same shape as the one in transaction.completed, and the same shape GET /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.

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 eventv3 eventNotes
BankAccountRemovedbank_account.removed
BankLinkFailedbank_account.link_failed
BankLinkedSuccessfullybank_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.
BatchRefundbatch_refund.created
BusinessCreatedbusiness.created
BusinessUpdatedbusiness.updated
ClawbackStartedclawback.createdConfirm.
ClawbackCaptureStartedclawback.processingConfirm.
ClawbackFailedclawback.failed
ClawbackCompletedclawback.completed
ComplianceStatusChangedcompliance_status.processing, compliance_status.in_review, compliance_status.approved, compliance_status.rejectedOne event per status instead of one event with a status field.
MerchantCreatedmerchant.created
MerchantUpdatedmerchant.updated
NocReceivednoc.received
NocProcessednoc.processed
PayoutCreatedpayout.created
—payout.processingNew in v3.
PayoutFailedpayout.failed
PayoutCompletedpayout.completed
PersonCreatedperson.created
PersonUpdatedperson.updated
PersonStatusChangedperson.status_changed
ProcessorPayoutCreatedprocessor_payout.created
—processor_payout.processingNew in v3.
ProcessorPayoutFailedprocessor_payout.failed
ProcessorPayoutCompletedprocessor_payout.completed
RefundPendingrefund.created
RefundCancelledrefund.cancelled
RefundCaptureStartedrefund.capture_processing
RefundCaptureFailedrefund.capture_failed
RefundCaptureCompletedrefund.capture_completed
RefundPayoutPendingrefund.payout_processing
RefundPayoutFailedrefund.payout_failed
RefundPayoutCompletedrefund.payout_completed
—reverse_payout.createdNew in v3. Confirm.
—reverse_payout.cancelledNew in v3.
ReversePayoutStartedreverse_payout.processingConfirm.
ReversePayoutFailedreverse_payout.failed
ReversePayoutCompletedreverse_payout.completed
TransactionStartedtransaction.created
TransactionCaptureStartedtransaction.processing
TransactionAwaitingCancellationtransaction.awaiting_cancellation
TransactionPausedtransaction.paused
TransactionResumedtransaction.resumed
TransactionCanceledtransaction.cancelledNote the spelling change.
TransactionDeclinedtransaction.declined
TransactionFailedtransaction.failed
TransactionCompletedtransaction.completed
—fbo.funding_created, fbo.funding_processing, fbo.funding_completed, fbo.funding_failed, fbo.funding_canceledNew in v3.
—fbo.account_snapshotNew in v3.

Every integration is different, but this sequence has worked well for teams we’ve talked to.

  1. 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.
  2. 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.
  3. Rework entity handling. Replace users calls with people, businesses, or merchants. 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.
  4. Rework webhook handling. Register one webhook through POST /api/v3/webhooks; there is no event list to maintain. Change your handler to read event and data, route on event and drop the events you don’t need, and use event_uuid to de-duplicate.
  5. Test in Sandbox. v3 is available in the sandbox environment. The Technical Overview covers how to get set up there.
  6. 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.

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