diff --git a/api_logs/api-logs.go b/api_logs/api-logs.go
index 2bb51c29b06df8a62acd54ee7bc8b95dc97f5d7e..0c797b7e98d2c89ee8f56a0e3bf46433b06173c5 100644
--- a/api_logs/api-logs.go
+++ b/api_logs/api-logs.go
@@ -2,7 +2,6 @@ package api_logs
 
 import (
 	"net/url"
-	"os"
 	"strconv"
 	"strings"
 	"time"
@@ -10,40 +9,7 @@ import (
 	"github.com/aws/aws-lambda-go/events"
 )
 
-var (
-	MaxReqBodyLength int = 1024
-	MaxResBodyLength int = 1024
-)
-
-func init() {
-	if s := os.Getenv("API_LOGS_MAX_REQ_BODY_LENGTH"); s != "" {
-		if i64, err := strconv.ParseInt(s, 10, 64); err == nil && i64 >= 0 {
-			MaxReqBodyLength = int(i64)
-		}
-	}
-	if s := os.Getenv("API_LOGS_MAX_RES_BODY_LENGTH"); s != "" {
-		if i64, err := strconv.ParseInt(s, 10, 64); err == nil && i64 >= 0 {
-			MaxResBodyLength = int(i64)
-		}
-	}
-}
-
-//var producer queues.Producer
-
-//func Init(p queues.Producer) {
-//	producer = p
-//}
-
-//Call this at the end of an API request handler to capture the req/res as well as all actions taken during the processing
-//(note: action list is only reset when this is called - so must be called after each handler, else action list has to be reset at the start)
-func LogIncomingAPIRequest(startTime time.Time, requestID string, claim map[string]interface{}, req events.APIGatewayProxyRequest, res events.APIGatewayProxyResponse) error {
-	//if producer == nil {
-	//	return errors.Errorf("api_logs queue producer not set")
-	//}
-
-	//todo: filter out some noisy (method+path)
-	//logs.Debugf("claim: %+v", claim)
-
+func GenerateIncomingAPILog(startTime time.Time, requestID string, claim map[string]interface{}, req events.APIGatewayProxyRequest, res events.APIGatewayProxyResponse) ApiLog {
 	endTime := time.Now()
 
 	var authType string
@@ -93,57 +59,24 @@ func LogIncomingAPIRequest(startTime time.Time, requestID string, claim map[stri
 			Headers:         req.Headers,
 			QueryParameters: req.QueryStringParameters,
 			BodySize:        len(req.Body),
-			//see below: Body:            req.Body,
+			Body:            req.Body,
 		},
 		Response: ApiLogResponse{
 			Headers:  res.Headers,
 			BodySize: len(res.Body),
-			//see below: Body:     res.Body,
+			Body:     res.Body,
 		},
 	}
-	if apiLog.Request.BodySize > MaxReqBodyLength {
-		apiLog.Request.Body = req.Body[:MaxReqBodyLength] + "..."
-	} else {
-		apiLog.Request.Body = req.Body
-	}
-	if apiLog.Response.BodySize > MaxResBodyLength {
-		apiLog.Response.Body = res.Body[:MaxResBodyLength] + "..."
-	} else {
-		apiLog.Response.Body = res.Body
-	}
 
 	//also copy multi-value query parameters to the log as CSV array values
 	for n, as := range req.MultiValueQueryStringParameters {
 		apiLog.Request.QueryParameters[n] = "[" + strings.Join(as, ",") + "]"
 	}
 
-	//todo: filter out excessive req/res body content per (method+path)
-	//todo: also need to do for all actions...
-	// if apiLog.Method == http.MethodGet {
-	// 	apiLog.Response.Body = "<not logged>"
-	// }
-
-	//todo: filter out sensitive values (e.g. OTP)
-	//if _, err := producer.NewEvent("API_LOGS").
-	//	Type("api-log").
-	//	RequestID(apiLog.RequestID).
-	//	Send(apiLog); err != nil {
-	//	return errors.Wrapf(err, "failed to send api-log")
-	//}
-	return nil
-} //LogIncomingAPIRequest()
-
-//Call LogOutgoingAPIRequest() after calling an API end-point as part of a handler,
-//to capture the details
-//and add it to the current handler log story for reporting/metrics
-func LogOutgoingAPIRequest(startTime time.Time, requestID string, claim map[string]interface{}, urlString string, method string, requestBody string, responseBody string, responseCode int) error {
-	//if producer == nil {
-	//	return errors.Errorf("api_logs queue producer not set")
-	//}
-
-	//todo: filter out some noisy (method+path)
-	//logs.Debugf("claim: %+v", claim)
+	return apiLog
+}
 
+func GenerateOutgoingAPILog(startTime time.Time, requestID string, claim map[string]interface{}, urlString string, method string, requestBody string, requestHeaders map[string]string, responseBody string, responseCode int) ApiLog {
 	endTime := time.Now()
 	userID, _ := claim["UserID"].(int64)
 	username, _ := claim["Username"].(string)
@@ -170,38 +103,20 @@ func LogOutgoingAPIRequest(startTime time.Time, requestID string, claim map[stri
 		Username:     username,
 		AccountID:    accountID,
 		Request: ApiLogRequest{
-			//Headers:         req.Headers,
+			Headers:         requestHeaders,
 			QueryParameters: params,
 			BodySize:        len(requestBody),
-			//See below: Body:            requestBody,
+			Body:            requestBody,
 		},
 		Response: ApiLogResponse{
-			//Headers:  res.Headers,
+			Headers:  requestHeaders,
 			BodySize: len(responseBody),
-			//See below: Body:     responseBody,
+			Body:     responseBody,
 		},
 	}
 
-	if apiLog.Request.BodySize > MaxReqBodyLength {
-		apiLog.Request.Body = requestBody[:MaxReqBodyLength] + "..."
-	} else {
-		apiLog.Request.Body = requestBody
-	}
-	if apiLog.Response.BodySize > MaxResBodyLength {
-		apiLog.Response.Body = responseBody[:MaxResBodyLength] + "..."
-	} else {
-		apiLog.Response.Body = responseBody
-	}
-
-	//todo: filter out sensitive values (e.g. OTP)
-	//if _, err := producer.NewEvent("API_LOGS").
-	//	Type("api-log").
-	//	RequestID(apiLog.RequestID).
-	//	Send(apiLog); err != nil {
-	//	return errors.Wrapf(err, "failed to send api-log")
-	//}
-	return nil
-} //LogOutgoingAPIRequest()
+	return apiLog
+}
 
 //ApiLog is the SQS event details struct encoded as JSON document, sent to SQS, to be logged for each API handler executed.
 type ApiLog struct {
diff --git a/search/time_series.go b/search/time_series.go
index e9f87dd5fc3a8bbbf45549a74b27e233ad2b9ae9..837878f068f3a9286f052e4a61652728c0cc4325 100644
--- a/search/time_series.go
+++ b/search/time_series.go
@@ -27,7 +27,25 @@ type TimeSeriesHeader struct {
 
 type TimeSeries interface {
 	Write(StartTime time.Time, EndTime time.Time, data interface{}) error
-	Search(query Query, limit int64) (docs interface{}, totalCount int, err error)
+
+	// Search returns docs indexed on OpenSearch document ID which cat be used in Get(id)
+	// The docs value type is the same as that of tmpl specified when you created the TimeSeries(..., tmpl)
+	// So you can safely type assert e.g.
+	//		type myType struct {...}
+	//		ts := search.TimeSeries(..., myType{})
+	//		docs,totalCount,err := ts.Search(...)
+	//		if err == nil {
+	//			for id,docValue := range docs {
+	//				doc := docValue.(myType)
+	//				...
+	//			}
+	//		}
+	Search(query Query, limit int64) (docs map[string]interface{}, totalCount int, err error)
+
+	// Get takes the id returned in Search()
+	// The id is uuid assigned by OpenSearch when documents are added with Write().
+	// The document value type is the same as that of tmpl specified when you created the TimeSeries(..., tmpl)
+	Get(id string) (interface{}, error)
 }
 
 type timeSeries struct {
@@ -41,11 +59,12 @@ type timeSeries struct {
 	createdDates map[string]bool
 
 	searchResponseBodyType reflect.Type
+	getResponseBodyType    reflect.Type
 }
 
-//purpose:
-//	create a time series to write e.g. api api_logs
-//parameters:
+// TimeSeries purpose:
+//	 create a time series to write e.g. api api_logs
+// parameters:
 //	name must be the openSearch index name prefix without the date, e.g. "uafrica-v3-api-api_logs"
 //		the actual indices in openSearch will be called "<indexName>-<ccyymmdd>" e.g. "uafrica-v3-api-api_logs-20210102"
 //	tmpl must be your log data struct consisting of public fields as:
@@ -114,6 +133,18 @@ func (w *writer) TimeSeries(name string, tmpl interface{}) (TimeSeries, error) {
 	if err != nil {
 		return nil, errors.Wrapf(err, "failed to make search response type for time-series")
 	}
+
+	// define get response type
+	// similar to GetResponseBody
+	ts.getResponseBodyType, err = reflection.CloneType(
+		reflect.TypeOf(GetResponseBody{}),
+		map[string]reflect.Type{
+			"._source": ts.dataType,
+		})
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to make get response type for time-series")
+	}
+
 	w.timeSeriesByName[name] = ts
 	return ts, nil
 }
@@ -360,7 +391,7 @@ type IndexSettings struct {
 //Search
 //Return:
 //	docs will be a slice of the TimeSeries data type
-func (ts *timeSeries) Search(query Query, limit int64) (docs interface{}, totalCount int, err error) {
+func (ts *timeSeries) Search(query Query, limit int64) (docs map[string]interface{}, totalCount int, err error) {
 	if ts == nil {
 		return nil, 0, errors.Errorf("time series == nil")
 	}
@@ -425,10 +456,66 @@ func (ts *timeSeries) Search(query Query, limit int64) (docs interface{}, totalC
 		return nil, 0, nil //no matches
 	}
 
-	items, err := reflection.Get(resBodyPtrValue, ".hits.hits[]._source")
+	hits, err := reflection.Get(resBodyPtrValue, ".hits.hits[]")
 	if err != nil {
 		err = errors.Wrapf(err, "cannot get search response documents")
 		return
 	}
-	return items.Interface(), hitsTotalValue.Interface().(int), nil
+
+	docs = map[string]interface{}{}
+	for i := 0; i < hits.Len(); i++ {
+		hit := hits.Index(i)
+		index := hit.Field(0).Interface().(string)    // HitDoc.Index
+		id := hit.Field(2).Interface().(string)       // HitDoc.ID
+		docs[index+"/"+id] = hit.Field(4).Interface() // HitDoc.Source
+	}
+	return docs, hitsTotalValue.Interface().(int), nil
+}
+
+func (ds *timeSeries) Get(id string) (doc interface{}, err error) {
+	if ds == nil {
+		return nil, errors.Errorf("document store == nil")
+	}
+	parts := strings.SplitN(id, "/", 2)
+	get := opensearchapi.GetRequest{
+		Index:        parts[0],
+		DocumentType: "_doc",
+		DocumentID:   parts[1],
+	}
+	getResponse, err := get.Do(context.Background(), ds.w.client)
+	if err != nil {
+		err = errors.Wrapf(err, "failed to get document")
+		return
+	}
+
+	switch getResponse.StatusCode {
+	case http.StatusOK:
+	default:
+		resBody, _ := ioutil.ReadAll(getResponse.Body)
+		err = errors.Errorf("Get failed with HTTP status %v: %s", getResponse.StatusCode, string(resBody))
+		return
+	}
+
+	resBodyPtrValue := reflect.New(ds.getResponseBodyType)
+	if err = json.NewDecoder(getResponse.Body).Decode(resBodyPtrValue.Interface()); err != nil {
+		err = errors.Wrapf(err, "cannot decode get response body")
+		return
+	}
+
+	foundVar, err := reflection.Get(resBodyPtrValue, ".found")
+	if err != nil {
+		err = errors.Wrapf(err, "cannot get found value")
+		return
+	}
+	if found, ok := foundVar.Interface().(bool); !ok || !found {
+		return nil, nil //not found
+	}
+
+	//found
+	source, err := reflection.Get(resBodyPtrValue, "._source")
+	if err != nil {
+		err = errors.Wrapf(err, "cannot get document from get response")
+		return
+	}
+	return source.Interface(), nil
 }