OpenAI API in ABAP: Auth, Streaming & Errors
ABAP

OpenAI API in ABAP: Auth, Streaming & Errors

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_url with 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_json with pretty_mode-low_case for 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.