Search in documentation
ctrl+4K
Modules
Authentication
Merchant
Catalog
Order
Events
Logistics
Shipping
Review
Financial
Solutions

Best practices and troubleshooting

Recommendations for robust, secure, and efficient integration with the Shipping module, plus solutions for the most common problems.
Event polling is the heart of the integration. Implement it correctly: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?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
}
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);

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

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:
AttemptDelayTotal
1Immediate0s
21s + jitter1s
32s + jitter3s
44s + jitter7s
58s + jitter15s
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
// 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();
}

Always send a realistic preparationTime:
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
preparationTimeImpact
Too shortCourier arrives before order is ready; costs more
Too longCourier waits without purpose
CorrectPerfect synergy between preparation and delivery
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)

Order created

REQUEST_DRIVER (202)

Polling... waiting for success

REQUEST_DRIVER_SUCCESS
  ↓ (NOW, query tracking)

GET /tracking
Never query tracking before REQUEST_DRIVER_SUCCESS!
// 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
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`;

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
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

// 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
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
    })
  });
}

// 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`);
  }
}
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;
}

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

For details about global rate limiting and Shipping-specific limits, consult Rate limit.Summary for Shipping:
OperationLimit
Check availability4,000 req/min
Request/Create delivery4,000 req/min
Cancellation600 req/min
Manage addresses500 req/min
Tracking(included in polling)

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

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

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.
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

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

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

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)

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

Message: "Service not enabled for your store"Cause: Shipping service is not activated on your account.Solutions:
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

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

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

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

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

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

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

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

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?     ✓

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...

Symptom: Request returns 202, but I never receive SUCCESS/FAILED.Possible causes:
  • Polling not implemented
  • Events being ignored
  • quoteId expired
  • Invalid 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

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

GET /deliveryAvailabilities
  ↓ (validates coverage + gets quoteId)

POST /requestDriver (with quoteId)
Never skip this step.
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;
}
- Success rate per endpoint
- P95 latency (95th percentile)
- Errors by type
- Polling to processing time
- Timeout rate
timestamp: 2023-08-17T20:10:00Z
orderId: 57cd1046-2e06-446f-a2dd-18a518212c3c
operation: requestDriver
status: success
duration: 150ms

For technical questions:For operational problems:
Was this page helpful?
Rate your experience in the new Developer portal: