Skip to main content

LinkedIn from Oracle APEX

Posting to LinkedIn from Oracle APEX: A Complete Beginner's Guide

Free oracleapex.com tier · personal LinkedIn profile posting · step-by-step with every real error included

If you've ever wanted a button in your Oracle APEX app that posts directly to your personal LinkedIn feed — no Zapier, no third-party middleman, just your own APEX app talking straight to LinkedIn's API — this guide walks through the whole thing, start to finish, including every mistake made along the way so you don't have to repeat them.

By the end, you'll have:

  • A LinkedIn Developer App configured for personal posting
  • Two APEX pages: one to connect/authorize, one that handles LinkedIn's callback and does the posting
  • A working "Post to LinkedIn" button that publishes text posts to your own profile

This is written for a free oracleapex.com workspace, but the same steps work on any hosted or on-premises APEX instance — just adjust the domain.

The Big Picture — How It All Connects

Before touching any code, it helps to see the shape of the whole thing. This is a standard OAuth2 Authorization Code flow — the same pattern used by "Sign in with Google" or "Sign in with Facebook." The difference here is we're using it to get permission to post on your behalf, not to log you in.

Page 1 — Connect ButtonBuilds a LinkedIn authorization URL and redirects the browser
LinkedIn Login + "Allow" ScreenYou approve access to your profile
Page 2 — Callback PageExchanges the one-time code for an access token, then looks up your member ID
Post to LinkedIn ButtonCalls LinkedIn's Posts API using the stored access token
Your Post Appears on LinkedIn

Part 1 — Create Your LinkedIn Developer App

1

Go to linkedin.com/developers/apps and click Create app.

2

Fill in the app name and logo.

3

The "LinkedIn Page" field trips up most beginners

LinkedIn requires every developer app to be linked to a Company Page, even if you only want to post to your own personal profile. You don't need a real business page:

  • Start typing into the field: Default Company Page for Individual Developer
  • Select it from the autocomplete dropdown — a real, LinkedIn-provided placeholder made exactly for this situation.
4

Add a privacy policy URL — any generic CCPA/GDPR-compliant link works, since you're only handling your own data.

5

Click Create app.

Add the Products You Need

Go to the Products tab and add:

  • Sign In with LinkedIn using OpenID Connect
  • Share on LinkedIn
Good to know

Both products are self-serve — they activate instantly, no LinkedIn review team involved. That manual review only applies if you need w_organization_social for a real Company Page, which you don't for personal posting. Just confirm both show status "Added" before moving on.

Get Your Client ID and Client Secret

Go to the Auth tab. Copy the Client ID and Client Secret somewhere safe — you'll need them shortly. Leave this tab open; you'll come back to register your redirect URL.

Part 2 — Create Settings Tables in APEX

In SQL Workshop → SQL Commands, run:

SQL · settings tableCREATE TABLE li_oauth_settings (
  id            NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  client_id     VARCHAR2(200),
  client_secret VARCHAR2(200),
  access_token  VARCHAR2(4000),
  refresh_token VARCHAR2(4000),
  expires_at    TIMESTAMP,
  person_urn    VARCHAR2(200)
);

INSERT INTO li_oauth_settings (client_id, client_secret)
VALUES ('PASTE_YOUR_CLIENT_ID', 'PASTE_YOUR_CLIENT_SECRET');

COMMIT;
Security note

This stores your secret in a plain database table rather than an encrypted credential store. For a personal, single-user tool this is a reasonable tradeoff — just never export this table's data when sharing your app.

A second table is genuinely useful too — a simple debug log that will save you hours later:

SQL · debug log tableCREATE TABLE li_debug_log (
  id       NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  log_time TIMESTAMP DEFAULT SYSTIMESTAMP,
  tag      VARCHAR2(50),
  message  CLOB
);

Part 3 — Build Page 1: The "Connect" Page

This page's only job: build LinkedIn's authorization URL and redirect the browser there.

1

Create a new blank page called "LinkedIn Connect."

2

Add a hidden page item: P1_REDIRECT_URL

3

Add a button: BTN_CONNECT

4

Add a Dynamic Action → Execute Server-Side Code on the button:

PL/SQL · build the LinkedIn auth URLDECLARE
  l_redirect_uri VARCHAR2(500) := 'https://oracleapex.com/ords/r/YOUR_WORKSPACE/YOUR_APP_ALIAS/page-2';
  l_client_id    VARCHAR2(200);
  l_url          VARCHAR2(1000);
BEGIN
  SELECT client_id INTO l_client_id FROM li_oauth_settings WHERE id = 1;

  l_url := 'https://www.linkedin.com/oauth/v2/authorization'
         || '?response_type=code'
         || '&client_id=' || l_client_id
         || '&redirect_uri=' || APEX_UTIL.URL_ENCODE(l_redirect_uri)
         || '&scope=' || APEX_UTIL.URL_ENCODE('openid profile w_member_social')
         || '&state=xyz123';

  :P1_REDIRECT_URL := l_url;
END;
5

Follow with a "Redirect to URL" True Action pointing at &P1_REDIRECT_URL. — this actually sends the browser to LinkedIn.

Getting the Redirect URI Exactly Right (the biggest time-sink)

The l_redirect_uri value must be the exact, real URL of your Page 2, registered character-for-character in LinkedIn's Auth tab. Run this to get it:

SQL · find your real page URLSELECT apex_page.get_url(p_application => YOUR_APP_ID, p_page => 2) FROM dual;
Two corrections before using this

1. Drop everything from ?session= onward — that ID is tied to your current login and breaks the moment your session changes.

2. Prepend your actual domain. On a free workspace this is https://oracleapex.comnot https://apex.oracle.com, which is a completely different, Oracle-hosted domain. Mixing these up produces LinkedIn's "redirect_uri does not match the registered value" error.

The final, correct value should look like:

https://oracleapex.com/ords/r/pothiarun/linkedin-integration-application/page-2

Paste this exact string into LinkedIn's Auth tab → Authorized redirect URLs for your app, and click Update.

Part 4 — Build Page 2: The Callback Page

This is where LinkedIn sends the browser back after you click "Allow." It needs to read the code, exchange it for a token, look up your member ID, and give you a textarea + button to post.

Create Two Page Items

  • CODE (Hidden) — name must match exactly, since APEX auto-maps query string parameters to items with matching names.
  • STATE (Hidden) — same reasoning.

Make the Page Public

Critical, easy to miss

LinkedIn's redirect is an anonymous browser request with no logged-in APEX session. If Page 2 requires authentication, APEX intercepts it and forces a login screen — and the code parameter gets lost in that bounce.

Page 2 → page-level node in Page Designer → Security section → set Authentication / "Page is Public" to Yes.

Add the Token-Exchange Process (Pre-Rendering)

PL/SQL · Page 2, Pre-Rendering, sequence 10DECLARE
  l_resp          CLOB;
  l_client_id     VARCHAR2(200);
  l_client_secret VARCHAR2(200);
  l_redirect_uri  VARCHAR2(500) := 'https://oracleapex.com/ords/r/YOUR_WORKSPACE/YOUR_APP_ALIAS/page-2';
BEGIN
  INSERT INTO li_debug_log (tag, message) VALUES ('A_START', 'CODE=' || :CODE);
  COMMIT;

  IF :CODE IS NOT NULL AND LENGTH(:CODE) > 0 THEN
    SELECT client_id, client_secret INTO l_client_id, l_client_secret
      FROM li_oauth_settings WHERE id = 1;

    APEX_WEB_SERVICE.g_request_headers.delete;
    APEX_WEB_SERVICE.g_request_headers(1).name  := 'Content-Type';
    APEX_WEB_SERVICE.g_request_headers(1).value := 'application/x-www-form-urlencoded';

    l_resp := APEX_WEB_SERVICE.make_rest_request(
      p_url         => 'https://www.linkedin.com/oauth/v2/accessToken',
      p_http_method => 'POST',
      p_body        => 'grant_type=authorization_code'
                      || '&code=' || APEX_UTIL.URL_ENCODE(:CODE)
                      || '&client_id=' || APEX_UTIL.URL_ENCODE(l_client_id)
                      || '&client_secret=' || APEX_UTIL.URL_ENCODE(l_client_secret)
                      || '&redirect_uri=' || APEX_UTIL.URL_ENCODE(l_redirect_uri)
    );

    INSERT INTO li_debug_log (tag, message) VALUES ('B_RESPONSE', l_resp);
    COMMIT;

    UPDATE li_oauth_settings
       SET access_token  = JSON_VALUE(l_resp, '$.access_token'),
           refresh_token = JSON_VALUE(l_resp, '$.refresh_token' NULL ON ERROR),
           expires_at    = SYSTIMESTAMP + NUMTODSINTERVAL(JSON_VALUE(l_resp,'$.expires_in'),'SECOND')
     WHERE id = 1 AND JSON_VALUE(l_resp, '$.access_token') IS NOT NULL;
    COMMIT;

    :CODE := NULL;  -- prevent this same code being reused on a later page load
  ELSE
    INSERT INTO li_debug_log (tag, message) VALUES ('A_SKIP', 'no code present');
    COMMIT;
  END IF;
EXCEPTION
  WHEN OTHERS THEN
    INSERT INTO li_debug_log (tag, message) VALUES ('C_EXCEPTION', SQLERRM);
    COMMIT;
END;
Two details baked in here, both learned the hard way

1. Encode every field — including client_id and client_secret, not just code and redirect_uri. An un-encoded special character in the secret corrupts the form body and LinkedIn silently rejects it with invalid_client, even though the stored secret is completely correct. This one cost the most debugging time in the whole project.

2. Clear :CODE after use — LinkedIn's authorization codes are single-use. Without nulling it, a stale code gets resubmitted on the next page reload, LinkedIn rejects it, and without a guard on the WHERE clause that failed response can silently overwrite a previously-good token with NULL.

Add the "Fetch My Member ID" Process (runs after the one above)

PL/SQL · Page 2, Pre-Rendering, sequence 20DECLARE
  l_resp  CLOB;
  l_token VARCHAR2(4000);
BEGIN
  SELECT access_token INTO l_token FROM li_oauth_settings WHERE id = 1;

  IF l_token IS NOT NULL THEN
    APEX_WEB_SERVICE.g_request_headers.delete;
    APEX_WEB_SERVICE.g_request_headers(1).name  := 'Authorization';
    APEX_WEB_SERVICE.g_request_headers(1).value := 'Bearer ' || l_token;

    l_resp := APEX_WEB_SERVICE.make_rest_request(
      p_url         => 'https://api.linkedin.com/v2/userinfo',
      p_http_method => 'GET'
    );

    INSERT INTO li_debug_log (tag, message) VALUES ('D_USERINFO', l_resp);
    COMMIT;

    UPDATE li_oauth_settings
       SET person_urn = 'urn:li:person:' || JSON_VALUE(l_resp, '$.sub')
     WHERE id = 1;
    COMMIT;
  END IF;
END;

Add the posting UI to Page 2 as well: a textarea item P2_COMMENT and a button BTN_POST.

Part 5 — The Reusable Posting Procedure

Create this once, in SQL Workshop:

PL/SQL · post_to_linkedin procedureCREATE OR REPLACE PROCEDURE post_to_linkedin (
  p_content IN CLOB
) IS
  l_body        CLOB;
  l_resp        CLOB;
  l_status_code NUMBER;
  l_token       VARCHAR2(4000);
  l_person_urn  VARCHAR2(200);
BEGIN
  SELECT access_token, person_urn INTO l_token, l_person_urn
    FROM li_oauth_settings WHERE id = 1;

  l_body := '{
    "author": "' || l_person_urn || '",
    "commentary": ' || APEX_JSON.stringify(p_content) || ',
    "visibility": "PUBLIC",
    "distribution": {
      "feedDistribution": "MAIN_FEED",
      "targetEntities": [],
      "thirdPartyDistributionChannels": []
    },
    "lifecycleState": "PUBLISHED",
    "isReshareDisabledByAuthor": false
  }';

  APEX_WEB_SERVICE.g_request_headers.delete;
  APEX_WEB_SERVICE.g_request_headers(1).name  := 'Authorization';
  APEX_WEB_SERVICE.g_request_headers(1).value := 'Bearer ' || l_token;
  APEX_WEB_SERVICE.g_request_headers(2).name  := 'LinkedIn-Version';
  APEX_WEB_SERVICE.g_request_headers(2).value := '202506';
  APEX_WEB_SERVICE.g_request_headers(3).name  := 'X-Restli-Protocol-Version';
  APEX_WEB_SERVICE.g_request_headers(3).value := '2.0.0';
  APEX_WEB_SERVICE.g_request_headers(4).name  := 'Content-Type';
  APEX_WEB_SERVICE.g_request_headers(4).value := 'application/json';

  l_resp := APEX_WEB_SERVICE.make_rest_request(
              p_url         => 'https://api.linkedin.com/rest/posts',
              p_http_method => 'POST',
              p_body        => l_body
            );

  l_status_code := APEX_WEB_SERVICE.g_status_code;

  IF l_status_code != 201 THEN
    RAISE_APPLICATION_ERROR(-20001,
      'LinkedIn API error (' || l_status_code || '): ' || l_resp);
  END IF;
END post_to_linkedin;
/
Why it's built this way

LinkedIn's newer /rest/posts endpoint (not the deprecated /v2/ugcPosts) requires the LinkedIn-Version header in YYYYMM format plus X-Restli-Protocol-Version: 2.0.0. A successful call returns HTTP 201 with an empty body — the new post's ID comes back in a response header, not the JSON.

Wire Up the Post Button

On BTN_POST, add a Dynamic Action → Execute Server-Side Code (After Submit):

PL/SQL · button actionBEGIN
  post_to_linkedin(:P2_COMMENT);
  apex_application.g_print_success_message := 'Posted to LinkedIn!';
EXCEPTION
  WHEN OTHERS THEN
    apex_error.add_error(
      p_message => 'Failed to post: ' || SQLERRM,
      p_display_location => apex_error.c_inline_in_notification
    );
END;

Part 6 — Test the Whole Flow

1

Reset your tables so you're starting clean:

DELETE FROM li_debug_log;
UPDATE li_oauth_settings SET access_token=NULL, refresh_token=NULL, person_urn=NULL WHERE id=1;
COMMIT;
2

Navigate to Page 1 and click Connect.

3

You'll be sent to LinkedIn's login/consent screen — log in and click Allow.

4

You'll land automatically back on Page 2 — no login prompt this time, since it's public.

5

Check the debug log:

SELECT id, tag, log_time, message FROM li_debug_log ORDER BY id;

You should see A_START with a real code value, and B_RESPONSE containing a real access_token, expires_in, and id_token.

6

Confirm the table now has real values:

SELECT SUBSTR(access_token,1,30) token_preview, LENGTH(access_token) len, person_urn
  FROM li_oauth_settings WHERE id = 1;
7

Type something into P2_COMMENT and click Post to LinkedIn.

8

Check your actual LinkedIn feed — your post should be there.

Common Errors and What They Actually Mean

A summary of every error hit while building this — chances are you'll see at least one of these too.

ErrorWhat it actually meansFix
redirect_uri does not match the registered valueURL registered in LinkedIn's Auth tab doesn't character-for-character match your PL/SQL. Often a domain mismatch or leftover ?session= fragment.Use apex_page.get_url, strip the session param, prepend the correct domain, keep both identical.
PLS-00306: wrong number or types of arguments in call to 'MAKE_REST_REQUEST'APEX_WEB_SERVICE.MAKE_REST_REQUEST has no p_content_type parameter.Set Content-Type via g_request_headers instead.
ERR-1002 Unable to find item ID for item "CODE"Newer APEX auto-maps query string parameters to page items with matching names, and errors if none exists.Create hidden items named exactly CODE and STATE.
PLS-00302: component 'URL_DECODE' must be declaredAPEX_UTIL.URL_DECODE doesn't exist in older APEX versions.Usually unnecessary — once CODE is a proper item, APEX auto-decodes it.
{"code":"EMPTY_ACCESS_TOKEN"} (401)access_token column is NULL — the exchange never actually succeeded.Check the debug log for LinkedIn's real response from the exchange step.
Callback page silently asks you to log in againPage requires APEX authentication, but LinkedIn's redirect is anonymous — the login prompt swallows the code.Make the callback page Public.
{"error":"invalid_client"}Client ID/Secret mismatch, often from an un-encoded special character in the secret corrupting the form body.URL-encode every field in the token-exchange POST body.

What Happens Next — Token Expiry

Note

LinkedIn access tokens for this product tier last roughly 60 days. When yours expires, simply repeat the Page 1 → Connect flow to get a fresh one — for a personal tool used occasionally, this is simpler than building automatic refresh-token handling, though that's a reasonable next enhancement.


Wrap-up: what looks like a simple "click a button, post to LinkedIn" feature actually touches a surprising number of APEX and OAuth details — friendly URL routing, page-level public access, form encoding, and LinkedIn's specific API version requirements. None of the individual pieces are hard, but they only click into place once you've hit each error at least once. Hopefully seeing them laid out end-to-end here saves you a few of those debugging cycles.

Comments

Popular posts from this blog

APEX - Tip: Fix Floating Label Issue

Oracle APEX's Universal Theme provides a modern and clean user experience through features like floating (above) labels for page items.  These floating labels work seamlessly when users manually enter data, automatically moving the label above the field on focus or input.  However, a common UI issue appears when page item values are set Dynamically the label and the value overlap, resulting in a broken and confusing user interface. once the user focuses the affected item even once, the label immediately corrects itself and displays properly. When an issue is reported, several values are populated based on a single user input, causing the UI to appear misaligned and confusing for the end user. Here, I'll share a few tips to fix this issue. For example, employee details are populated based on the Employee name. In this case, the first True Action is used to set the values, and in the second True Action, paste the following code setTimeout(function () {   $("#P29_EMAIL,#P29_...

Oracle APEX UI Tip: Display Page Title Next to the APEX Logo

In most Oracle APEX applications, every page has a Page Title displayed at the top. While useful, this title occupies vertical space, especially in apps where screen real estate matters (dashboards, reports, dense forms). So the goal is simple: Show the page title near the APEX logo instead of consuming page content space. This keeps the UI clean, professional, and consistent across all pages. Instead of placing the page title inside the page body:         ✅ Fetch the current page title dynamically         ✅ Display it right after the APEX logo         ✅ Do it globally, so it works for every page All of this is achieved using:         ✅ Global Page (Page 0)         ✅ One Dynamic Action         ✅ PL/SQL + JavaScript Simple, effective, and reusable. 1️⃣ Create a Global Page Item On Page 0 (Global Page), create a hidden item:      P0_PAGE_TITLE This item wi...

Building a Custom Debug Package for Oracle APEX Using PL/SQL

While developing Oracle APEX applications, debugging page processes and backend PL/SQL logic can be challenging—especially when values are lost between processes or execution flow is unclear.  Although DBMS_OUTPUT is useful, it doesn’t work well inside APEX runtime. To solve this, I built a custom PL/SQL debug Package that logs execution flow and variable values into a database table.  This approach helps trace exactly where the code reached, what values were passed, and whether a block executed or not - even inside page-level processes and packaged procedures Why a Custom Debug Package? Works seamlessly inside Oracle APEX page processes Persists debug information even after session ends Helps trace execution flow Captures runtime values Can be turned ON/OFF dynamically Does not interrupt business logic The Package consists of:- Debug Table                         -  Stores debug messages Sequence ...