Calling the OpenAI API from ABAP is one of those things that sounds straightforward until you actually try it. You send a request, nothing comes back, and you spend two hours staring at a blank response buffer. I've been there. In this post I'll walk you through a production-ready pattern using cl_http_client — covering API key authentication, handling streaming responses, and building the kind of error handling that won't embarrass you in a production system.
No BTP middleware. No third-party adapters. Pure ABAP talking directly to OpenAI's REST endpoint. Let's get into it.
Why cl_http_client for the OpenAI API in ABAP?
cl_http_client is the workhorse of HTTP communication in classic ABAP. It's available on all NetWeaver-based systems, including S/4HANA on-premise, and gives you fine-grained control over headers, timeouts, and response parsing. For OpenAI's REST API — which is essentially HTTPS POST with JSON bodies — it's the right tool.
The alternative, cl_web_http_client_manager, is newer and cleaner but requires ABAP 7.54+. I'll focus on cl_http_client here because it works on more landscapes, but the concepts transfer directly.
Setting Up the HTTP Client and Authentication
OpenAI uses Bearer token authentication. Every request needs an Authorization header with your API key. Here's the basic scaffolding:
DATA: lo_client TYPE REF TO if_http_client,
lo_request TYPE REF TO if_http_request,
lo_response TYPE REF TO if_http_response,
lv_api_key TYPE string,
lv_body TYPE string,
lv_response TYPE string,
lv_status TYPE i.
" Retrieve API key from secure storage (never hardcode!)
lv_api_key = me->get_api_key( ). " fetch from SSFAPPL or custom secure table
cl_http_client=>create_by_url(
EXPORTING
url = 'https://api.openai.com/v1/chat/completions'
IMPORTING
client = lo_client
EXCEPTIONS
argument_not_found = 1
plugin_not_active = 2
internal_error = 3
OTHERS = 4
).
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_openai_error
EXPORTING
message = 'Failed to create HTTP client'.
ENDIF.
lo_request = lo_client->request.
" Set method and headers
lo_request->set_method( if_http_request=>co_request_method_post ).
lo_request->set_header_field(
name = 'Content-Type'
value = 'application/json'
).
lo_request->set_header_field(
name = 'Authorization'
value = |Bearer { lv_api_key }|
).A few things worth calling out here. First, never hardcode the API key in your ABAP source. Store it in a custom secure configuration table with appropriate authorizations, or use the SAP Secure Storage (SSFAPPL). I've seen production systems with API keys hardcoded in class attributes — don't be that developer.
Second, notice the RAISE EXCEPTION TYPE pattern. You should have a proper exception class for OpenAI errors. If you're not yet comfortable with class-based exceptions in ABAP, have a read through the exception handling concepts — clean error propagation matters a lot when integrating with external APIs.
Building the JSON Request Body
OpenAI's chat completions endpoint expects a JSON body with a model name and a messages array. You can build this with string concatenation, but using a proper JSON serializer is safer and more maintainable:
TYPES: BEGIN OF ty_message,
role TYPE string,
content TYPE string,
END OF ty_message.
DATA: lt_messages TYPE STANDARD TABLE OF ty_message WITH DEFAULT KEY,
ls_message TYPE ty_message.
ls_message-role = 'user'.
ls_message-content = iv_user_prompt.
APPEND ls_message TO lt_messages.
" Use /ui2/cl_json for serialization (available 7.40+)
DATA(lo_json) = NEW /ui2/cl_json( ).
DATA: BEGIN OF ls_request,
model TYPE string,
messages TYPE STANDARD TABLE OF ty_message WITH DEFAULT KEY,
max_tokens TYPE i,
temperature TYPE f,
END OF ls_request.
ls_request-model = 'gpt-4o'.
ls_request-messages = lt_messages.
ls_request-max_tokens = 1000.
ls_request-temperature = '0.7'.
/ui2/cl_json=>serialize(
EXPORTING
data = ls_request
pretty_name = /ui2/cl_json=>pretty_mode-low_case
RECEIVING
r_json = lv_body
).
lo_request->set_cdata( lv_body )./ui2/cl_json is your friend here. The pretty_mode-low_case option ensures your field names come out as lowercase JSON keys, which is what OpenAI expects. Without it, you'd get "MESSAGES" instead of "messages" and the API would return a 400 error that takes you a while to diagnose.
Sending the Request and Handling the Response
lo_client->send(
EXCEPTIONS
http_communication_failure = 1
http_invalid_state = 2
OTHERS = 3
).
IF sy-subrc <> 0.
DATA(lv_error_text) TYPE string.
lo_client->get_last_error(
IMPORTING
message = lv_error_text
).
RAISE EXCEPTION TYPE zcx_openai_error
EXPORTING
message = |HTTP send failed: { lv_error_text }|.
ENDIF.
lo_client->receive(
EXCEPTIONS
http_communication_failure = 1
http_invalid_state = 2
http_processing_failed = 3
OTHERS = 4
).
IF sy-subrc <> 0.
lo_client->get_last_error(
IMPORTING
message = lv_error_text
).
RAISE EXCEPTION TYPE zcx_openai_error
EXPORTING
message = |HTTP receive failed: { lv_error_text }|.
ENDIF.
lo_response = lo_client->response.
lv_status = lo_response->get_status( ).
lv_response = lo_response->get_cdata( ).Always call get_last_error after a failed send or receive. The default sy-subrc alone tells you almost nothing useful — the actual error message is what you need in your logs.
OpenAI API Error Handling in ABAP
HTTP status codes from OpenAI follow standard REST conventions, but you need to handle them explicitly. A 200 doesn't always mean success at the application level, and OpenAI returns structured JSON error objects you should parse:
CASE lv_status.
WHEN 200.
" Parse the successful response
me->parse_completion_response(
iv_json = lv_response
ev_content = rv_result
).
WHEN 401.
RAISE EXCEPTION TYPE zcx_openai_error
EXPORTING
message = 'Authentication failed — check your API key'.
WHEN 429.
" Rate limit hit — log and potentially retry
RAISE EXCEPTION TYPE zcx_openai_rate_limit
EXPORTING
message = 'OpenAI rate limit exceeded'
retry_able = abap_true.
WHEN 400.
" Bad request — parse OpenAI error body
DATA: BEGIN OF ls_error_wrapper,
error TYPE BEGIN OF error_detail,
message TYPE string,
type TYPE string,
code TYPE string,
END OF error_detail,
END OF ls_error_wrapper.
/ui2/cl_json=>deserialize(
EXPORTING
json = lv_response
CHANGING
data = ls_error_wrapper
).
RAISE EXCEPTION TYPE zcx_openai_error
EXPORTING
message = |OpenAI error: { ls_error_wrapper-error-message }|.
WHEN 500 OR 503.
RAISE EXCEPTION TYPE zcx_openai_error
EXPORTING
message = |OpenAI server error (HTTP { lv_status }) — retry later|.
WHEN OTHERS.
RAISE EXCEPTION TYPE zcx_openai_error
EXPORTING
message = |Unexpected HTTP status: { lv_status }|.
ENDCASE.The 429 rate limit case deserves a separate exception class (zcx_openai_rate_limit) specifically so callers can distinguish between "something is broken" and "slow down and retry". This matters in batch processing scenarios where you're sending many requests.
Parsing the Completion Response
METHOD parse_completion_response.
TYPES: BEGIN OF ty_message_content,
role TYPE string,
content TYPE string,
END OF ty_message_content.
TYPES: BEGIN OF ty_choice,
index TYPE i,
message TYPE ty_message_content,
finish_reason TYPE string,
END OF ty_choice.
DATA: BEGIN OF ls_response,
id TYPE string,
object TYPE string,
choices TYPE STANDARD TABLE OF ty_choice WITH DEFAULT KEY,
END OF ls_response.
/ui2/cl_json=>deserialize(
EXPORTING
json = iv_json
CHANGING
data = ls_response
).
IF lines( ls_response-choices ) = 0.
RAISE EXCEPTION TYPE zcx_openai_error
EXPORTING
message = 'No choices returned in OpenAI response'.
ENDIF.
ev_content = ls_response-choices[ 1 ]-message-content.
ENDMETHOD.A Note on Streaming Responses
OpenAI supports server-sent events (SSE) streaming via "stream": true in the request body. This is where cl_http_client hits a wall — it's a synchronous request/response client and doesn't natively support chunked streaming the way a WebSocket or async client would.
In practice, for ABAP there are two realistic approaches:
- Don't stream: For most SAP use cases (document processing, data enrichment, batch AI calls), you don't need streaming. Set
stream: false, wait for the full response, parse it. Simpler, more reliable, easier to test. - Partial streaming via chunked read: If you genuinely need progressive output — say, for a custom Fiori app that wants to show tokens as they arrive — you'd need to read the response body in chunks and parse SSE lines. This requires custom handling of the HTTP response stream and is significantly more complex. Consider whether the user experience benefit is worth the implementation cost.
For most enterprise ABAP scenarios I've worked on, the non-streaming approach is the right call. The latency of a full OpenAI response (typically 2-8 seconds for GPT-4o) is acceptable for background jobs, enhancement spots, and user-triggered document generation.
Timeout and Connection Management
One thing developers often forget: set explicit timeouts. OpenAI can take 30+ seconds for complex requests, and your ABAP work process will hang indefinitely without a timeout configured:
" Set timeouts BEFORE sending
lo_client->request->set_header_field(
name = if_http_header_fields=>request_timeout
value = '60' " seconds
).
" Also configure on the client itself
lo_client->propertytype_logon_popup = lo_client->co_disabled.
lo_client->propertytype_accept_cookie = lo_client->co_disabled.And always close the client when you're done:
lo_client->close(
EXCEPTIONS
http_invalid_state = 1
OTHERS = 2
).Leaving HTTP clients open is one of the more insidious memory leak patterns in ABAP. Make it a habit to close in a CLEANUP block if you're inside a TRY...CATCH.
SSL and Network Configuration
Don't forget the infrastructure side. For cl_http_client to reach api.openai.com over HTTPS, you need:
- The OpenAI SSL certificate chain imported into your SAP system via transaction STRUST (SSL client identity)
- A proxy configuration in SM59 if your SAP system sits behind a corporate proxy
- The target host whitelisted in your network firewall rules
Missing any of these produces cryptic SSL handshake errors that look like ABAP bugs but are actually infrastructure issues. Check SM59 and STRUST first before debugging your code.
Putting It All Together
Here's what the full method signature of a clean wrapper class looks like:
CLASS zcl_openai_client DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
constructor
RAISING zcx_openai_error,
chat_completion
IMPORTING
iv_prompt TYPE string
iv_model TYPE string DEFAULT 'gpt-4o'
iv_max_tokens TYPE i DEFAULT 1000
iv_temperature TYPE f DEFAULT '0.7'
RETURNING
VALUE(rv_result) TYPE string
RAISING
zcx_openai_error
zcx_openai_rate_limit.
PRIVATE SECTION.
DATA: mv_api_key TYPE string.
METHODS:
get_api_key
RETURNING VALUE(rv_key) TYPE string
RAISING zcx_openai_error,
parse_completion_response
IMPORTING iv_json TYPE string
EXPORTING ev_content TYPE string
RAISING zcx_openai_error.
ENDCLASS.Encapsulating all OpenAI communication in a single class with a clean public interface means your calling code stays simple, and you can swap out the underlying HTTP implementation without touching business logic. This kind of design thinking is what separates maintainable AI integrations from ones that become technical debt six months later.
If you're interested in how similar patterns apply to other AI providers, I covered the same architecture for Claude API integration from ABAP — worth reading alongside this post since the approaches are complementary.
For the broader context of building AI-powered capabilities directly in your SAP system, the article on RAG pipelines for SAP documentation with open-source LLMs shows how HTTP-based AI calls fit into larger retrieval architectures.
And if your ABAP code calling these APIs starts to smell after a few iterations of adding features, the guidance on refactoring ABAP to clean code standards will help you keep things manageable.
Key Takeaways
- Use
cl_http_client=>create_by_urlwith HTTPS for OpenAI endpoints — configure SSL in STRUST first - Always set the
Authorization: Bearer <key>header — store keys securely, never in source code - Use
/ui2/cl_jsonwithpretty_mode-low_casefor serialization/deserialization - Handle HTTP status codes explicitly — especially 429 (rate limit) and 400 (bad request with JSON error body)
- Set timeouts and always close the HTTP client in a CLEANUP block
- Skip streaming for most SAP use cases — synchronous full-response is simpler and reliable enough
Further reading: Once your ABAP code is calling the OpenAI API in production, the next concern is output safety — see AI Guardrails for SAP: Stop Hallucinations in Production for the validation patterns that keep LLM responses trustworthy.
This article is part of the SAP AI integration & architecture hub — patterns, use cases, and guardrails for AI in SAP systems.