Skip to main content

Command Palette

Search for a command to run...

Opening Your Oracle APEX Application to the Outside World with RESTful services

Fixing HTTP 555 — ORDS-25001 / ORA-06550 / PLS-00306 errors

Updated
12 min read
Opening Your Oracle APEX Application to the Outside World with RESTful services
V
I am an experienced Oracle APEX Developer with more than 4 years of proven expertise in designing and delivering high-quality web applications across diverse industries—including insurance, healthcare, utilities, and analytics. With a solid track record of building scalable and secure applications,

I've been working with Oracle APEX for a while now. I've built REST services before with mostly GET endpoints to fetch data, feed a dashboard somewhere, or let another team query something without giving them direct DB access. That part is straightforward enough. But recently I had a requirement that was a bit different. An insurance client needed their existing external application to connect to our APEX system and trigger a core business operation it was not just read data, but actually execute logic. Issue a Quotation and Commit a transaction.

The first thought is usually "okay, we'll set up a separate connection, expose a DB endpoint, maybe write a wrapper." But that means duplicating business logic, maintaining two entry points, and creating a sync problem. We already had everything inside APEX. the packages, the procedures, the validation logic. so why rewrite?

That's when I went deeper into ORDS REST Services than I ever had before. And it did not go smoothly. This post is mostly about the mistakes I made, the errors I hit, and what finally worked. If you're reading this as someone who already knows APEX and ORDS, this is not a "here's how to create a REST module" walkthrough. I'm going to skip that part. What I want to talk about is the stuff that breaks in production and doesn't have a clean answer one google away.


Why Even Do This in APEX?

Worth saying quickly that the reason to do this inside ORDS rather than building a separate middleware is that your business logic stays in one place. The PL/SQL packages and procedures you've already written, tested, and are running in production, those are still the source of truth. You're just reusing them. No duplication, no separate deployment, no drift between two implementations of the same logic.

ORDS gives you that door for free if you're already on APEX. You define a module, a template, a handler, write some PL/SQL, and you have an HTTP endpoint. The hard part, as I found out, is not the setup. It's the binding.


The Handler Setup - The Basics

Assuiming you've created your REST module and template. Your POST handler source type is PL/SQL. The goal is simple receive a JSON body, parse it, call a stored procedure, return a response.

A basic handler looks like this:

DECLARE
    v_json   APEX_JSON.T_VALUES;
    v_name   VARCHAR2(512);
BEGIN
    APEX_JSON.PARSE(v_json, :body_text);
    v_name := APEX_JSON.GET_VARCHAR2(p_values => v_json, p_path => 'customer.full_name');

    APEX_JSON.OPEN_OBJECT;
        APEX_JSON.WRITE('success', TRUE);
        APEX_JSON.WRITE('name', v_name);
    APEX_JSON.CLOSE_OBJECT;
END;

Simple enough. But notice :body_text. That is where the first rabbit hole starts.


Mistake #1 - Using :body Instead of :body_text

My first version used :body as the bind variable for the request body. I declared it as a CLOB:

DECLARE
    v_body CLOB;
    v_json APEX_JSON.T_VALUES;
BEGIN
    v_body := :body;
    APEX_JSON.PARSE(v_json, v_body);
    ...

This looked reasonable. The APEX documentation does mention :body as an implicit bind. But what it doesn't make obvious is that :body gives you a BLOB raw bytes. Not a CLOB. Assigning it to a CLOB variable doesn't throw a compile error, it just silently causes problems at runtime.

The error I got was a generic 555 from ORDS:

User Defined Resource Error
HTTP Status Code: 555
Error Code: ORDS-25001

That's ORDS telling you something went wrong in your PL/SQL, but it's vague. Could be anything. I assumed it was the procedure call failing. Spent time looking in the wrong place.

To test whether the body binding itself was working, I stripped everything down to the simplest possible handler:

BEGIN
    APEX_JSON.OPEN_OBJECT;
        APEX_JSON.WRITE('received', UTL_RAW.CAST_TO_VARCHAR2(:body));
    APEX_JSON.CLOSE_OBJECT;
