Skip to main content

Command Palette

Search for a command to run...

APEX + OCI Email Logs: Track Bounces, Complaints, Suppression

Published
10 min read
APEX + OCI Email Logs: Track Bounces, Complaints, Suppression
J
Hi, thanks for stopping by! I am focused on designing and building innovative solutions using AI, the Oracle Database, Oracle APEX, and Oracle REST Data Services (ORDS). I hope you enjoy my blog.

Introduction

I am sure many of you are already using the OCI Email Delivery Service to send emails from your APEX Applications. It offers a convenient and inexpensive way to handle emails that integrates easily with APEX. Matt Mulvaney wrote a step-by-step guide to setting it up here.

If you are using this service and have asked yourself these questions, then this post is for you:

  • How do I know if my email was bounced?

  • How do I know if my emails are getting marked as spam?

  • Basically, did the recipient receive the email?

To answer these questions, you must enable logging for your OCI Email Service. In this post, we will:

  • Enable Email Delivery logs (OutboundAccepted/OutboundRelayed)

  • Query logs via Logging Search API

  • Surface results in APEX (Interactive Report) and/or sync to a table for history

Suppression Vs Bounce

Before we start, it is important to understand what the two types of email delivery logs reveal.

A bounce is a downstream delivery failure reported by the recipient’s mail system after an attempt is made (typically seen in the OutboundRelayed log) and usually points to issues such as an invalid mailbox, a missing domain, or temporary recipient-side problems.

Suppression often happens before any delivery attempt; your send can look “fine” from APEX’s perspective, but Email Delivery may block or drop the message due to policy, reputation, or suppression-list conditions (often visible in OutboundAccepted and sometimes reflected in log messages indicating a suppressed recipient). Practically, this is the difference between “the destination rejected it” and “we never really tried,” and it changes your remediation: bounces drive address hygiene and retry rules, while suppression drives sender/domain configuration and suppression-list/deliverability review.

What Can I Learn

I use these logs for APEX Developer Blogs, which has over 300 subscribers. Here are some examples of errors from these logs:

APEX Page Showing Email LOgs

Setup Logging

Let's start by setting up logging from the OCI Console.

Navigation: Developer Services > Email Delivery > Click on Your Domain

OCI Email Delivery Setup for Domain

Then click on the 'Monitoring' tab and scroll down to the 'Logs' section, click the ellipses for the 'Outbound Relayed' log, and click 'Enable Log'.

OCI Email Delivery Monitoring Logging
💡
Enable both OutboundAccepted and OutboundRelayed to detect both suppression and delivery outcomes.

If you don’t already have a log group set up, click 'Create new group':

OCI Email Delivery - Enable Resource Log 1

Enter a log group name and description, and click 'Create':

OCI Email Delivery - Enable Resource Log 2

Once back on the Enable resource log page, click 'Enable log':

OCI Email Delivery - Enable Resource Log 3

After a few seconds, your log should be active:

OCI Email Delivery - Log Group and Log Active

Adjust the retention period to match your audit needs/cost constraints.

💡
Make a note of the OCIDs for the log group and the log. We will use these later.

Test the Logs

Send a test email from your instance to make sure it shows up in the logs:

DECLARE
  l_body  CLOB;
BEGIN
  l_body := '<h1>Testing APEX Mail</h1>';
  apex_mail.send
   (p_to        => 'test@example.com',
    p_from      => 'info@example.com',
    p_body      => l_body,
    p_body_html => l_body,
    p_subj      => 'Testing APEX Mail');
  apex_mail.push_queue;
END;
💡
Remember to set p_from to an email address that is on your OCI Email Delivery Approved Sender List.

After a few seconds, you should see the message in the logs:

OCI Email Delivery -Explore Log

If we send an email to an invalid email address, we can see the bounce from the destination email server. Note: I have changed the OCIDs in the sample JSON below to 'AAA' and the domains to example.com.

{
  "datetime": 1771705254189,
  "logContent": {
    "data": {
      "action": "bounce",
      "bounceCategory": "bad-mailbox",
      "bounceCode": "5.1.10",
      "errorType": "hard",
      "message": "Suppressed recipient sam@example.com for email from info@example.com: bad-mailbox hard bounce",
      "messageId": "4B5C41B678F782A0E063E815000AC99A@apps.example.com",
      "originalMessageAcceptedTime": "2026-02-21T20:20:39.614Z",
      "receivingDomain": "example.com",
      "recipient": "sam@example.com",
      "reportGeneratedTime": "2026-02-21T20:20:41Z",
      "sender": "info@example.com",
      "senderCompartmentId": "AAA",
      "senderId": "AAA",
      "smtpStatus": "550 5.1.10 RESOLVER.ADR.RecipientNotFound; Recipient sam@example.com not found by SMTP address lookup"
    },
    "id": "4e81ce60-b8c7-40be-8a19-79448a3f4f2d",
    "oracle": {
      "compartmentid": "AAA",
      "ingestedtime": "2026-02-21T20:20:56.815Z",
      "loggroupid": "AAA",
      "logid": "AAA",
      "tenantid": "AAA"
    },
    "source": "example.com",
    "specversion": "1.0",
    "time": "2026-02-21T20:20:54.189Z",
    "type": "com.oraclecloud.emaildelivery.emaildomain.outboundrelayed"
  },
  "regionId": "us-phoenix-1"
}

In the above example, we received a hard bounce, indicating that the email address was invalid.

💡
Knowing that an email was not delivered can be critical to your workflow. Knowing why it was not delivered allows you to address the issue.

