How to Show Data from a REST API in a Report in Oracle APEX

Publish an API with ORDS, create a REST Data Source, filter an Interactive Report through a URL parameter, and copy the rows into a local table.

Many applications need data that lives in another system: a supplier price list, a stock service, or an API of another team. Copying that data by hand gets old fast, and writing HTTP calls and JSON parsing for every page is a lot of code to maintain.

Oracle APEX solves this with REST Data Sources. You describe the web service once in Shared Components, and then any report can use it like a table.

This guide builds a REST Data Source on an ORDS service, shows it in an Interactive Report with a category filter that is sent to the API, and copies the rows into a local table with REST Source Synchronization.

The overview Oracle APEX Data Sources: REST, Data Loads, and Duality Views explains the source types and uses a public web service with local post-processing.

This guide is the hands-on build: your own API, a parameter driven by a page item, the network ACL the database needs, and a scheduled copy into a local table. The example is a product catalog; the same steps work for any API that returns JSON.

Step 1: Create the Tables

The API in this example reads the CATALOG_ITEMS table. In a real project this table lives in the other system; here it is in the same database so that you can rebuild everything yourself:

Example:

create table catalog_items (
    item_id     number generated by default as identity
                constraint catalog_items_pk primary key,
    sku         varchar2(20)  not null
                constraint catalog_items_sku_uk unique,
    item_name   varchar2(100) not null,
    category    varchar2(30)  not null
                constraint catalog_items_category_ck
                check (category in ('Computers', 'Accessories', 'Furniture')),
    unit_price  number(10,2)  not null
                constraint catalog_items_price_ck check (unit_price > 0),
    in_stock    number default on null 0 not null
                constraint catalog_items_stock_ck check (in_stock >= 0),
    updated_on  date default on null sysdate
);

create index catalog_items_category_ix on catalog_items (category);

insert into catalog_items (sku, item_name, category, unit_price, in_stock)
values ('CMP-100', 'Laptop 14 inch', 'Computers', 899, 25);

insert into catalog_items (sku, item_name, category, unit_price, in_stock)
values ('CMP-110', 'Laptop 16 inch', 'Computers', 1299, 12);

insert into catalog_items (sku, item_name, category, unit_price, in_stock)
values ('CMP-200', 'Desktop Tower', 'Computers', 1099, 8);

insert into catalog_items (sku, item_name, category, unit_price, in_stock)
values ('CMP-300', 'Monitor 27 inch', 'Computers', 329, 40);

insert into catalog_items (sku, item_name, category, unit_price, in_stock)
values ('ACC-100', 'Wireless Mouse', 'Accessories', 25, 150);

insert into catalog_items (sku, item_name, category, unit_price, in_stock)
values ('ACC-110', 'Mechanical Keyboard', 'Accessories', 89, 60);

insert into catalog_items (sku, item_name, category, unit_price, in_stock)
values ('ACC-120', 'USB-C Dock', 'Accessories', 149, 35);

insert into catalog_items (sku, item_name, category, unit_price, in_stock)
values ('ACC-130', 'Webcam 1080p', 'Accessories', 59, 0);

insert into catalog_items (sku, item_name, category, unit_price, in_stock)
values ('FUR-100', 'Office Chair', 'Furniture', 249, 18);

insert into catalog_items (sku, item_name, category, unit_price, in_stock)
values ('FUR-110', 'Standing Desk', 'Furniture', 499, 6);

insert into catalog_items (sku, item_name, category, unit_price, in_stock)
values ('FUR-120', 'Desk Lamp', 'Furniture', 39, 70);

insert into catalog_items (sku, item_name, category, unit_price, in_stock)
values ('FUR-130', 'Filing Cabinet', 'Furniture', 179, 9);

commit;

select item_id, sku, item_name, category, unit_price, in_stock
from   catalog_items
order  by item_id;

Output:

Table CATALOG_ITEMS created.

Index CATALOG_ITEMS_CATEGORY_IX created.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

Commit complete.

   ITEM_ID SKU        ITEM_NAME              CATEGORY          UNIT_PRICE    IN_STOCK