END;

This returned the body correctly and I could see the JSON I sent in the response. So the binding itself wasn't completely broken, it was the CLOB assignment and the subsequent APEX_JSON.PARSE call that was failing.

The fix here isn't to cast it. it's to not use :body at all for JSON payloads. Use :body_text instead.


Mistake #2 - Trying to Configure :body as a Parameter

After I saw the BLOB issue, I thought the right move was to go into the handler's Parameters tab and manually define body as a parameter with the correct data type. I added it there, set the source type, tried to set the data type to BLOB.

ORDS won't let you. The error was clear enough once I read it properly:

Invalid value provided for parameter body.
class java.lang.String is not a supported primitive datatype.

The Parameters tab in ORDS is for binding URI template parameters, query string parameters, and HTTP headers. things like :id in /policies/{id}, or a custom header value. It is not the place to configure the request body. The body binding is handled implicitly by ORDS itself, and trying to manually define it in the parameter tab just breaks things.

This is one of those things that feels like it should work because the UI lets you try it, but it doesn't.


The Actual Solution - :body_text

ORDS provides two implicit body bind variables that require zero configuration in the Parameters tab:

Bind variable Type Use for
:body_text VARCHAR2 / CLOB JSON, XML, plain text
:body BLOB Binary data, file uploads

For any JSON payload, :body_text is what you want. You don't define it anywhere. You don't add it to the Parameters tab. You just use it directly in your PL/SQL:

DECLARE
    v_json APEX_JSON.T_VALUES;
BEGIN
    APEX_JSON.PARSE(v_json, :body_text);
    ...

That's it. Once I made that switch, the parsing worked immediately.


The Full Working Handler Pattern

Here's the full structure of a POST handler that receives a JSON body, calls a stored procedure, and returns a structured response. The procedure in this example takes customer and transaction details as parameters and returns output values that get included in the response.

DECLARE
    -- Output variables from the procedure
    v_record_id      NUMBER;
    v_reference_no   VARCHAR2(64);
    v_amount         NUMBER;

    -- JSON parser
    v_json           APEX_JSON.T_VALUES;

    -- Input variables
    v_agency_id      NUMBER;
    v_issued_by      NUMBER;
    v_product_id     NUMBER;
    v_full_name      VARCHAR2(512);
    v_id_type        VARCHAR2(16);
    v_id_number      VARCHAR2(128);
    v_mobile         VARCHAR2(64);
    v_email          VARCHAR2(255);
    v_address        VARCHAR2(4000);
    v_cover_from_str VARCHAR2(20);
    v_cover_from     DATE;
BEGIN
    -- Parse the request body — :body_text, not :body
    APEX_JSON.PARSE(v_json, :body_text);

    -- Extract values from JSON
    v_agency_id      := APEX_JSON.GET_NUMBER(  p_values => v_json, p_path => 'agency_id');
    v_issued_by      := APEX_JSON.GET_NUMBER(  p_values => v_json, p_path => 'issued_by');
    v_product_id     := APEX_JSON.GET_NUMBER(  p_values => v_json, p_path => 'product_id');
    v_full_name      := APEX_JSON.GET_VARCHAR2(p_values => v_json, p_path => 'customer.full_name');
    v_id_type        := APEX_JSON.GET_VARCHAR2(p_values => v_json, p_path => 'customer.id_type');
    v_id_number      := APEX_JSON.GET_VARCHAR2(p_values => v_json, p_path => 'customer.id_number');
    v_mobile         := APEX_JSON.GET_VARCHAR2(p_values => v_json, p_path => 'customer.mobile');
    v_email          := APEX_JSON.GET_VARCHAR2(p_values => v_json, p_path => 'customer.email');
    v_address        := APEX_JSON.GET_VARCHAR2(p_values => v_json, p_path => 'customer.address');

    -- Parse date separately — GET_VARCHAR2 first, then TO_DATE
    v_cover_from_str := APEX_JSON.GET_VARCHAR2(p_values => v_json, p_path => 'cover_from');
    v_cover_from     := TO_DATE(v_cover_from_str, 'YYYY-MM-DD');

    -- Call your existing business logic procedure
    YOUR_BUSINESS_PROCEDURE(
        p_agency_id    => v_agency_id,
        p_issued_by    => v_issued_by,
        p_product_id   => v_product_id,
        p_full_name    => v_full_name,
        p_id_type      => v_id_type,
        p_id_number    => v_id_number,
        p_mobile       => v_mobile,
        p_email        => v_email,
        p_address      => v_address,
        p_cover_from   => v_cover_from,
        p_record_id    => v_record_id,      -- OUT
        p_reference_no => v_reference_no,   -- OUT
        p_amount       => v_amount          -- OUT
    );

    -- Build the JSON response
    APEX_JSON.OPEN_OBJECT;
        APEX_JSON.WRITE('success', TRUE);
        APEX_JSON.OPEN_OBJECT('data');
            APEX_JSON.WRITE('record_id',    v_record_id);
            APEX_JSON.WRITE('reference_no', v_reference_no);
            APEX_JSON.WRITE('amount',       v_amount);
            APEX_JSON.WRITE('status',       'PENDING');
        APEX_JSON.CLOSE_OBJECT;
    APEX_JSON.CLOSE_OBJECT;

