The retry mechanism allows your payment connector to request that VTEX Payment Gateway retry the authorization request when processing asynchronous payments.
Overview
When a payment requires asynchronous processing (like bank invoices or redirect flows), your connector can return a pending status and use the retry mechanism to notify the gateway when to check for updates.
The retry flow replaces the deprecated callback flow. Payment providers implemented using VTEX IO cannot callback the Payment Gateway directly. Instead, they request a retry, asking the gateway to call the authorization route again.
How retry works
The retry mechanism follows this flow:
Return pending status
Your connector receives an authorization request and determines it requires async processing.
Trigger retry
The connector calls the callback() method with the final payment status and returns a pending response.
Gateway retries
The Payment Gateway calls your authorization endpoint again with the same paymentId.
Return final status
Your connector recognizes the retry (using VBase or another storage) and returns the final approved/denied status.
The connector must be able to respond with the final status consistently when the gateway retries the request.
Implementation
Here’s the complete implementation from the example connector:
import {
AuthorizationRequest ,
AuthorizationResponse ,
PaymentProvider ,
} from '@vtex/payment-provider'
import { VBase } from '@vtex/api'
const authorizationsBucket = 'authorizations'
const persistAuthorizationResponse = async (
vbase : VBase ,
resp : AuthorizationResponse
) => vbase . saveJSON ( authorizationsBucket , resp . paymentId , resp )
const getPersistedAuthorizationResponse = async (
vbase : VBase ,
req : AuthorizationRequest
) =>
vbase . getJSON < AuthorizationResponse | undefined >(
authorizationsBucket ,
req . paymentId ,
true
)
export default class TestSuiteApprover extends PaymentProvider {
private async saveAndRetry (
req : AuthorizationRequest ,
resp : AuthorizationResponse
) {
await persistAuthorizationResponse ( this . context . clients . vbase , resp )
this . callback ( req , resp )
}
public async authorize (
authorization : AuthorizationRequest
) : Promise < AuthorizationResponse > {
// Check if we already processed this payment
const persistedResponse = await getPersistedAuthorizationResponse (
this . context . clients . vbase ,
authorization
)
if ( persistedResponse !== undefined && persistedResponse !== null ) {
// Return the previously stored result
return persistedResponse
}
// First time processing - execute and save for retry
return executeAuthorization ( authorization , response =>
this . saveAndRetry ( authorization , response )
)
}
}
This code is from node/connector.ts:41-64 in the example repository.
Async payment flows
The example connector demonstrates several async payment flows:
Async approved
AsyncApproved : ( request , retry ) => {
retry (
Authorizations . approve ( request , {
authorizationId: randomString (),
nsu: randomString (),
tid: randomString (),
})
)
return Authorizations . pending ( request , {
delayToCancel: 1000 ,
tid: randomString (),
})
}
Async denied
AsyncDenied : ( request , retry ) => {
retry ( Authorizations . deny ( request , { tid: randomString () }))
return Authorizations . pending ( request , {
delayToCancel: 1000 ,
tid: randomString (),
})
}
Bank invoice
BankInvoice : ( request , retry ) => {
retry (
Authorizations . approve ( request , {
authorizationId: randomString (),
nsu: randomString (),
tid: randomString (),
})
)
return Authorizations . pendingBankInvoice ( request , {
delayToCancel: 1000 ,
paymentUrl: randomUrl (),
tid: randomString (),
})
}
Redirect flow
Redirect : ( request , retry ) => {
retry (
Authorizations . approve ( request , {
authorizationId: randomString (),
nsu: randomString (),
tid: randomString (),
})
)
return Authorizations . redirect ( request , {
delayToCancel: 1000 ,
redirectUrl: randomUrl (),
tid: randomString (),
})
}
These flow examples are from node/flow.ts:39-93 in the example repository.
Using VBase for persistence
VBase is VTEX’s key-value storage service, ideal for persisting payment states:
Add VBase policy
Ensure your manifest.json includes the VBase policy: {
"policies" : [
{
"name" : "vbase-read-write"
}
]
}
Create storage helpers
Implement functions to save and retrieve payment responses: import { VBase } from '@vtex/api'
import { AuthorizationResponse } from '@vtex/payment-provider'
const BUCKET = 'payment-authorizations'
export const savePaymentResponse = async (
vbase : VBase ,
response : AuthorizationResponse
) => {
await vbase . saveJSON ( BUCKET , response . paymentId , response )
}
export const getPaymentResponse = async (
vbase : VBase ,
paymentId : string
) : Promise < AuthorizationResponse | null > => {
return vbase . getJSON < AuthorizationResponse >( BUCKET , paymentId , true )
}
Use in connector
Access VBase through the context: public async authorize (
request : AuthorizationRequest
): Promise < AuthorizationResponse > {
const vbase = this . context . clients . vbase
// Check for existing response
const existing = await getPaymentResponse ( vbase , request . paymentId )
if ( existing ) {
return existing
}
// Process new payment
const response = await this . processPayment ( request )
await savePaymentResponse ( vbase , response )
return response
}
Callback method
The callback() method is inherited from the PaymentProvider base class:
this . callback ( request : AuthorizationRequest , response : AuthorizationResponse )
Parameters
request: The original authorization request
response: The final payment status to return on retry
Example usage
import {
PaymentProvider ,
AuthorizationRequest ,
AuthorizationResponse ,
Authorizations ,
} from '@vtex/payment-provider'
export default class MyConnector extends PaymentProvider {
public async authorize (
request : AuthorizationRequest
) : Promise < AuthorizationResponse > {
// Start async processing
const processingId = await this . context . clients . paymentApi . startPayment ({
amount: request . value ,
currency: request . currency ,
})
// Set up webhook to detect completion
this . setupWebhook ( processingId , async ( finalStatus ) => {
const finalResponse = finalStatus === 'approved'
? Authorizations . approve ( request , {
authorizationId: processingId ,
nsu: randomString (),
tid: randomString (),
})
: Authorizations . deny ( request )
// Save for retry
await this . saveResponse ( request . paymentId , finalResponse )
// Trigger retry
this . callback ( request , finalResponse )
})
// Return pending immediately
return Authorizations . pending ( request , {
delayToCancel: 5000 ,
tid: processingId ,
})
}
}
Retry flow diagram
Best practices
Always persist before callback
Ensure the final response is saved before calling callback(): // ✅ Correct order
await this . saveResponse ( paymentId , finalResponse )
this . callback ( request , finalResponse )
// ❌ Wrong order (retry might fail)
this . callback ( request , finalResponse )
await this . saveResponse ( paymentId , finalResponse )
Handle missing persisted responses
Always handle cases where the persisted response might not exist: const persisted = await getPaymentResponse ( vbase , request . paymentId )
if ( persisted !== undefined && persisted !== null ) {
return persisted
}
// Process new payment
Set appropriate delay to cancel
Use delayToCancel to specify how long to wait before auto-canceling: return Authorizations . pending ( request , {
delayToCancel: 300000 , // 5 minutes in milliseconds
tid: transactionId ,
})
Rely on paymentId as the unique identifier for storage: // ✅ Use paymentId from request
const key = request . paymentId
// ❌ Don't generate your own keys
const key = `payment- ${ Date . now () } `
Track retry behavior for debugging: const persisted = await getPaymentResponse ( vbase , request . paymentId )
if ( persisted ) {
console . log ( `Retry detected for payment ${ request . paymentId } ` )
return persisted
}
console . log ( `First attempt for payment ${ request . paymentId } ` )
Testing retry mechanism
import { describe , test , expect , jest } from '@jest/globals'
import TestSuiteApprover from '../connector'
import { AuthorizationRequest , Authorizations } from '@vtex/payment-provider'
describe ( 'Retry Mechanism' , () => {
test ( 'should return persisted response on retry' , async () => {
const connector = new TestSuiteApprover ( mockContext )
const request : AuthorizationRequest = {
paymentId: 'test-payment-123' ,
value: 10000 ,
currency: 'BRL' ,
}
// First call - process and return pending
const firstResponse = await connector . authorize ( request )
expect ( firstResponse . status ). toBe ( 'pending' )
// Wait for async processing (simulated)
await new Promise ( resolve => setTimeout ( resolve , 100 ))
// Retry - should return final status
const retryResponse = await connector . authorize ( request )
expect ( retryResponse . status ). toBe ( 'approved' )
expect ( retryResponse . paymentId ). toBe ( request . paymentId )
})
test ( 'should handle multiple retries' , async () => {
const connector = new TestSuiteApprover ( mockContext )
const request : AuthorizationRequest = {
paymentId: 'test-payment-456' ,
value: 10000 ,
currency: 'BRL' ,
}
// First call
await connector . authorize ( request )
// Multiple retries should return same result
const retry1 = await connector . authorize ( request )
const retry2 = await connector . authorize ( request )
const retry3 = await connector . authorize ( request )
expect ( retry1 ). toEqual ( retry2 )
expect ( retry2 ). toEqual ( retry3 )
})
})
Troubleshooting
Payment stuck in pending
If payments remain pending:
Verify callback() is being called after saving the response
Check VBase policies in manifest.json
Ensure paymentId is used consistently as the storage key
Verify the persisted response structure matches AuthorizationResponse
Callback not triggering retry
If the gateway doesn’t retry:
Ensure you’re calling this.callback() (not creating a new instance)
Verify the request object passed to callback matches the original
Check outbound access policy for callback endpoint
Review logs for callback errors
VBase storage errors
If VBase operations fail:
Confirm vbase-read-write policy in manifest
Check bucket name consistency
Verify VBase client is available in context
Handle VBase errors gracefully with try/catch