__________ __________ ______________________ ______________ _____________ ___________
         1 CMP-100    Laptop 14 inch         Computers                899          25
         2 CMP-110    Laptop 16 inch         Computers               1299          12
         3 CMP-200    Desktop Tower          Computers               1099           8
         4 CMP-300    Monitor 27 inch        Computers                329          40
         5 ACC-100    Wireless Mouse         Accessories               25         150
         6 ACC-110    Mechanical Keyboard    Accessories               89          60
         7 ACC-120    USB-C Dock             Accessories              149          35
         8 ACC-130    Webcam 1080p           Accessories               59           0
         9 FUR-100    Office Chair           Furniture                249          18
        10 FUR-110    Standing Desk          Furniture                499           6
        11 FUR-120    Desk Lamp              Furniture                 39          70
        12 FUR-130    Filing Cabinet         Furniture                179           9

12 rows selected.
  • CATALOG_ITEMS_PK is the key that the API returns as item_id. The REST Data Source uses it later as its primary key.
  • CATALOG_ITEMS_SKU_UK keeps the SKU unique, and the check constraints allow only three categories, positive prices and a stock that is not negative.
  • IN_STOCK and UPDATED_ON get their values from DEFAULT ON NULL, so no trigger is needed.
  • CATALOG_ITEMS_CATEGORY_IX supports the category filter of the API.

The second table receives the copy of the API rows in Step 9. Its column names match the JSON attributes, and ITEM_ID is the primary key, so APEX can merge new data into existing rows:

Example:

create table catalog_items_local (
    item_id     number        constraint catalog_items_local_pk primary key,
    sku         varchar2(20)  not null,
    item_name   varchar2(100) not null,
    category    varchar2(30)  not null,
    unit_price  number(10,2),
    in_stock    number,
    updated_on  timestamp with time zone
);

create index catalog_items_local_cat_ix on catalog_items_local (category);

select count(*) as row_count from catalog_items_local;

Output:

Table CATALOG_ITEMS_LOCAL created.

Index CATALOG_ITEMS_LOCAL_CAT_IX created.

   ROW_COUNT
____________
           0

Step 2: Publish the Table as a REST API with ORDS

If you already have an API to show, skip this step. Otherwise, publish CATALOG_ITEMS with an ORDS module. The GET handler is a collection query with an optional category filter, and ORDS adds paging to it:

Example:

begin
    ords.enable_schema(
        p_enabled             => true,
        p_schema              => 'APXSOL',
        p_url_mapping_type    => 'BASE_PATH',
        p_url_mapping_pattern => 'apxsol',
        p_auto_rest_auth      => true);

    ords.define_module(
        p_module_name    => 'catalog',
        p_base_path      => '/catalog/',
        p_items_per_page => 25);

    ords.define_template(
        p_module_name => 'catalog',
        p_pattern     => 'items/');

    ords.define_handler(
        p_module_name    => 'catalog',
        p_pattern        => 'items/',
        p_method         => 'GET',
        p_source_type    => ords.source_type_collection_feed,
        p_items_per_page => 25,
        p_source         =>
'select item_id, sku, item_name, category, unit_price, in_stock, updated_on
from   catalog_items
where  :category is null or category = :category
order  by item_id');

    commit;
end;
/

select name, uri_prefix, status from user_ords_modules;

Output:

PL/SQL procedure successfully completed.

NAME       URI_PREFIX    STATUS
__________ _____________ ____________
catalog    /catalog/     PUBLISHED

When :category is empty, the handler returns all rows. When the URL has ?category=Computers, it returns only that category. The guide How to Create a REST Module, Template, and Handler with ORDS.DEFINE_MODULE explains these calls in detail.

On the host computer, the same API answers at localhost:8182. Call it to check the response:

Example:

curl "http://localhost:8182/ords/apxsol/catalog/items/?category=Computers"

Output:

{
  "items": [
    {
      "item_id": 1,
      "sku": "CMP-100",
      "item_name": "Laptop 14 inch",
      "category": "Computers",
      "unit_price": 899,
      "in_stock": 25,
      "updated_on": "2026-10-14T14:55:49Z"
    },
    {
      "item_id": 2,
      "sku": "CMP-110",
      "item_name": "Laptop 16 inch",
      "category": "Computers",
      "unit_price": 1299,
      "in_stock": 12,
      "updated_on": "2026-10-14T14:55:49Z"
    },
    {
      "item_id": 3,
      "sku": "CMP-200",
      "item_name": "Desktop Tower",
      "category": "Computers",
      "unit_price": 1099,
      "in_stock": 8,
      "updated_on": "2026-10-14T14:55:49Z"
    },
    {
      "item_id": 4,
      "sku": "CMP-300",
      "item_name": "Monitor 27 inch",
      "category": "Computers",
      "unit_price": 329,
      "in_stock": 40,
      "updated_on": "2026-10-14T14:55:49Z"
    }
  ],
  "hasMore": false,
  "limit": 25,
  "offset": 0,
  "count": 4,
  "links": [
    {
      "rel": "self",
      "href": "http://localhost:8182/ords/apxsol/catalog/items/?category=Computers"
    },
    {
      "rel": "describedby",
      "href": "http://localhost:8182/ords/apxsol/metadata-catalog/catalog/items/"
    },
    {
      "rel": "first",
      "href": "http://localhost:8182/ords/apxsol/catalog/items/?category=Computers"
    }
  ]
}

The rows are in the items array, and hasMore, limit and offset are the ORDS paging attributes. APEX understands this format when you pick the Oracle REST Data Services type in Step 4.

Step 3: Allow the Database to Call the API

A REST Data Source is called by the database, not by the browser. The URL must therefore be reachable from the database server, and the APEX schema needs a network ACL for that host and port.

In this example the database runs in a Docker container and ORDS runs on the host computer, so the URL uses host.docker.internal and port 8182, a plain HTTP listener of ORDS. Use the host and port of your own API.

In a first test, without an ACL, the Create REST Data Source wizard stopped with this error:

HTTP Failure dialog: The endpoint URL could not be invoked, ORA-24247: network access denied by access control list (ACL)
Without an ACL, the database is not allowed to open the connection.

Run this block as SYS or ADMIN. APEX_260100 is the APEX schema of APEX 26.1; use the schema of your APEX version:

Example:

begin
    dbms_network_acl_admin.append_host_ace(
        host       => 'host.docker.internal',
        lower_port => 8182,
        upper_port => 8182,
        ace        => xs$ace_type(
                          privilege_list => xs$name_list('http'),
                          principal_name => 'APEX_260100',
                          principal_type => xs_acl.ptype_db));
end;
/

select host, lower_port, upper_port, principal, privilege
from   dba_host_aces
where  principal = 'APEX_260100'
and    host = 'host.docker.internal';

Output:

PL/SQL procedure successfully completed.

HOST                   LOWER_PORT UPPER_PORT PRINCIPAL    PRIVILEGE
---------------------- ---------- ---------- ------------ ----------
host.docker.internal         8182       8182 APEX_260100  HTTP

1 row selected.

Step 4: Create the REST Data Source

In App Builder, open the application, go to Shared Components, and click REST Data Sources under Data Sources. Click Create:

REST Data Sources page with the Create button marked with a red box
The REST Data Sources list is empty at first.

On the Method screen, keep From scratch and click Next:

Create REST Data Source Method screen with From scratch selected and marked with a red box
Create the REST Data Source from scratch.

On the General screen, set REST Data Source Type to Oracle REST Data Services, Name to Catalog Items, and URL Endpoint to the URL of the API. Click Next:

General screen with REST Data Source Type Oracle REST Data Services, Name Catalog Items and URL Endpoint marked with red boxes
The type tells APEX how the service pages and filters its data.

APEX splits the URL into a Base URL and a Service URL Path. Keep Remote Server on - Create New - and click Next:

Remote Server screen with Remote Server Create New, Base URL and Service URL Path marked with a red box
The base URL becomes a remote server that other sources can share.

The API in this example is public, so leave Authentication Required off. Click Advanced to add the parameter before discovery:

Authentication screen with Authentication Required off and the Advanced button marked with red boxes
Advanced opens the Parameters screen.

Set Parameter Type to URL Query String and Parameter Name to category. Leave Value empty, so the discovery call returns all rows. Click Discover:

Parameters screen with Parameter Type URL Query String and Parameter Name category marked with a red box
The query string parameter becomes ?category= in the URL.

APEX calls the API and shows the rows it found:

REST Data Source Discovery Data tab showing the 12 catalog items
The Data tab shows what the API returned.

The Data Profile tab shows how the JSON becomes columns: the row selector is items, and each column has a data type and a selector. Click Create REST Data Source:

Data Profile tab with row selector items, seven columns, and the Create REST Data Source button marked with a red box
APEX found seven columns in the items array.

The new source appears in the list with its endpoint:

REST Data Sources list with Catalog Items and the message REST Data Source created
The REST Data Source is ready.

Step 5: Mark the Primary Key in the Data Profile

Discovery marked no column as the primary key. Synchronization in Step 9 needs one to merge rows. Click Catalog Items, then Edit Data Profile, and click the edit icon of the ITEM_ID column. Turn on Primary Key and click Apply Changes:

Data Profile Column ITEM_ID with the Primary Key switch turned on and marked with a red box
ITEM_ID identifies each row of the API.

Step 6: Create the Report Page

Click Create Page and choose Interactive Report on the Component tab:

Create a Page wizard with the Interactive Report tile marked with a red box
An Interactive Report gives users search, sort and filters for free.

Enter Page Number 300 and Name Catalog Items. Set Data Source to REST Source and REST Data Source to Catalog Items:

Create Interactive Report wizard with Page Number 300, Name Catalog Items, Data Source REST Source and REST Data Source Catalog Items marked with red boxes
The report reads the REST Data Source instead of a table.

Scroll down to Navigation. Turn off Use Breadcrumb, set Icon to fa-cloud-download, and click Create Page:

Navigation section with Use Breadcrumb off and Icon fa-cloud-download marked with red boxes
The page gets its own navigation menu entry.

The wizard creates the report columns from the data profile. It also adds a node for the category parameter under Parameters in the region, with no value yet.

Step 7: Add the Category Filter

In Page Designer, right-click the Catalog Items region and add a page item in the Region Body slot. Set Name to P300_CATEGORY, Type to Select List and Label to Category.

Under List of Values, set Type to Static Values and enter three values, Computers, Accessories and Furniture, each with the same display and return value. Set Null Display Value to - All Categories -:

Page Designer for P300_CATEGORY with Name, Type, Label, List of Values Type, Static Values and Null Display Value marked with red boxes
The select list sits in the region body above the report.

Select the Catalog Items region. Under Source, set Page Items to Submit to P300_CATEGORY, so the value is in session state when the region refreshes:

Catalog Items region with Location REST Source, REST Source Catalog Items and Page Items to Submit P300_CATEGORY marked with a red box
The region submits the item before it calls the API.

Expand Parameters under the region and select category. Under Value, set Type to Item and Item to P300_CATEGORY:

Parameter category with Value Type Item and Item P300_CATEGORY marked with a red box
The item value goes into the URL as ?category=.

Now add a dynamic action that refreshes the report when the category changes. On the Dynamic Actions tab, create a dynamic action named Refresh on Category Change with Event Change, Selection Type Item(s) and Item(s) P300_CATEGORY:

Dynamic action Refresh on Category Change with Event Change, Selection Type Item(s) and Item(s) P300_CATEGORY marked with red boxes
The dynamic action fires when the user picks a category.

Set its True action to Refresh with Selection Type Region and Region Catalog Items. Save the page:

True action Refresh with Selection Type Region and Region Catalog Items marked with red boxes
The refresh calls the API again with the new parameter value.

Step 8: Run the Report

Run page 300. The report shows the 12 rows of the API with the usual Interactive Report search bar and Actions menu:

Catalog Items report showing 12 rows from the REST API with the Category select list above it
All rows come from the REST API.

Choose Furniture in the Category list. The report refreshes and shows four rows:

Catalog Items report filtered to Furniture with the Category select list marked with a red box
The filter runs inside the API, not in the browser.

Step 9: Copy the Rows into a Local Table

Every refresh of the report calls the API. For data that changes slowly, or for reports that join the data with local tables, keep a copy with REST Source Synchronization.

Open the Catalog Items REST Data Source and click Manage Synchronization on the right. Set Synchronize to Existing Table, choose CATALOG_ITEMS_LOCAL as the Table Name, and click Save:

REST Synchronization page with Synchronize to Existing Table, Table Name CATALOG_ITEMS_LOCAL and the Save button marked with red boxes
APEX can also create a new table for you.

Set Synchronization Type to Merge and Synchronization Schedule to FREQ=DAILY;BYHOUR=6;BYMINUTE=0. Click Save and Run:

REST Synchronization page with Synchronization Type Merge, Schedule FREQ=DAILY;BYHOUR=6;BYMINUTE=0 and the Save and Run button marked with red boxes
Merge updates existing rows and inserts new ones.

The page warns that CATALOG_ITEMS_LOCAL is different from the data profile. The profile uses VARCHAR2(4000) and plain NUMBER, while the table has smaller sizes. APEX still copies every column that exists in both, and Show SQL offers ALTER TABLE statements if you want them to match.

The Log section at the bottom of the page shows the first run: one HTTP request and 12 rows:

Synchronization log with status Success, 12 rows, 1 request and 2KB
The first synchronization copied all 12 rows.

Check the local table in SQL:

Example:

select item_id, sku, item_name, category, unit_price, in_stock
from   catalog_items_local
order  by item_id;

Output:

   ITEM_ID SKU        ITEM_NAME              CATEGORY          UNIT_PRICE    IN_STOCK
__________ __________ ______________________ ______________ _____________ ___________
         1 CMP-100    Laptop 14 inch         Computers                899          25
         2 CMP-110    Laptop 16 inch         Computers               1299          12
         3 CMP-200    Desktop Tower          Computers               1099           8
         4 CMP-300    Monitor 27 inch        Computers                329          40
         5 ACC-100    Wireless Mouse         Accessories               25         150
         6 ACC-110    Mechanical Keyboard    Accessories               89          60
         7 ACC-120    USB-C Dock             Accessories              149          35
         8 ACC-130    Webcam 1080p           Accessories               59           0
         9 FUR-100    Office Chair           Furniture                249          18
        10 FUR-110    Standing Desk          Furniture                499           6
        11 FUR-120    Desk Lamp              Furniture                 39          70
        12 FUR-130    Filing Cabinet         Furniture                179           9

12 rows selected.

Test the Report and the Synchronization

Change the source data: a new price for ACC-100, a new item ACC-140, and FUR-130 removed:

Example:

update catalog_items
set    unit_price = 22, updated_on = sysdate
where  sku = 'ACC-100';

insert into catalog_items (sku, item_name, category, unit_price, in_stock)
values ('ACC-140', 'Laptop Stand', 'Accessories', 45, 30);

delete from catalog_items
where  sku = 'FUR-130';

commit;

Output:

1 row updated.

1 row inserted.

1 row deleted.

Commit complete.

Reload page 300. The report reads the API, so it shows the new price and the new item right away, and FUR-130 is gone:

Catalog Items report with the changed ACC-100 row and the new ACC-140 row marked with red boxes
The report always shows the current API data.

The local table has not changed yet, because the next scheduled run is tomorrow morning:

Example:

select item_id, sku, item_name, unit_price, in_stock
from   catalog_items_local
where  category = 'Accessories' or item_id >= 12
order  by item_id;

Output:

   ITEM_ID SKU        ITEM_NAME                 UNIT_PRICE    IN_STOCK
__________ __________ ______________________ _____________ ___________
         5 ACC-100    Wireless Mouse                    25         150
         6 ACC-110    Mechanical Keyboard               89          60
         7 ACC-120    USB-C Dock                       149          35
         8 ACC-130    Webcam 1080p                      59           0
        12 FUR-130    Filing Cabinet                   179           9

You do not have to wait for the schedule. APEX_REST_SOURCE_SYNC.SYNCHRONIZE_DATA runs the synchronization from PL/SQL. Outside an APEX page it needs an APEX session, which APEX_SESSION.CREATE_SESSION provides:

Example:

begin
    apex_session.create_session(
        p_app_id   => 400,
        p_page_id  => 1,
        p_username => 'DEMO_DEV');

    apex_rest_source_sync.synchronize_data(
        p_module_static_id => 'catalog-items',
        p_run_in_background => false);
end;
/

select item_id, sku, item_name, unit_price, in_stock
from   catalog_items_local
where  category = 'Accessories' or item_id >= 12
order  by item_id;

Output:

PL/SQL procedure successfully completed.

   ITEM_ID SKU        ITEM_NAME                 UNIT_PRICE    IN_STOCK
__________ __________ ______________________ _____________ ___________
         5 ACC-100    Wireless Mouse                    22         150
         6 ACC-110    Mechanical Keyboard               89          60
         7 ACC-120    USB-C Dock                       149          35
         8 ACC-130    Webcam 1080p                      59           0
        12 FUR-130    Filing Cabinet                   179           9
        41 ACC-140    Laptop Stand                      45          30

6 rows selected.

Merge updated the price of ACC-100 and inserted ACC-140. FUR-130 is still in the local table, although the API no longer returns it. In this test, Merge did not remove rows that disappeared from the API, so plan for that if your local copy must drop deleted rows.

To see what APEX sent to the API, query APEX_WEBSERVICE_LOG. The report calls carry the category parameter and a limit for paging:

Example:

select to_char(request_date, 'HH24:MI:SS') as called_at,
       http_method,
       status_code,
       substr(url, instr(url, '/catalog')) as url_path
from   apex_webservice_log
where  url like '%/catalog/items/%'
and    http_method = 'GET'
and    request_date > sysdate - interval '30' minute
order  by request_date desc
fetch first 5 rows only;

Output:

CALLED_AT    HTTP_METHOD       STATUS_CODE URL_PATH
____________ ______________ ______________ ______________________________________________
15:27:51     GET                       200 /catalog/items/?category=
15:27:37     GET                       200 /catalog/items/?category=&limit=51
15:24:37     GET                       200 /catalog/items/?category=&limit=51
15:24:34     GET                       200 /catalog/items/?category=Furniture&limit=51
15:24:32     GET                       200 /catalog/items/?category=&limit=51

The limit=51 comes from the paging of the report. The call without a limit, at 15:27:51, is the APEX_REST_SOURCE_SYNC run above.

Finally, a negative test: stop the API and reload page 300. The report cannot read its data and shows the database error instead of rows:

Catalog Items report showing ORA-29273: HTTP request failed, marked with a red box
A live REST report depends on the API being up.

The local copy is still there while the API is down:

Example:

select count(*) as local_rows
from   catalog_items_local;

Output:

   LOCAL_ROWS
_____________
           13

Things to Know

  • The database makes the HTTP call. A URL that works in your browser can still fail in APEX if the database server cannot reach it, and every host needs a network ACL for the APEX schema (ORA-24247 without it).
  • With the Oracle REST Data Services type, APEX found the items array by itself and added the ORDS limit parameter to the report calls.
  • A parameter mapped to a page item only works if the item is in Page Items to Submit, or the region refresh sends the old value.
  • Discovery does not mark a primary key. Set it in the data profile before you use Merge synchronization.
  • Merge kept a row that was deleted in the source. Test the behavior with your own API before you rely on the local copy for deleted rows.
  • After synchronization is set up, the region properties show a REST Synchronization group with a Use Local Table switch. This guide left it off, so the report and its category filter always call the API.

Related Guides

Conclusion

To show data from a REST API in a report in Oracle APEX, give the database a network ACL for the API host, describe the service once as a REST Data Source, and use it as the source of an Interactive Report. A page item mapped to a query string parameter sends the user's filter to the API.

When the data must stay available while the API is slow or down, add REST Source Synchronization. A Merge into a local table with a primary key keeps a copy on a schedule, and APEX_REST_SOURCE_SYNC runs it on demand.

Vinish Kapoor
Vinish Kapoor

An Oracle ACE, author of four books on Oracle APEX, SQL and PL/SQL, and Oracle Forms, and a software developer building Oracle database applications since 2001.

guest

0 Comments
Oldest
Newest Most Voted