EXCEPTION
    WHEN OTHERS THEN
        APEX_JSON.OPEN_OBJECT;
            APEX_JSON.WRITE('success', FALSE);
            APEX_JSON.OPEN_OBJECT('error');
                APEX_JSON.WRITE('code',      'FAILED');
                APEX_JSON.WRITE('message',   SQLERRM);
                APEX_JSON.WRITE('backtrace', DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);
                APEX_JSON.WRITE('callstack', DBMS_UTILITY.FORMAT_CALL_STACK);
            APEX_JSON.CLOSE_OBJECT;
        APEX_JSON.CLOSE_OBJECT;
END;

A few things worth noting here:

Date parsing - APEX_JSON doesn't have a GET_DATE function. You have to get the value as a string and convert it yourself with TO_DATE. If you try to pull a date directly you'll get a type mismatch. This is minor but it catches you off guard.

The EXCEPTION block - Always include DBMS_UTILITY.FORMAT_ERROR_BACKTRACE and FORMAT_CALL_STACK in your error response during development. SQLERRM tells you what went wrong. The backtrace tells you exactly which line inside your called procedure threw the exception. Without it you're guessing.

Nested JSON - APEX_JSON.GET_VARCHAR2 handles dot notation for nested paths cleanly. 'customer.full_name' reaches into {"customer": {"full_name": "..."}} without any extra parsing. This works for objects inside objects too.


Debugging the 555 - A Strategy

The ORDS 555 error is frustrating because it can mean several different things:

  1. Your PL/SQL itself threw an unhandled exception

  2. ORDS couldn't bind the parameters before your PL/SQL even ran

  3. The response output was malformed

The way to isolate which layer is failing is to strip down the handler progressively. Start with the absolute minimum:

BEGIN
    APEX_JSON.OPEN_OBJECT;
        APEX_JSON.WRITE('alive', TRUE);
    APEX_JSON.CLOSE_OBJECT;
END;

If this returns a 555, the problem is in the ORDS handler configuration itself wrong method, authentication issue, or something at the module level. It has nothing to do with your code.

If this works, add the body parsing:

DECLARE
    v_json APEX_JSON.T_VALUES;
BEGIN
    APEX_JSON.PARSE(v_json, :body_text);
    APEX_JSON.OPEN_OBJECT;
        APEX_JSON.WRITE('agency_id', APEX_JSON.GET_NUMBER(p_values => v_json, p_path => 'agency_id'));
    APEX_JSON.CLOSE_OBJECT;
EXCEPTION
    WHEN OTHERS THEN
        APEX_JSON.OPEN_OBJECT;
            APEX_JSON.WRITE('error', SQLERRM);
            APEX_JSON.WRITE('backtrace', DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);
        APEX_JSON.CLOSE_OBJECT;
END;

If this works, the parsing is fine and the issue is inside your procedure. Then add the procedure call and let the exception handler report what's failing inside it.

Each step either passes cleanly or tells you exactly which layer broke. Don't try to debug everything at once.


Extra Note - What About the External Application and The CORS Problem

Once the endpoint was working perfectly in Postman, I built a quick proof of concept frontend to show the client. a simple HTML form that calls the API Using AI and displays the result. And immediately it errored.

Failed to fetch

No useful error. Just that. The browser's developer console had more detail a CORS error. The APEX server wasn't sending back the Access-Control-Allow-Origin header, so the browser blocked the response entirely.

This is worth understanding clearly: CORS is a browser restriction, not an API restriction. Postman doesn't care about CORS because Postman is not a browser. Your APEX API is responding correctly. the browser is just refusing to hand the response to your JavaScript code because the server didn't explicitly permit it.

There are two ways to handle this:

Option 1 - Configure CORS in ORDS (Quick but Not recommended)

If you control the ORDS configuration, you can enable CORS headers for your REST module. In ORDS settings or in the REST module definition, there are options to set allowed origins.

Option 2 - A server-side proxy

If you can't touch the ORDS CORS config, or you're building an external app that needs to call the API from a browser, a thin proxy server solves it cleanly. The browser calls your proxy (same origin, no CORS issue), the proxy calls the APEX API server-side (CORS doesn't apply to server-to-server calls), and returns the response.

A minimal Node.js example using Express:

const express = require('express');
const fetch = require('node-fetch'); // use node-fetch@2 for CommonJS require()
const app = express();

app.use(express.json());
app.use(express.static(__dirname)); // serve your frontend HTML

const APEX_BASE = 'https://your-ords-host/ords/your_workspace/api/v1';

// Proxy for GET endpoints
app.get('/proxy/products', async (req, res) => {
    const r = await fetch(`${APEX_BASE}/products`);
    res.json(await r.json());
});

// Proxy for POST — forward the request body
app.post('/proxy/issue', async (req, res) => {
    const r = await fetch(`${APEX_BASE}/your-post-endpoint`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(req.body)
    });
    res.json(await r.json());
});

app.listen(3000);

The browser talks to localhost:3000/proxy/.... Node makes the actual call to APEX. No CORS issue anywhere in the chain.

One practical note - if you're using require() style imports in Node (CommonJS), install node-fetch@2 specifically. Version 3 and above is ESM only and will throw ERR_REQUIRE_ESM if you try to require it. That error has nothing to do with your API and will waste your time if you don't know it upfront.


Summary - The Things That Actually Matter

A lot of tutorials on ORDS REST services cover the happy path, create a module, write a handler, it works. What they skip is the binding behavior and what happens when you push beyond simple GETs. Here's what I'd want to know before starting:

:body_text for JSON, :body for binary. This is the single most important thing. Don't use :body for a JSON payload. Don't try to define these in the Parameters tab. they're implicit and ORDS handles them automatically.

The Parameters tab is not for body binding. It's for URI parameters and headers. Adding body as a parameter there will either do nothing or actively break things depending on how you configure it.

Always include FORMAT_ERROR_BACKTRACE in your exception handler. SQLERRM gives you the error message. The backtrace gives you the line number inside whichever procedure actually threw. Without it, debugging a procedure call that fails is just guessing.

555 means different things. Strip your handler down to the minimum and build up layer by layer to find where it breaks. Don't assume the error is where you think it is.

CORS only matters in browsers. If it works in Postman but fails in a browser, it's CORS. A server-side proxy is the fastest fix if you can't configure CORS on the ORDS side.

Test dates carefully. APEX_JSON has no date getter. Extract as string, convert with TO_DATE. Use ISO format (YYYY-MM-DD) on the client side and convert explicitly on the PL/SQL side. Don't rely on implicit date conversion. it depends on session NLS settings and will fail inconsistently across environments.


That's about everything I hit on this one. The core win here is that you don't need a separate application server or a duplicate implementation of your business logic to open your APEX application to external integrations. ORDS gives you that capability directly. You just have to know where the traps are.

Hopefully this saves someone a few hours of staring at a 555 error wondering what went wrong.