Documentation

Access the Logs from a REST API

Even though the OCI console includes a deliverability dashboard and a UI to access logs, it would be much easier if we could get these logs into the database so we can view them from an APEX page. In this section, I will cover how to set up an OCI service account to access the OCI Logging REST API.

Create an OCI User

Navigation: Identity and Security > Domains > Select your domain > Click Create

Create OCI User - Step 1

Enter a username and click 'Create':

Create OCI User - Step 2

Click Actions > Edit User Capabilities:

Create OCI User - Step 3

Uncheck all options except 'API Keys' and click 'Save Changes':

Create OCI User - Step 4
💡
Keep both key files safe. You will use the content of the private file in the APEX Web Credential below.

On the user page, select the 'API keys' tab, then click Actions > Add API key

Create OCI User - Step 5

Download the public and private key, then click 'Add':

Create OCI User - Step 6
💡
Copy the resulting 'Configuration file preview' details and keep them safe. You will use these values in the APEX Web Credential below.

Create an OCI Group

Back under the User Management tab, scroll down to Groups:

Create OCI Group - Step 1

Create a new Group:

Create OCI Group - Step 2

Add the new user to the group:

Create OCI Group - Step 3

Create an OCI Policy

Navigate to: Identity & Security > Policies > Click 'Create Policy':

Create OCI Policy - Step 1

Complete the Policy details and click the 'Create' button:

Create OCI Policy - Step 2

Policy Statements:

allow group apex_rest_api_access_grp to read log-groups in tenancy
allow group apex_rest_api_access_grp to read log-content in tenancy
💡
If you prefer least privilege, scope the policy to the compartment that contains the log group (instead of the tenancy-wide scope).

The Logging REST API

The OCI logging service offers a REST API you can use to consume any logs. My instance is in the Phoenix region, so my endpoint is:

https://logging.us-phoenix-1.oci.oraclecloud.com/20190909/search

You can see a full list of Logging Endpoints here, and details on using the search API here. You will also need to understand the logging query language. This query language allows you to filter results to see only bounces if that is what you are interested in.

The endpoint requires that you send a POST request with a payload like this:

{
  "timeStart": "2026-01-19T01:02:29.600Z",
  "timeEnd":   "2026-01-19T02:02:29.600Z",
  "searchQuery": "search \"<tenancy_ocid>/<log_group_ocid>/<log_ocid>\" | sort by datetime desc",
  "isReturnFieldInfo": false
}
💡
You must replace the <value> placeholders with the actual OCIDs for your Tenancy, Log Group, and Log, respectively.

API Limits

The Logging Search API returns up to 1000 entries per call and supports paging via a next-page token/header (client-managed). Searches/exports are limited to a maximum 14-day time window per request.

Consuming the Logging API from APEX

Create an APEX Web Credential

You will need an APEX Web Credential of type 'OCI Native Authentication' to access the REST API from APEX.

Create APEX Web Credential - Step 1

Enter the details from your OCI user's 'Configuration file preview' above. For the OCI Private Key, copy and paste the value from the private key file downloaded above. Click Create to complete the creation of the APEX Web Credential.

Create APEX Web Credential - Step 2

Consumption Options

APEX REST Data Source

The obvious first choice for consuming a REST API is to use a REST Data Source. I won't get into the step-by-step, but here are a few things that tripped me up when I created one.

In the 'POST' Operation, set the 'Database Operation' field to 'Fetch rows':

REST Data Source Setup - Step 1
💡
You will want to replace hardcoded timeStart and timeEnd with variables such as #START_TS# and #END_TS#.

Delete the GET, PUT, and DELETE Operations; we do not need them.

Set up the following parameters:

REST Data Source Setup - Step 1

In the Data Profile, set the 'Row Selector' field to 'results'.

REST Data Source Setup - Step 2

Once all the above is in place, click 'Rediscover Data Profile', then click 'Replace Data Profile'.

REST Data Source Setup - Step 3

Unfortunately, REST Source Types for 'Oracle Cloud Infrastructure (OCI) REST Service' do not automatically walk OCI Logging Search paging tokens. If you are only expecting a few hundred emails a week, that should not be an issue, as you can fetch up to 1,000 log entries at a time. You could create an Interactive Report based on the REST Source, add some Start and End Date Time parameters, and you have everything you need.

If you expect higher email volumes, you could use APEX_WEB_SERVICE to retrieve the data and loop through the pages yourself, or build a REST Source Connector plug-in.

Syncing to a Table

If you expect to send fewer than 1,000 emails per hour, it may be easier to use a REST Source Sync to sync the last hour's logs to a local table. This has the advantage of circumventing the 14-day window limit of the logging API.

You will need to use the REST Source Sync 'Steps' feature to pass the limit, START_TS, and END_TS parameters. In the example below, I am looking back 1 day. You may need to adjust this based on the number of emails you expect to receive.

REST Source Sync Steps
💡
Remember to add a purge routine to periodically clear old records from the sync table.

Conclusion

Enabling OutboundAccepted and OutboundRelayed logs gives you a reliable way to determine whether an email was delivered, bounced, complained about, or suppressed.

For low-volume use, a REST Data Source plus an Interactive Report is usually sufficient, as long as you stay within the 1,000 records-per-call limit. For higher volume or longer retention, use a REST Source Sync (or PL/SQL paging) to persist results to a table and work around the 14-day query window. Once the data is local, you can join it to your tables and build deliverability views and alerts that meet your requirements.