Best practices and troubleshooting Recommendations for robust, secure, and efficient integration with the Shipping module, plus solutions for the most common problems. Event polling Event polling is the heart of the integration. Implement it correctly: Correct interval Minimum: 30 seconds
const POLLING_INTERVAL = 30000 ; // 30 seconds
setInterval ( async () => {
try {
const events = await fetchEvents ();
for ( const event of events ) {
await processEvent ( event );
await acknowledgeEvent ( event . eventId );
}
} catch ( error ) {
logger . error ( 'Polling failed' , error );
}
}, POLLING_INTERVAL ); Why 30 seconds? Event acknowledgment Always send acknowledge immediately after processing:
async function processEvent ( event ) {
// 1. Process
await updateOrderStatus ( event . orderId , event . type );
// 2. Acknowledge IMMEDIATELY
await acknowledgeEvent ( event . eventId );
// Without acknowledge = continuous reprocessing
} Deduplication Implement protection against duplicate events:
const processedEventIds = new Set ();
async function processEvent ( event ) {
if ( processedEventIds . has ( event . eventId )) {
logger . info ( `Event ${ event . eventId } already processed` );
return ; // Ignore duplicate
}
// ... process event ...
processedEventIds . add ( event . eventId );
}
// Clear Set periodically (e.g., every 24h)
setInterval (() => {
processedEventIds . clear ();
}, 24 * 60 * 60 * 1000 ); Authentication and tokens Token renewal Renew 5 minutes before expiration:
const TOKEN_BUFFER = 5 * 60 * 1000 ; // 5 minutes
async function ensureValidToken () {
if (! token || isExpiringSoon ( token )) {
token = await refreshToken ();
token . refreshedAt = Date . now ();
}
return token ;
}
function isExpiringSoon ( token ) {
const expiresIn = token . expiresAt - Date . now ();
return expiresIn < TOKEN_BUFFER ;
} Never: Let a token expire before renewing Renew a token with every request Reuse old tokens Error handling Retry with exponential backoff Recommended implementation:
async function requestWithRetry ( fn , maxRetries = 5 ) {
for ( let attempt = 1 ; attempt <= maxRetries ; attempt ++) {
try {
return await fn ();
} catch ( error ) {
// Transient errors: retry
if ( isTransient ( error )) {
if ( attempt < maxRetries ) {
const delay = Math . pow ( 2 , attempt - 1 ) * 1000 ; // 1s, 2s, 4s, 8s...
const jitter = Math . random () * 1000 ; // Prevents thundering herd
await sleep ( delay + jitter );
continue ;
}
}
// Permanent errors: fail immediately
throw error ;
}
}
}
function isTransient ( error ) {
// 5xx, timeouts, rate limiting
return error . status >= 500 ||
error . status === 429 ||
error . code === 'ECONNREFUSED' ;
} Recommended pattern: Attempt Delay Total 1 Immediate 0s 2 1s + jitter 1s 3 2s + jitter 3s 4 4s + jitter 7s 5 8s + jitter 15s
Structured logging Always include context:
const logEntry = {
timestamp: new Date (). toISOString (),
orderId: order . id ,
operation: 'requestDriver' ,
status: 'success' ,
duration: Date . now () - startTime ,
attempt: currentAttempt ,
error: error ?. message ,
statusCode: response ?. status ,
};
logger . info ( 'Shipping operation' , logEntry ); Keep logs for minimum: 30 days Data validation Before sending to iFood
// Validate orderId
function isValidUUID ( uuid ) {
return / ^ [ 0-9a-f ] {8} - [ 0-9a-f ] {4} - [ 0-9a-f ] {4} - [ 0-9a-f ] {4} - [ 0-9a-f ] {12} $ / i . test ( uuid );
}
// Validate phone
function isValidPhone ( countryCode , areaCode , number ) {
return countryCode === '55' && // Brazil
areaCode . length === 2 &&
( number . length === 8 || number . length === 9 );
}
// Validate postal code
function isValidPostalCode ( postalCode ) {
return / ^ \d {8} $ / . test ( postalCode . replace ( '-' , '' ));
}
// Validate coordinates
function isValidCoordinates ( lat , lng ) {
return lat >= - 90 && lat <= 90 &&
lng >= - 180 && lng <= 180 ;
}
// Validate quoteId before using
function isValidQuote ( quote ) {
return quote &&
isValidUUID ( quote . id ) &&
new Date ( quote . expirationAt ) > new Date ();
} Preparation time Always send a realistic preparationTime: How to calculate
1. Average preparation time (benchmarking)
Ex: 12 minutes
2. Add margin for peak hours
Ex: +3 minutes
3. Convert to seconds
(12 + 3) * 60 = 900 seconds Why it matters preparationTime Impact Too short Courier arrives before order is ready; costs more Too long Courier waits without purpose Correct Perfect synergy between preparation and delivery
Monitor and adjust
Week 1: preparationTime = 900s (15 min)
↓ (Monitor deliveries)
↓
If courier arrives very early:
→ Increase to 1000s (16 min)
If courier arrives very late:
→ Decrease to 800s (13 min) Delivery tracking Correct timing
Order created
↓
REQUEST_DRIVER (202)
↓
Polling... waiting for success
↓
REQUEST_DRIVER_SUCCESS
↓ (NOW, query tracking)
↓
GET /tracking Never query tracking before REQUEST_DRIVER_SUCCESS! Polling interval for tracking
// After REQUEST_DRIVER_SUCCESS
setInterval ( async () => {
try {
const tracking = await getTracking ( orderId );
updateUI ( tracking );
} catch ( error ) {
if ( error . status === 404 ) {
logger . info ( 'Tracking not yet available' );
// Courier still being assigned, retry later
}
}
}, 30000 ); // 30 seconds Handling null values Fields may return null during assignment:
const tracking = await getTracking ( orderId );
// Safe: check null before using
if ( tracking . latitude && tracking . longitude ) {
updateMapMarker ( tracking . latitude , tracking . longitude );
}
// Can be null
if ( tracking . expectedDelivery ) {
updateETA ( tracking . expectedDelivery );
}
// Can be negative (pickup delay)
const pickupDelay = tracking . pickupEtaStart < 0
? `Delayed ${ Math . abs ( tracking . pickupEtaStart ) } s`
: ` ${ tracking . pickupEtaStart } s remaining` ; Monitoring and alerts Essential metrics
1. Success rate per endpoint
- Alert: < 95%
- Interval: 1 hour
2. P95 latency
- Alert: > 5 seconds
- Interval: 5 minutes
3. Error rate by type
- Alert: HighDemand > 20%
- Interval: 15 minutes
4. Consecutive polling failures
- Alert: > 3 failures in a row
- Interval: real-time
5. Timeout rate
- Alert: > 5%
- Interval: 1 hour Critical alerts
1. Token renewal failure
→ All requests will fail with 401
2. Polling stopped
→ Events will not be processed
3. Error rate > 10% for 30 min
→ System-wide issue in progress
4. Event delay > 5 min
→ Possible system bottleneck Asynchronous operations Never assume success from 202
// WRONG
const response = await requestDriver ( orderId , quoteId );
// response.status === 202
console . log ( 'Courier allocated!' ); // WRONG!
// CORRECT
const response = await requestDriver ( orderId , quoteId );
// response.status === 202
logger . info ( 'Allocation requested, awaiting confirmation...' );
// Then monitor events
// Only confirm when receiving REQUEST_DRIVER_SUCCESS Idempotency Use Idempotency-Key in critical operations:
async function cancelOrder ( orderId ) {
const idempotencyKey = `cancel- ${ orderId } - ${ Date . now () } ` ;
return fetch ( `/orders/ ${ orderId } /cancel` , {
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ token } ` ,
'Idempotency-Key' : idempotencyKey
},
body: JSON . stringify ({
reason: 'Customer requested' ,
cancellationCode: 817
})
});
} Delivery security (On-Demand) Confirmation code
// Monitor event to get code
function handleEvent ( event ) {
if ( event . type === 'DELIVERY_DROP_CODE_REQUESTED' ) {
const code = event . metadata . CODE ;
// Notify customer via SMS/email/app
sendCodeToCustomer ( customer . phone , code );
// Instruct courier to request code
updateDeliveryInstructions ( orderId ,
`Request code ${ code } before delivery` );
}
} Safe Delivery Score Use score to make decisions:
const safeDelivery = await getSafeDeliveryScore ( orderId );
switch ( safeDelivery . score ) {
case 'LOW' :
// Require additional validation
sendVerificationToCustomer ( orderId );
addAlert ( `Low-risk delivery: ${ orderId } ` );
break ;
case 'MODERATE' :
// Monitor normally
logger . info ( `Moderate risk: ${ orderId } ` );
break ;
case 'HIGH' :
case 'VERY_HIGH' :
// Standard processing
break ;
} Test integration Pre-production checklist
Orders on platform:
[ ] GET /deliveryAvailabilities works
[ ] POST /requestDriver returns 202
[ ] Events are received
[ ] GET /tracking works after success
[ ] Cancellation works
Orders outside platform:
[ ] GET /merchants/{merchantId}/deliveryAvailabilities works
[ ] POST /merchants/{merchantId}/orders returns 202 + trackingUrl
[ ] Confirmation code is received
[ ] Address changes work
[ ] Safe Delivery Score works
Errors:
[ ] HighDemand is handled with retry
[ ] 404 on tracking is ignored before success
[ ] Rate limiting is respected
[ ] Expired tokens are renewed
Monitoring:
[ ] Structured logs at all points
[ ] Alerts configured
[ ] Dashboard visible
[ ] Metrics being collected Rate limiting For details about global rate limiting and Shipping-specific limits, consult Rate limit . Summary for Shipping: Operation Limit Check availability 4,000 req/min Request/Create delivery 4,000 req/min Cancellation 600 req/min Manage addresses 500 req/min Tracking (included in polling)
Availability errors (400 Bad Request) DeliveryDistanceTooHigh Message: "On-Demand unavailable: delivery address is more than 10 km from your store."Cause: Address is outside iFood logistics coverage.Solutions: Confirm the coordinates (latitude/longitude) sent Validate the address with the customer Check if the region has iFood coverage available Consult the coverage map in Partner Portal ServiceAreaMismatch Message: "Address outside iFood coverage"Cause: Region is not served by iFood logistics.Solutions: Check iFood coverage in customer's region Consult iFood coverage map Offer alternative logistics for that region OffOpeningHours Message: "Outside operating hours"Cause: Request was made outside logistics operating hours.Solutions: Try again within business hours Set up alerts for operating hours Implement automatic retry with exponential backoff Operating hours: Vary by region. Consult Partner Portal.HighDemand Message: "iFood logistics is temporarily unavailable. Try again later."Cause: Courier fleet is saturated during peak demand.Solutions: Implement retry with exponential backoff (2x, max 8 attempts) Use larger preparationTime to distribute demand over time Monitor peak hours and plan ahead Consider offering delivery alternatives Recommended strategy:
Attempt 1: 1s
Attempt 2: 2s
Attempt 3: 4s
Attempt 4: 8s
Attempt 5: 16s OriginNotFound Message: "Store not found in logistics area"Cause: Your store is not registered in iFood logistics system.Solutions: Go to Partner Portal Verify your store address is correct Register your store in available logistics areas Wait for validation (may take 24-48 hours) Contact support if store continues to not be found BadRequestMerchant Message: "Your store is temporarily unavailable"Cause: Your store has operation restrictions or is temporarily deactivated.Solutions: Check store status in Partner Portal Review pending documents (contract, address proof) Contact support if you have questions about status InvalidPaymentMethods Message: "Payment method not supported"Cause: Payment method sent is invalid or not accepted.Solutions: Validate payment method against the quote Use only CREDIT, DEBIT, or CASH Check available methods in availability response Verify card brand is accepted (Visa, Mastercard, Elo) NRELimitExceeded Message: "Simultaneous couriers limit reached"Cause: You reached your contracted simultaneous allocation limit.Solutions: Wait for ongoing deliveries to complete Cancel non-viable deliveries Contact support to increase limit Monitor concurrency metrics MerchantEasyDeliveryDisabled Message: "Service not enabled for your store"Cause: Shipping service is not activated on your account.Solutions: Request errors (400 Bad Request) BadRequest Message: "There was a problem with your request..."Cause: Data sent in request is invalid or malformed.Solutions: Validate UUID format of orderId or merchantId Check all required fields were sent Confirm data types (string vs. number vs. boolean) Review endpoint documentation examples UUID validation:
Valid format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Example: 57cd1046-2e06-446f-a2dd-18a518212c3c BadRequestCustomer Message: "Customer unavailable or invalid"Cause: Invalid customer data or customer cannot receive deliveries.Solutions: Validate name: max 50 characters Validate phone:Country: 2 digits (ex: 55) Area: 2 digits (ex: 11) Number: 7-9 digits Confirm phone is valid (not expired, active) Check customer has no restrictions Tracking errors 404 Not Found (Tracking) Message: "Order not found"Cause: Courier has not yet been assigned to the order.Solutions: Wait for ASSIGN_DRIVER or REQUEST_DRIVER_SUCCESS event Implement retry after receiving confirmation Recommended retry interval: 30 seconds Fields may return null during assignment Correct flow:
1. Request courier (202)
2. Wait for REQUEST_DRIVER_SUCCESS event (polling)
3. After that, tracking becomes available Address change errors MaxDistanceHigherThanAllowed Message: "Change > 500m from original coordinates"Cause: Customer requested change to more than 500m distance.Solutions: Limit changes to maximum 500m For larger distances, cancel and recreate the order Implement frontend validation before sending RegionMismatch Message: "New address in different region"Cause: New address falls in different iFood coverage area.Solutions: Validate new address maintains coverage Cancel and recreate order if changing regions Implement region validation on frontend ChangeAddressOperationConflict Message: "Operation conflict"Cause: Address change already pending.Solutions: Wait for previous change result Monitor DELIVERY_ADDRESS_CHANGE_* events Implement request queue Default timeout: 15 minutes Authentication errors (401) Token expired or invalid Message: "Unauthorized"Cause: JWT token expired or invalid.Solutions: Renew token before expiration Implement automatic refresh (renew 5 minutes before expiry) Store expiresAt to track validity Do not reuse old tokens Internal errors (500) Internal Server Error Message: "Oops. There was a failure. Try again in a moment."Cause: Temporary error on iFood server.Solutions: Implement retry with exponential backoff Maximum 3-5 attempts Interval: 1s, 2s, 4s, 8s, 16s Log all attempts After failures, notify support Polling problems Missing events Symptom: I'm not receiving delivery events.Possible causes: Polling interval too long (>30s) Events consumed elsewhere Event ID not recognized System not acknowledging Solutions: Implement polling every 30 seconds (never less) Use /polling endpoint correctly Implement immediate acknowledgment with /acknowledgment Check events have unique eventId Implement deduplication by eventId Checklist:
1. Polling every 30s? ✓
2. Event acknowledgment? ✓
3. Deduplication by ID? ✓
4. Logs for each event? ✓ Rate limiting (429) Message: "Too Many Requests"Cause: You exceeded endpoint request limit.Solutions: Respect polling interval (min 30s) Implement request queue Use exponential backoff Check endpoint-specific limits Monitor rate limit headers in response Important headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 50
X-RateLimit-Reset: 1693... Integration problems Order stays pending Symptom: Request returns 202, but I never receive SUCCESS/FAILED.Possible causes: Polling not implemented Events being ignored quoteId expiredInvalid data Solutions: Check polling is running Confirm events are being processed Validate quoteId is still valid (< 24h) Review logs for silent errors Contact support with orderId for investigation Courier not allocated Symptom: I receive REQUEST_DRIVER_SUCCESS, but cannot track.Possible causes: System still processing allocation Error in background assignment Timeout before ASSIGN_DRIVER Solutions: Wait for ASSIGN_DRIVER event (may take minutes) Implement tracking retry (max 5 attempts) Interval between attempts: 30-60s If timeout continues, check coverage Contact support with orderId Best practices to avoid errors 1. Always check availability
GET /deliveryAvailabilities
↓ (validates coverage + gets quoteId)
↓
POST /requestDriver (with quoteId) Never skip this step. 2. Implement smart retry
async function requestWithRetry ( fn , maxRetries = 3 ) {
for ( let attempt = 1 ; attempt <= maxRetries ; attempt ++) {
try {
return await fn ();
} catch ( error ) {
if ( isTransient ( error ) && attempt < maxRetries ) {
const delay = Math . pow ( 2 , attempt - 1 ) * 1000 ;
await sleep ( delay );
} else {
throw error ;
}
}
}
}
function isTransient ( error ) {
return error . status >= 500 || error . status === 429 ;
} 3. Monitor metrics
- Success rate per endpoint
- P95 latency (95th percentile)
- Errors by type
- Polling to processing time
- Timeout rate 4. Structured logging
timestamp: 2023-08-17T20:10:00Z
orderId: 57cd1046-2e06-446f-a2dd-18a518212c3c
operation: requestDriver
status: success
duration: 150ms
Was this page helpful? Yes No
Rate your experience in the new Developer portal: Rate now