Overview
ADDI is a buy now, pay later (BNPL) platform that allows customers to split payments into installments. This integration provides seamless checkout and automatic payment tracking.
ADDI offers flexible payment terms, making high-value purchases more accessible to customers.
Prerequisites
Required Credentials
ADDI merchant account
Client ID
Client Secret
Webhook URL configured
Configuration
Environment Variables
Add these to your Firebase Functions configuration:
ADDI_CLIENT_ID = your_addi_client_id
ADDI_CLIENT_SECRET = your_addi_client_secret
Code Configuration
From ~/workspace/source/functions/addi.js:10-24:
const IS_SANDBOX = false ; // Set to true for testing
const ADDI_BASE_URL = IS_SANDBOX
? "https://api.addi-staging.com"
: "https://api.addi.com" ;
const ADDI_AUTH_URL = "https://auth.addi.com" ;
const ADDI_AUDIENCE = "https://api.addi.com" ;
const ADDI_CLIENT_ID = process . env . ADDI_CLIENT_ID ;
const ADDI_CLIENT_SECRET = process . env . ADDI_CLIENT_SECRET ;
const WEBHOOK_URL = "https://addiwebhook-muiondpggq-uc.a.run.app" ;
Authentication
ADDI uses OAuth 2.0 client credentials flow.
Get Access Token
From ~/workspace/source/functions/addi.js:28-54:
async function getAddiToken () {
try {
console . log ( `🔐 Requesting Token ( ${ IS_SANDBOX ? 'SANDBOX' : 'PROD' } )...` );
if ( ! ADDI_CLIENT_ID || ! ADDI_CLIENT_SECRET ) {
throw new Error ( "ADDI credentials missing." );
}
const response = await axios ({
method: 'post' ,
url: ` ${ ADDI_AUTH_URL } /oauth/token` ,
data: {
client_id: ADDI_CLIENT_ID . trim (),
client_secret: ADDI_CLIENT_SECRET . trim (),
audience: ADDI_AUDIENCE ,
grant_type: "client_credentials"
},
headers: { 'Content-Type' : 'application/json' }
});
return response . data . access_token ;
} catch ( error ) {
console . error ( "❌ Auth Error:" , error . response ?. data || error . message );
throw new Error ( "Error autenticando con ADDI" );
}
}
Creating ADDI Checkout
Function: createAddiCheckout
Creates an ADDI financing application.
Parameters
Firebase authentication token
Array of cart items with product IDs from Firestore
Customer information Colombian ID number (Cédula)
Additional order information
Example Request
const createAddiCheckout = firebase . functions (). httpsCallable ( 'createAddiCheckout' );
const result = await createAddiCheckout ({
userToken: await firebase . auth (). currentUser . getIdToken (),
items: [
{
id: 'prod_123' ,
quantity: 1 ,
color: 'Negro' ,
capacity: '256GB'
}
],
shippingCost: 20000 ,
buyerInfo: {
name: 'María González' ,
document: '1234567890' ,
phone: '3001234567' ,
address: 'Carrera 45 #67-89' ,
city: 'Medellín' ,
department: 'Antioquia'
},
extraData: {
needsInvoice: false
}
});
console . log ( result . data );
// { initPoint: 'https://api.addi.com/v1/online-applications/...' }
Response
ADDI application URL to redirect the customer
Implementation Flow
From ~/workspace/source/functions/addi.js:59-252:
1. Authenticate User
const userToken = data . userToken ;
let uid , email ;
if ( userToken ) {
const decoded = await auth . verifyIdToken ( userToken );
uid = decoded . uid ;
email = decoded . email ;
} else if ( context . auth ) {
uid = context . auth . uid ;
email = context . auth . token . email ;
} else {
throw new Error ( "User auth failed" );
}
2. Build Items and Calculate Total
let dbItems = [];
let subtotal = 0 ;
const removeAccents = ( str ) =>
str ? str . normalize ( "NFD" ). replace ( / [ \u0300 - \u036f ] / g , "" ) : "" ;
for ( const item of rawItems ) {
const pDoc = await db . collection ( 'products' ). doc ( item . id ). get ();
if ( ! pDoc . exists ) continue ;
const pData = pDoc . data ();
const price = Number ( pData . price ) || 0 ;
const qty = parseInt ( item . quantity ) || 1 ;
subtotal += price * qty ;
dbItems . push ({
id: item . id ,
name: pData . name ,
price: price ,
quantity: qty ,
color: item . color || "" ,
capacity: item . capacity || "" ,
mainImage: pData . mainImage || pData . image || "https://pixeltechcol.com/img/logo.png"
});
}
const totalAmount = subtotal + shippingCost ;
3. Create Order in Firestore
const newOrderRef = db . collection ( 'orders' ). doc ();
const firebaseOrderId = newOrderRef . id ;
await newOrderRef . set ({
source: 'TIENDA_WEB' ,
createdAt: admin . firestore . FieldValue . serverTimestamp (),
userId: uid ,
userEmail: email ,
userName: clientName ,
phone: clientPhone ,
clientDoc: clientDoc ,
shippingData: shippingData ,
items: dbItems ,
subtotal: subtotal ,
shippingCost: shippingCost ,
total: totalAmount ,
status: 'PENDIENTE_PAGO' ,
paymentMethod: 'ADDI' ,
paymentStatus: 'PENDING' ,
isStockDeducted: false
});
4. Prepare ADDI Payload
Data cleaning for ADDI API requirements:
// Clean document number
const cleanDoc = String ( clientDoc ). replace ( / \D / g , '' );
// Parse name
const fullNameParts = String ( clientName ). trim (). split ( " " );
const firstName = fullNameParts [ 0 ];
const lastName = fullNameParts . slice ( 1 ). join ( " " ) || "Apellido" ;
// Format phone number
let rawPhone = String ( clientPhone ). replace ( / \D / g , '' );
let cellNumber = rawPhone . startsWith ( '57' ) ? rawPhone . substring ( 2 ) : rawPhone ;
if ( ! cellNumber ) cellNumber = "3000000000" ;
// Clean city name
let cleanCity = removeAccents ( shippingData . city || "Bogota" ). trim ();
if ( cleanCity . toLowerCase (). includes ( "bogota" )) cleanCity = "Bogota D.C" ;
const addressObj = {
lineOne: removeAccents ( String ( shippingData . address || "Direccion" )). substring ( 0 , 60 ),
city: cleanCity ,
country: "CO"
};
5. Create ADDI Application
From ~/workspace/source/functions/addi.js:185-251:
const addiToken = await getAddiToken ();
const addiPayload = {
orderId: firebaseOrderId ,
totalAmount: totalAmount . toFixed ( 1 ),
shippingAmount: shippingCost . toFixed ( 1 ),
totalTaxesAmount: "0.0" ,
currency: "COP" ,
items: dbItems . map ( i => ({
sku: i . id . substring ( 0 , 50 ),
name: removeAccents ( i . name ). substring ( 0 , 50 ),
quantity: String ( i . quantity ),
unitPrice: Math . round ( i . price ),
tax: 0 ,
pictureUrl: i . mainImage || i . image ,
category: "technology" ,
brand: "PixelTech"
})),
client: {
idType: "CC" ,
idNumber: cleanDoc || "11111111" ,
firstName: removeAccents ( firstName ). substring ( 0 , 50 ),
lastName: removeAccents ( lastName ). substring ( 0 , 50 ),
email: String ( email ). trim (). toLowerCase (),
cellphone: cellNumber ,
cellphoneCountryCode: "+57" ,
address: addressObj
},
shippingAddress: addressObj ,
billingAddress: addressObj ,
allyUrlRedirection: {
logoUrl: "https://pixeltechcol.com/img/logo.png" ,
callbackUrl: WEBHOOK_URL ,
redirectionUrl: `https://pixeltechcol.com/shop/success.html?order= ${ firebaseOrderId } `
}
};
const response = await axios . post (
` ${ ADDI_BASE_URL } /v1/online-applications` ,
addiPayload ,
{
headers: {
'Authorization' : `Bearer ${ addiToken } ` ,
'Content-Type' : 'application/json' ,
'User-Agent' : 'PixelTechStore/1.0'
},
maxRedirects: 0 ,
validateStatus : status => status >= 200 && status < 400
}
);
// Extract redirect URL
let redirectUrl = null ;
if ( response . status === 301 || response . status === 302 ) {
redirectUrl = response . headers . location || response . headers . Location ;
} else if ( response . data ) {
redirectUrl = response . data . redirectionUrl ||
response . data . applicationUrl ||
response . data . _links ?. webRedirect ?. href ;
}
if ( ! redirectUrl ) throw new Error ( "ADDI no devolvió URL." );
return { initPoint: redirectUrl };
Webhook Handling
Function: webhook
Processes ADDI payment notifications.
From ~/workspace/source/functions/addi.js:260-376:
Webhook Payload
ADDI sends POST requests with:
{
"orderId" : "firebase_order_id" ,
"status" : "APPROVED" | "COMPLETED" | "REJECTED" | "DECLINED" | "ABANDONED" ,
"applicationId" : "addi_application_id"
}
Processing Approved Payments
if ( status === 'APPROVED' || status === 'COMPLETED' ) {
await db . runTransaction ( async ( t ) => {
const docSnap = await t . get ( orderRef );
if ( ! docSnap . exists ) return ;
const oData = docSnap . data ();
// Prevent duplicate processing
if ( oData . paymentStatus === 'PAID' || oData . status === 'PAGADO' ) {
console . log ( `⚠️ Webhook duplicado ignorado. La orden ${ orderId } ya estaba pagada.` );
return ;
}
// 1. Deduct stock
const prodReads = [];
if ( ! oData . isStockDeducted ) {
for ( const i of oData . items ) {
const pRef = db . collection ( 'products' ). doc ( i . id );
const pDoc = await t . get ( pRef );
if ( pDoc . exists ) {
const pData = pDoc . data ();
let newStock = ( pData . stock || 0 ) - ( i . quantity || 1 );
let combinations = pData . combinations || [];
// Handle variants
if ( i . color || i . capacity ) {
const idx = combinations . findIndex ( c =>
( c . color === i . color || ( ! c . color && ! i . color )) &&
( c . capacity === i . capacity || ( ! c . capacity && ! i . capacity ))
);
if ( idx >= 0 ) {
combinations [ idx ]. stock = Math . max ( 0 , combinations [ idx ]. stock - i . quantity );
}
}
prodReads . push ({
ref: pRef ,
stock: Math . max ( 0 , newStock ),
combos: combinations
});
}
}
}
// 2. Update treasury
const accQ = await t . get (
db . collection ( 'accounts' )
. where ( 'gatewayLink' , '==' , 'ADDI' )
. limit ( 1 )
);
let accDoc = ( ! accQ . empty ) ? accQ . docs [ 0 ] : null ;
if ( ! accDoc ) {
const defQ = await t . get (
db . collection ( 'accounts' )
. where ( 'isDefaultOnline' , '==' , true )
. limit ( 1 )
);
if ( ! defQ . empty ) accDoc = defQ . docs [ 0 ];
}
if ( accDoc ) {
t . update ( accDoc . ref , {
balance: ( Number ( accDoc . data (). balance ) || 0 ) + Number ( oData . total )
});
const incRef = db . collection ( 'expenses' ). doc ();
t . set ( incRef , {
amount: Number ( oData . total ),
category: "Ingreso Ventas Online" ,
description: `Venta ADDI # ${ orderId . slice ( 0 , 8 ) } ` ,
paymentMethod: accDoc . data (). name ,
date: admin . firestore . FieldValue . serverTimestamp (),
type: 'INCOME' ,
orderId: orderId ,
supplierName: oData . userName
});
}
// 3. Apply stock updates
for ( const p of prodReads ) {
t . update ( p . ref , { stock: p . stock , combinations: p . combos });
}
// 4. Create remission (check if exists first)
const remRef = db . collection ( 'remissions' ). doc ( orderId );
const remSnap = await t . get ( remRef );
if ( ! remSnap . exists ) {
t . set ( remRef , {
orderId ,
source: 'WEBHOOK_ADDI' ,
items: oData . items ,
clientName: oData . userName ,
clientPhone: oData . phone ,
clientDoc: oData . clientDoc ,
clientAddress: ` ${ oData . shippingData ?. address } , ${ oData . shippingData ?. city } ` ,
total: oData . total ,
status: 'PENDIENTE_ALISTAMIENTO' ,
type: 'VENTA_WEB' ,
createdAt: admin . firestore . FieldValue . serverTimestamp ()
});
}
// 5. Update order
t . update ( orderRef , {
status: 'PAGADO' ,
paymentStatus: 'PAID' ,
paymentId: body . applicationId || 'ADDI' ,
updatedAt: admin . firestore . FieldValue . serverTimestamp (),
isStockDeducted: true
});
});
}
Handling Rejected Applications
else if ( status === 'REJECTED' || status === 'DECLINED' || status === 'ABANDONED' ) {
const docCheck = await orderRef . get ();
if ( docCheck . exists && docCheck . data (). paymentStatus !== 'PAID' ) {
await orderRef . update ({
status: 'RECHAZADO' ,
statusDetail: status
});
console . log ( "❌ Orden Rechazada por ADDI" );
}
}
ADDI Status Flow
ADDI Status Codes
Status Description Action PENDINGApplication submitted Wait for customer APPROVEDCredit approved Process payment COMPLETEDPayment completed Process payment REJECTEDCredit denied Mark as RECHAZADO DECLINEDCustomer declined Mark as RECHAZADO ABANDONEDCustomer left checkout Mark as RECHAZADO
Data Sanitization
ADDI requires clean data. The integration removes accents and validates formats:
Remove Accents
const removeAccents = ( str ) =>
str ? str . normalize ( "NFD" ). replace ( / [ \u0300 - \u036f ] / g , "" ) : "" ;
City Name Handling
let cleanCity = removeAccents ( shippingData . city || "Bogota" ). trim ();
if ( cleanCity . toLowerCase (). includes ( "bogota" )) {
cleanCity = "Bogota D.C" ;
}
let rawPhone = String ( clientPhone ). replace ( / \D / g , '' );
let cellNumber = rawPhone . startsWith ( '57' ) ? rawPhone . substring ( 2 ) : rawPhone ;
if ( ! cellNumber ) cellNumber = "3000000000" ; // Fallback
Testing
Sandbox Mode
Enable sandbox in configuration:
const IS_SANDBOX = true ;
const ADDI_BASE_URL = "https://api.addi-staging.com" ;
Test Flow
Create checkout with valid test data
Complete ADDI application in staging environment
ADDI will send webhook notification
Verify order status updates
Check stock deduction
Confirm treasury update
Test Customer Data
{
name : "Test User" ,
document : "1234567890" ,
phone : "3001234567" ,
email : "[email protected] " ,
address : "Calle 123" ,
city : "Bogota"
}
Treasury Configuration
Create an ADDI treasury account:
// Firestore: accounts collection
{
name : "ADDI" ,
gatewayLink : "ADDI" ,
balance : 0 ,
isDefaultOnline : false ,
type : "ONLINE_PAYMENT"
}
Error Handling
Cause: Environment variables not setSolution: firebase functions:config:set addi.client_id="YOUR_ID"
firebase functions:config:set addi.client_secret="YOUR_SECRET"
Error autenticando con ADDI
Cause: Invalid credentials or OAuth errorSolution: Verify credentials in ADDI dashboard
Cause: Invalid payload or API errorSolution: Check logs for ADDI API response details
Duplicate webhook processing
Cause: ADDI sent multiple notificationsSolution: Already handled - system checks paymentStatus === 'PAID'
Best Practices
Clean data - Always remove accents and special characters
Validate documents - Ensure Colombian ID format
Handle retries - ADDI may retry webhooks
Log everything - Track application IDs for support
Test thoroughly - Use sandbox before production
Next Steps
MercadoPago Integration Add credit card payments
Sistecrédito Add another financing option
Treasury Setup Configure ADDI account
Order Management Process ADDI orders