The previous post covered DB2 for i triggers — SQL and external triggers, BEFORE and AFTER timing, using the trigger buffer in RPG programs, audit logging patterns, and validation triggers using SIGNAL. This post covers the reverse direction: calling external REST APIs from IBM i RPG programs using Scott Klement’s widely used HTTPAPI open source library. Where triggers push logic down into the database, HTTPAPI pushes IBM i outward — allowing ILE RPG programs to retrieve shipping rates, authorise payments, validate addresses, or interact with any HTTP-based service without leaving the IBM i transaction.
Why Call External REST APIs from RPG
The business case is straightforward. Modern business ecosystems depend on services that are only available over HTTP. When your RPG order-entry program needs to do any of the following, it must make an outbound HTTP call:
- Shipping rate calculation — courier APIs (FedEx, UPS, DHL, Australia Post) accept order weight, dimensions, and destination and return rate options in real time
- Payment authorisation — Stripe, Braintree, and bank payment gateways require an HTTPS POST with card or token details before an order can be confirmed
- Address validation — Google Maps, Loqate, and similar APIs verify and normalise a postal address at data-entry time
- Weather data for agriculture and logistics — free APIs such as Open-Meteo supply current conditions or forecasts that influence dispatch decisions
- Outbound webhooks — notifying a CRM, ERP, or e-commerce platform that an order has shipped by posting a JSON payload to their webhook endpoint
The alternative — writing an intermediate Java or Python microservice that IBM i calls via a stored procedure or data queue — adds an extra moving part. HTTPAPI lets the RPG program make the call directly, keeping the logic in one place.
HTTPAPI Overview
HTTPAPI (also known as HTTP API for ILE) is an open source ILE service program library written and maintained by Scott Klement. It wraps IBM i’s native socket and SSL/TLS layers and exposes a clean set of RPG-callable procedure prototypes for HTTP GET, POST, PUT, DELETE, and more. The library has been maintained since the early 2000s and is widely deployed on production IBM i systems across every release from V5R1 to IBM i 7.5.
Key characteristics:
- Pure ILE — compiles to a *SRVPGM and is called from any ILE language (RPG, COBOL, CL, C)
- Uses IBM i’s own SSL stack (GSKit) — no third-party SSL dependency
- Supports chunked transfer encoding, redirects, cookies, and custom headers
- Response data can be written to a stream file in the IFS or returned directly to a memory buffer
- Source is hosted at
https://github.com/ScottKlement/httpapiand on SourceForge
YAJL (Yet Another JSON Library), also by Scott Klement, is the companion JSON parser and generator. It wraps the C YAJL library and exposes RPG-callable procedures for both parsing incoming JSON and generating outgoing JSON payloads. HTTPAPI and YAJL are typically installed and used together.
Installing HTTPAPI and YAJL
The recommended installation method in 2026 is to download the pre-built save files from the GitHub releases page or compile from source. The steps below cover compilation from source, which gives you the latest version.
Download the source archives from GitHub and transfer them to the IFS:
-- On a workstation, download: -- https://github.com/ScottKlement/httpapi/archive/refs/heads/master.zip -- https://github.com/ScottKlement/yajl/archive/refs/heads/master.zip -- Transfer to IBM i IFS via FTP binary mode or ACS IFS explorer: -- /home/MYUSR/httpapi-master.zip -- /home/MYUSR/yajl-master.zip
Unzip and compile YAJL first (HTTPAPI depends on it):
CALL QSYS/RSTOBJ OBJ(*ALL) SAVLIB(YAJL) DEV(*SAVF) SAVF(QGPL/YAJLSAVF)
If compiling from source using the provided build CL:
/* Create a build library */
CRTLIB LIB(YAJL) TEXT('YAJL JSON library')
/* Run the build CL script supplied in the source */
CALL YAJLBLD/BLDYAJL
/* Repeat for HTTPAPI */
CRTLIB LIB(HTTPAPI) TEXT('HTTPAPI HTTP client library')
CALL HTTAPIBLD/BLDHTTP
After a successful build, the library HTTPAPI contains:
HTTPAPI— the main HTTP service program (*SRVPGM)HTTPAPI_H— the binding directory (*BNDDIR) to reference in your RPG compileYAJL— the JSON service program (*SRVPGM)YAJL_H— the binding directory for YAJL
Add HTTPAPI to your library list or use qualified names. Reference the binding directories in your program compile:
CRTRPGMOD MODULE(MYLIB/MYMOD) SRCFILE(MYLIB/QRPGLESRC)
CRTPGM PGM(MYLIB/MYPGM) MODULE(MYLIB/MYMOD)
BNDDIR(HTTPAPI/HTTPAPI_H HTTPAPI/YAJL_H)
Making an HTTP GET Request
The core HTTPAPI procedure for GET requests is http_get(). It takes a URL and writes the response to a location you specify. The simplest form writes the response to a stream file in the IFS:
http_get( URL : '/tmp/response.json' );
HTTPAPI includes a comprehensive set of copybooks (copy source members) that contain all procedure prototypes. Include the main copybook at the top of your RPG source:
/COPY HTTPAPI/QRPGLESRC,HTTPAPI_H
Error handling uses http_error() which returns a descriptive error string, and the return code of http_get() itself (0 = success, negative = error):
DCL-S rc INT(10);
DCL-S errMsg VARCHAR(256);
rc = http_get('https://api.example.com/resource' : '/tmp/response.json');
IF rc < 0;
errMsg = http_error();
// Handle error — log to message queue, set error flag, etc.
ENDIF;
Complete GET Example — Calling a Weather API
This example calls the free Open-Meteo API (api.open-meteo.com) to retrieve the current temperature for a given latitude and longitude, then parses the JSON response with YAJL to extract the temperature value. The result is stored in a DB2 for i table for downstream use.
**FREE
// ─────────────────────────────────────────────────────────────────────
// GETWEATHER – Fetch current temperature from Open-Meteo API
// Stores result in MYLIB.WTHRLOG
// ─────────────────────────────────────────────────────────────────────
CTL-OPT DFTACTGRP(*NO) ACTGRP('GETWEATHER') BNDDIR('HTTPAPI_H':'YAJL_H');
/COPY HTTPAPI/QRPGLESRC,HTTPAPI_H
/COPY HTTPAPI/QRPGLESRC,YAJL_H
DCL-S URL VARCHAR(512);
DCL-S RespFile VARCHAR(128) INZ('/tmp/weather_resp.json');
DCL-S rc INT(10);
DCL-S errMsg VARCHAR(256);
DCL-S Temperature PACKED(7:2);
DCL-S EventType INT(10);
DCL-S StringVal VARCHAR(512);
DCL-S yHandle POINTER;
DCL-S InTemp IND INZ(*OFF);
// ── Build the API URL ─────────────────────────────────────────────────
URL = 'https://api.open-meteo.com/v1/forecast'
+ '?latitude=28.6139' // New Delhi
+ '&longitude=77.2090'
+ '¤t_weather=true'
+ '&temperature_unit=celsius';
// ── Call the API ──────────────────────────────────────────────────────
rc = http_get(URL : RespFile);
IF rc < 0;
errMsg = http_error();
DSPLY errMsg;
*INLR = *ON;
RETURN;
ENDIF;
// ── Parse the JSON response with YAJL ────────────────────────────────
// Response structure:
// { "current_weather": { "temperature": 34.1, "windspeed": 12.5, ... } }
yHandle = yajl_open_file(RespFile);
IF yHandle = *NULL;
DSPLY 'Cannot open JSON response file';
*INLR = *ON;
RETURN;
ENDIF;
DOW yajl_lexer(yHandle : EventType : StringVal) > 0;
SELECT;
WHEN EventType = YAJL_STRING_KEY AND StringVal = 'temperature';
InTemp = *ON;
WHEN EventType = YAJL_NUMBER AND InTemp = *ON;
Temperature = %DEC(StringVal : 7 : 2);
InTemp = *OFF;
ENDSL;
ENDDO;
yajl_close(yHandle);
// ── Store result in DB2 ───────────────────────────────────────────────
EXEC SQL
INSERT INTO MYLIB.WTHRLOG
(WLOGDT, WLOGTM, WLOGLAT, WLOGLON, WLOGTEMP)
VALUES
(CURRENT_DATE, CURRENT_TIME, 28.6139, 77.2090, :Temperature);
*INLR = *ON;
RETURN;
Making an HTTP POST Request with a JSON Body
POST requests require setting the Content-Type request header to application/json and supplying the request body. HTTPAPI provides http_setOption() for options and a family of POST procedures. The most flexible is http_url_post_stmf() which posts a stream file as the request body, and http_url_post() which posts a memory buffer.
To build a JSON request body, use the YAJL generator procedures:
DCL-S yGen POINTER; DCL-S jsonBuf VARCHAR(4096) CCSID(1208); // UTF-8 yGen = yajl_genOpen(*ON); // *ON = beautify output yajl_beginObject(yGen); yajl_addChar(yGen : 'weight' : '12.5'); yajl_addChar(yGen : 'length' : '40'); yajl_addChar(yGen : 'width' : '30'); yajl_addChar(yGen : 'height' : '20'); yajl_addChar(yGen : 'destination' : 'Sydney'); yajl_addChar(yGen : 'service' : 'express'); yajl_endObject(yGen); jsonBuf = yajl_copyBuf(yGen); yajl_genClose(yGen);
Then set the Content-Type header and call http_url_post():
http_setOption('Content-Type' : 'application/json');
rc = http_url_post( 'https://api.courier.example.com/rates'
: %ADDR(jsonBuf) + 2 // skip the VARCHAR 2-byte length prefix
: %LEN(jsonBuf)
: '/tmp/rates_resp.json' );
Complete POST Example — Courier Shipping Rate API
This example sends order weight, dimensions, and destination to a courier rates API, then parses the JSON response to find the cheapest rate and updates the order record in DB2 for i.
**FREE
// ─────────────────────────────────────────────────────────────────────
// GETRATES – Fetch courier shipping rates for an order
// Parameters: order number (input), cheapest rate (output)
// ─────────────────────────────────────────────────────────────────────
CTL-OPT DFTACTGRP(*NO) ACTGRP('GETRATES') BNDDIR('HTTPAPI_H':'YAJL_H');
/COPY HTTPAPI/QRPGLESRC,HTTPAPI_H
/COPY HTTPAPI/QRPGLESRC,YAJL_H
DCL-PI *N;
pOrdNo CHAR(10) CONST;
pRate PACKED(9:2);
pService CHAR(30);
END-PI;
DCL-S URL VARCHAR(256) INZ('https://api.courier.example.com/v2/rates');
DCL-S RespFile VARCHAR(128) INZ('/tmp/rates_resp.json');
DCL-S rc INT(10);
DCL-S yGen POINTER;
DCL-S yHandle POINTER;
DCL-S jsonBuf VARCHAR(4096) CCSID(1208);
DCL-S EventType INT(10);
DCL-S StringVal VARCHAR(512);
DCL-S BestRate PACKED(9:2) INZ(999999);
DCL-S BestSvc CHAR(30) INZ('');
DCL-S CurrRate PACKED(9:2) INZ(0);
DCL-S CurrSvc CHAR(30) INZ('');
DCL-S InRate IND INZ(*OFF);
DCL-S InService IND INZ(*OFF);
DCL-S InObject INT(10) INZ(0);
// ── Read order dimensions from DB2 ───────────────────────────────────
DCL-S w_Wt PACKED(7:2);
DCL-S w_Len PACKED(5:0);
DCL-S w_Wid PACKED(5:0);
DCL-S w_Hgt PACKED(5:0);
DCL-S w_Dst CHAR(30);
EXEC SQL
SELECT ORDWT, ORDLEN, ORDWID, ORDHGT, ORDSHIPTO
INTO :w_Wt, :w_Len, :w_Wid, :w_Hgt, :w_Dst
FROM ORDLIB.ORDHDR
WHERE ORDNO = :pOrdNo;
// ── Build JSON request body ───────────────────────────────────────────
yGen = yajl_genOpen(*OFF);
yajl_beginObject(yGen);
yajl_addChar(yGen : 'weight' : %CHAR(w_Wt));
yajl_addChar(yGen : 'length' : %CHAR(w_Len));
yajl_addChar(yGen : 'width' : %CHAR(w_Wid));
yajl_addChar(yGen : 'height' : %CHAR(w_Hgt));
yajl_addChar(yGen : 'destination' : %TRIMR(w_Dst));
yajl_endObject(yGen);
jsonBuf = yajl_copyBuf(yGen);
yajl_genClose(yGen);
// ── Set headers and POST ──────────────────────────────────────────────
http_setOption('Content-Type' : 'application/json');
http_setOption('Accept' : 'application/json');
rc = http_url_post( URL
: %ADDR(jsonBuf) + 2
: %LEN(jsonBuf)
: RespFile );
IF rc < 0;
pRate = -1;
pService = http_error();
*INLR = *ON;
RETURN;
ENDIF;
// ── Parse rates array: [{"service":"express","rate":18.50}, ...] ──────
yHandle = yajl_open_file(RespFile);
DOW yajl_lexer(yHandle : EventType : StringVal) > 0;
SELECT;
WHEN EventType = YAJL_BEGIN_OBJECT;
InObject += 1;
IF InObject = 2; // inside a rate object (depth 2)
CurrRate = 0;
CurrSvc = '';
ENDIF;
WHEN EventType = YAJL_END_OBJECT AND InObject = 2;
IF CurrRate < BestRate AND CurrRate > 0;
BestRate = CurrRate;
BestSvc = CurrSvc;
ENDIF;
InObject -= 1;
WHEN EventType = YAJL_STRING_KEY AND StringVal = 'service';
InService = *ON;
WHEN EventType = YAJL_STRING_KEY AND StringVal = 'rate';
InRate = *ON;
WHEN EventType = YAJL_STRING AND InService = *ON;
CurrSvc = StringVal;
InService = *OFF;
WHEN EventType = YAJL_NUMBER AND InRate = *ON;
CurrRate = %DEC(StringVal : 9 : 2);
InRate = *OFF;
ENDSL;
ENDDO;
yajl_close(yHandle);
// ── Update order record with cheapest rate ────────────────────────────
EXEC SQL
UPDATE ORDLIB.ORDHDR
SET ORDSHIPCST = :BestRate,
ORDSHIPSVC = :BestSvc
WHERE ORDNO = :pOrdNo;
pRate = BestRate;
pService = BestSvc;
*INLR = *ON;
RETURN;
OAuth 2.0 Token Flow from RPG
Most production APIs require OAuth 2.0 bearer tokens rather than simple API keys. The client credentials grant is the appropriate flow for machine-to-machine calls from IBM i — there is no user interaction, only a server-to-server token exchange.
The flow has three steps: POST to the token endpoint with client ID and secret, receive an access token with an expiry time, then include the token as a Bearer header in subsequent API calls. The token should be cached in a DB2 table or data area to avoid requesting a new token on every transaction.
Step 1 — request a token:
**FREE
// ─────────────────────────────────────────────────────────────────────
// GETTOKEN – OAuth 2.0 client credentials token fetch
// Caches the token in MYLIB.OAUTHTOK
// ─────────────────────────────────────────────────────────────────────
CTL-OPT DFTACTGRP(*NO) ACTGRP('GETTOKEN') BNDDIR('HTTPAPI_H':'YAJL_H');
/COPY HTTPAPI/QRPGLESRC,HTTPAPI_H
/COPY HTTPAPI/QRPGLESRC,YAJL_H
DCL-C CLIENT_ID 'my_client_id_here';
DCL-C CLIENT_SEC 'my_client_secret_here';
DCL-C TOKEN_URL 'https://auth.example.com/oauth2/token';
DCL-S RespFile VARCHAR(128) INZ('/tmp/token_resp.json');
DCL-S PostBody VARCHAR(256) CCSID(1208);
DCL-S AuthHdr VARCHAR(512) CCSID(1208);
DCL-S rc INT(10);
DCL-S yHandle POINTER;
DCL-S EventType INT(10);
DCL-S StringVal VARCHAR(1024);
DCL-S Token VARCHAR(2048) INZ('');
DCL-S ExpiresIn INT(10) INZ(0);
DCL-S InToken IND INZ(*OFF);
DCL-S InExpiry IND INZ(*OFF);
// ── Build the x-www-form-urlencoded POST body ─────────────────────────
PostBody = 'grant_type=client_credentials'
+ '&client_id=' + CLIENT_ID
+ '&client_secret=' + CLIENT_SEC;
// ── Set Content-Type for form-encoded body ────────────────────────────
http_setOption('Content-Type' : 'application/x-www-form-urlencoded');
// ── POST to token endpoint ────────────────────────────────────────────
rc = http_url_post( TOKEN_URL
: %ADDR(PostBody) + 2
: %LEN(PostBody)
: RespFile );
IF rc < 0;
DSPLY ('Token request failed: ' + http_error());
*INLR = *ON;
RETURN;
ENDIF;
// ── Parse the token response ──────────────────────────────────────────
// Response: {"access_token":"eyJ...","expires_in":3600,"token_type":"Bearer"}
yHandle = yajl_open_file(RespFile);
DOW yajl_lexer(yHandle : EventType : StringVal) > 0;
SELECT;
WHEN EventType = YAJL_STRING_KEY AND StringVal = 'access_token';
InToken = *ON;
WHEN EventType = YAJL_STRING_KEY AND StringVal = 'expires_in';
InExpiry = *ON;
WHEN EventType = YAJL_STRING AND InToken = *ON;
Token = StringVal;
InToken = *OFF;
WHEN EventType = YAJL_NUMBER AND InExpiry = *ON;
ExpiresIn = %INT(StringVal);
InExpiry = *OFF;
ENDSL;
ENDDO;
yajl_close(yHandle);
// ── Store token and expiry in DB2 cache table ─────────────────────────
// OAUTHTOK(TOKID CHAR(50), TOKVAL VARCHAR(2048), TOKEXPIRY TIMESTAMP)
EXEC SQL
MERGE INTO MYLIB.OAUTHTOK AS t
USING (VALUES('COURIER_API', :Token,
CURRENT_TIMESTAMP + :ExpiresIn SECONDS)) AS s(TOKID, TOKVAL, TOKEXPIRY)
ON t.TOKID = s.TOKID
WHEN MATCHED THEN UPDATE SET t.TOKVAL = s.TOKVAL, t.TOKEXPIRY = s.TOKEXPIRY
WHEN NOT MATCHED THEN INSERT (TOKID, TOKVAL, TOKEXPIRY)
VALUES (s.TOKID, s.TOKVAL, s.TOKEXPIRY);
*INLR = *ON;
RETURN;
Step 2 — use the cached token in an API call. A helper procedure reads the token from DB2, checks expiry, refreshes if needed, and returns the Bearer header value:
// ── Retrieve a valid bearer token ─────────────────────────────────────
DCL-S CachedToken VARCHAR(2048);
DCL-S TokenExpiry TIMESTAMP;
DCL-S Now TIMESTAMP;
Now = CURRENT_TIMESTAMP;
EXEC SQL
SELECT TOKVAL, TOKEXPIRY
INTO :CachedToken, :TokenExpiry
FROM MYLIB.OAUTHTOK
WHERE TOKID = 'COURIER_API';
IF SQLCODE <> 0 OR TokenExpiry <= Now + 5 MINUTES;
// Token missing or expires within 5 minutes — refresh it
CALLP GetToken(); // call the GETTOKEN program or procedure
EXEC SQL
SELECT TOKVAL INTO :CachedToken
FROM MYLIB.OAUTHTOK
WHERE TOKID = 'COURIER_API';
ENDIF;
// ── Set the Bearer header for the next HTTP call ──────────────────────
http_setOption('Authorization' : 'Bearer ' + %TRIMR(CachedToken));
SSL/TLS and Certificate Management
HTTPAPI uses IBM i’s own SSL/TLS stack via GSKit. For HTTPS calls to succeed, the server’s certificate chain must be trusted by the IBM i Digital Certificate Manager (DCM). This is the most common cause of HTTPAPI failures on first deployment.
IBM i Digital Certificate Manager (DCM) is the system certificate store. Access it through the IBM i Navigator web interface or via the 5250 command STRDCM. Procedure to add a trusted root CA:
- In DCM, navigate to Manage Certificate Store > *SYSTEM
- Select Populate with default IBM CA certificates if starting fresh
- To add a custom or private CA, select Import Certificate and upload the PEM or DER file
- Assign the certificate store to the *SYSTEM application definition
To tell HTTPAPI which certificate store to use, call http_setCertificate() before making the HTTP call:
// Use the *SYSTEM certificate store (default for most environments)
http_setCertificate( '*SYSTEM' : '' );
// Or specify an explicit certificate store path and password:
http_setCertificate( '/QIBM/UserData/ICSS/Cert/Server/DEFAULT.KDB'
: 'my_keystore_password' );
The environment variable QIBM_USE_SIGNCERT can also influence certificate validation. To disable it entirely for testing (never in production):
ADDENVVAR ENVVAR(QIBM_USE_SIGNCERT) VALUE('0') REPLACE(*YES)
Common SSL errors and their resolutions:
- SSL0008E: Certificate not trusted — the server’s root CA is not in the IBM i certificate store. Import the CA certificate into DCM.
- SSL0119E: Certificate expired — the server’s certificate has expired. Contact the API provider or check that the IBM i system date is correct.
- SSL0601E: GSKit not initialised — the QSSLPCL (protocol) and QSSLCSL (cipher) system values may be too restrictive. Review with
DSPSYSVAL QSSLPCLand enable TLSv1.2 or TLSv1.3. - TCP/IP connection refused — firewall or exit point blocking outbound port 443. Confirm with
PINGand check IBM i exit programs registered againstQIBM_QTC_CONNECT.
To verify connectivity from the IBM i command line before writing any RPG code:
CALL QP2TERM -- then at the PASE shell: curl -v https://api.open-meteo.com/v1/forecast?latitude=28.6&longitude=77.2¤t_weather=true
A successful curl response confirms that networking and SSL are working. If curl succeeds but HTTPAPI fails, the issue is in the certificate store configuration passed to HTTPAPI, not in the network itself.
Next post: IBM i System Startup and Shutdown Procedures — the QSTRUP startup programme, INZTCP, STRTCP, starting subsystems in the correct order, controlled shutdown with ENDSBS and PWRDWNSYS, IPL modes, and building reliable startup and shutdown CL programmes.