API Integration Guide

How to sign a request, how replies and errors behave, and how to embed the 2D, 3D and dashboard views in your own pages.


This guide explains how to call the BAPLIE Viewer Online API and how to show the 2D view, the 3D view and the dashboard inside your own web pages.

The complete list of calls, their fields and example replies is in the API reference. This guide covers what the reference does not: how to sign a request, how replies and errors behave, and how to embed the viewers.

1. What you need

ItemWhere it comes from
API keyYour facility administrator finds it in BAPLIE Viewer Online under Manage Facility → Developers. It is sent in every request and is not secret.
Private keySame place. It is used to sign requests and must stay secret: keep it on your server, never put it in a web page, mobile app or any code a user can open.
Base URLhttps://api-gateway.baplieviewer.com/v4

API access has to be switched on for your facility by TEDIVO first. If you do not see the Developers tab, ask TEDIVO to enable it. API access is a paid feature available on enterprise plans only; contact us to discuss pricing.

Regenerating the private key takes effect immediately. Every request signed with the old key fails from that moment on. The button is in the Developers tab.

2. Making a request

Every call is an HTTP POST to BASE_URL/<call>, for example https://api-gateway.baplieviewer.com/v4/read/json.

  • Header x-api-key: <your API key>.
  • Body: ordinary form fields (application/x-www-form-urlencoded). JSON bodies are not accepted.
  • Every call except health/status includes a hash field (section 3).
  • Call the API from your server. A browser cannot call it directly, because it cannot send the x-api-key header safely and the private key must not be in the browser.

Example, reading the file you uploaded as MyVessel001:

curl -X POST https://api-gateway.baplieviewer.com/v4/read/json \
  -H "x-api-key: YOUR_API_KEY" \
  --data-urlencode "file_id=MyVessel001" \
  --data-urlencode "hash=0fe0707288b2e23628acacd9879542f1ae408142e41ed8b4693b0f33b42e363fd90c93c40db4e1b74d15163dff3eeaefb42e8b9aecb46d93f26a29b93abd914d"

The hash above was made with the example private key EXAMPLE_PRIVATE_KEY; yours will differ.

A quick test that your key works, no hash needed:

curl -X POST https://api-gateway.baplieviewer.com/v4/health/status -H "x-api-key: YOUR_API_KEY"

Expected reply: {"ack":"success","error_code":0}

3. The request hash — read this first

The hash proves that a request really comes from you and was not changed on the way. It is a SHA-512 of some of the request's own field values followed by your private key. The server repeats the calculation with the private key it has stored for you; if the two results differ the request is refused.

3.1 How to build it

  1. Look up the call in the table in section 9. It lists which fields go into the hash, and in what order.
  2. Take the values of those fields, exactly as you send them in the request, and join them one after another with nothing in between — no spaces, commas or line breaks of your own.
  3. Add your private key to the end.
  4. Calculate SHA-512 of that text (UTF-8) and write the result as lowercase hexadecimal, 128 characters.
  5. Send it as the hash field.

In other words: hash = sha512( value1 + value2 + ... + privateKey )

3.2 Worked examples

Private key used here: EXAMPLE_PRIVATE_KEY — use your own.

CallFields in the hash, in orderText that is hashedHash
read/jsonfile_idMyVessel001EXAMPLE_PRIVATE_KEY0fe0707288b2e23628acacd9879542f1ae408142e41ed8b4693b0f33b42e363fd90c93c40db4e1b74d15163dff3eeaefb42e8b9aecb46d93f26a29b93abd914d
delete/filefile_id, file_upload_idMyVessel0011234EXAMPLE_PRIVATE_KEY5e3e34e783c8380f354c9a32d129f0cf536a76cac3352983cc4b3ed84d9453a712e2e4f6b05c9c1a328ac407490859f2c9fc4fae54dddae87bbb9b2eaf8da334
view/2Dfile_id, user_idMyVessel001user-1EXAMPLE_PRIVATE_KEY9c1308a9b159c75fc61fab504ecb8641ff11cb7a3ac4a75f34229cf6ee96460fb7f9b6daf5abc7b0189aa53a9d08c8a53b141af4b0287de69ab3f78f8abf3edb
uploaddata, file_id, main_json, anomalies_jsonEDI-DATAMyVessel00100EXAMPLE_PRIVATE_KEY
with data=EDI-DATA, main_json=0, anomalies_json=0
456ac42de934071d8e1847bb5bddf32e828cd040ebb70fe98b65979d7ea7e9670f9f6bfcf698c4252bb085526d9bcfec80e7bdb0d00dd9e09a19f981d6d19389

You can check your own code against the first row: with file_id=MyVessel001 and private key EXAMPLE_PRIVATE_KEY you must get the hash that starts 0fe07072. On the command line:

printf '%s' 'MyVessel001EXAMPLE_PRIVATE_KEY' | openssl dgst -sha512

3.3 The same calculation in each language

Python

import hashlib

def make_hash(*values):
    return hashlib.sha512(("".join(values) + PRIVATE_KEY).encode("utf-8")).hexdigest()

make_hash("MyVessel001")

PHP

function make_hash($privateKey, ...$values) {
    return hash('sha512', implode('', $values) . $privateKey);
}

make_hash($privateKey, 'MyVessel001');

Node.js

import { createHash } from 'node:crypto';

const makeHash = (...values) =>
    createHash('sha512').update(values.join('') + PRIVATE_KEY, 'utf8').digest('hex');

makeHash('MyVessel001');

Java

static String makeHash(String... values) throws Exception {
    byte[] digest = MessageDigest.getInstance("SHA-512")
        .digest((String.join("", values) + PRIVATE_KEY).getBytes(StandardCharsets.UTF_8));
    StringBuilder hex = new StringBuilder();
    for (byte b : digest) hex.append(String.format("%02x", b));
    return hex.toString();
}

Shell

make_hash() {
    printf '%s' "$(printf '%s' "$@")${PRIVATE_KEY}" | openssl dgst -sha512 -r | cut -d' ' -f1
}

Any language works: it is a plain SHA-512 of a text. Complete runnable clients are in section 8.

3.4 Things that go wrong, and how to spot them

  • The values in the hash must be the values you send. If you hash file_id but send it with different spelling, spaces or case, the hash fails.
  • Hash the raw value, not the URL-encoded one. Hash the text of data or json_data as it is in your program; your HTTP library encodes it for the form on its own. If you encode it yourself and let the library encode it again, the server sees a different text.
  • Line endings in data count. The text of an uploaded EDI file is hashed character for character, including its line breaks. Read the file once, and use that same text for the hash and for the data field.
  • Order matters. file_id then file_upload_id is not the same as file_upload_id then file_id. Follow the table in section 9 — note that read/url hashes file_upload_id first.
  • Fields that are not in the table are not hashed. For example api_file_type and report_type are sent but are not part of the hash.
  • Use the id you sent, not the id the server stores. The server keeps your file under an internal name, but in every request, and in every hash, you use the file_id you chose when you uploaded the file.
  • Lowercase hex. Uppercase hexadecimal is not accepted.
  • No private key in the request. Only the hash is sent; the private key itself never travels.
  • Hash mismatch error codes differ per call — for example 202 for upload, 302 for read/*, 504 for view/2D, 900 for account/*. They all mean the hash the server calculated differs from yours.

4. Replies and errors

Replies are JSON with at least ack and error_code:

{"ack":"success","error_code":0}

{"ack":"failed","error_code":302,"error_msg":"API not from trusted source","error_details":"HASH does not match expected value"}
Always check ack and error_code, not the HTTP status. Through the gateway, the HTTP status of replies that come from the API is 200 even when ack is failed.
  • error_msg is a fixed text for the code. error_details, present only when there is something to add, gives more — for example the list of fields that were missing.
  • Error codes belong to a call. The same number can mean different things in different calls, so read an error code together with the call that returned it. All codes and examples per call are in the API reference.
  • The 2D, 3D and dashboard calls do not answer with JSON when they succeed; see section 6.

Problems found by the gateway itself, before the API sees the request, look different and use HTTP status 403:

{"ack":"failed","error_code":9994,"error_msg":"Forbidden","error_detail":"Forbidden"}
    the x-api-key header is missing or wrong

{"ack":"failed","error_code":9995,"error_msg":"Missing Authentication Token","error_detail":"Missing Authentication Token"}
    the call name or method does not exist - check the spelling and use POST

Note error_detail without the final s in these two, unlike the API's own error_details.

Most common codes

CodeMeaning
102The x-api-key header was not sent (reaches the API only on direct calls)
105The request was refused as not coming from an authorised source
103 / 104 / 106Facility membership expired / no permission for the file type / your facility is not allowed to use this call
2xx, 3xx … 18xxCall specific, see the reference. The “required parameters missing or empty” and “API not from trusted source” errors exist in every call.

5. A typical workflow

  1. Upload a BAPLIE file with upload. You choose the file_id — letters, digits, - and _ only. The reply contains file_upload_id: store it, several later calls need it.
  2. Read it back as JSON (read/json), get its anomalies (read/anomalies), a cargo summary (read/cargoSummary), a MOVINS for a port (read/movins) or one container (read/container).
  3. Change it with the container/* and operations/* calls. These only work on files uploaded through the API.
  4. Show it to your users with the 2D view, 3D view or dashboard (section 6).
  5. Delete it with delete/file when you no longer need it.

account/checkAccess returns the list of calls your facility may use. account/status shows the state of your membership. health/status needs no hash and is meant for monitoring.

Which calls change files: all container/* and operations/* calls change the stored file. When the facility has file backups switched on, the first change keeps an _original copy of the file as it was before.

5.1 CSV uploads: header details and default values

upload/csvFile turns a CSV file into a BAPLIE. The CSV holds the containers; the BAPLIE also needs header details about the vessel call — sender, recipient, vessel, voyage, ports and dates. For each of them you can either send the value in the call, or save a default once in BAPLIE Viewer Online. A value that you send in the call always wins; when the call does not send it, or sends it empty, the saved default is used.

Where to set the defaults: Manage Facility → Developers → Advanced options: CSV upload defaults (facility administrators). The section is hidden until you press the button, and is shown from the start when a value has been saved. Fill in what stays the same for every file; leave a field empty if you always send it. A field you leave empty stays empty in the BAPLIE (ports and dates are then left out altogether).

Field in the callDefault in the Developers tabWhat it isFormat
sender_idSender IDWho sent the BAPLIE (header: sender identification)text
rec_idRecipient IDWho the BAPLIE is for (header: recipient identification)text
vessel_nbrVoyage NumberVoyage number of the vessel calltext
vessel_car_idVessel carrierCarrier (shipping line) code of the vesseltext
vessel_ref_idVessel ReferenceVessel reference, for example the Lloyd's or IMO numbertext
vessel_nameVessel NameName of the vesseltext
place_departPlace of DeparturePort the vessel leftUN/LOCODE, for example USLAX
next_portNPOCNext port of callUN/LOCODE, for example USNYC
act_dateATAActual time of arrivalYYMMDDHHMM, for example 2610211530
est_dateETAEstimated time of arrivalYYMMDD, for example 261021
act_date_depATDActual time of departureYYMMDDHHMM, for example 2610220800
est_date_depETDEstimated time of departureYYMMDD, for example 261022
port_load_override_hPOL OverridePort of load to use for every container instead of the port in the CSV fileUN/LOCODE

Good to know

  • The defaults are used by the CSV upload call only. No other API call reads them.
  • port_load_override_h works only when you send it with each call. The value saved under POL Override is not applied yet.
  • The first twelve rows follow the rule above (call value first, saved default second). If neither exists, the field stays empty in the BAPLIE; ports and dates are then left out altogether.

6. Showing the 2D view, 3D view and dashboard in your page

The three views are shown by BAPLIE Viewer Online itself, inside an <iframe> in your page. You do not draw anything yourself.

6.1 How it works

  1. Your page contains an iframe whose src is a script on your server.
  2. That script calls the API (view/2D, view/3D or view/dashboard) with your keys and a hash.
  3. The API answers with a short HTML page, not JSON, containing a form which submits itself.
  4. Your script sends that HTML to the browser unchanged.
  5. The browser submits the form inside the iframe and the viewer opens, served from https://app.baplieviewer.com.

The browser never sees your private key. It does receive your API key, inside the HTML page; that key is not secret.

6.2 The three calls

ViewCallFieldsHash
2D viewview/2Dfile_id, user_id (optional), bay (optional), hashfile_id + user_id + private key
without user_id: file_id + private key
3D viewview/3Dfile_id, user_id (optional), bay (optional), hashfile_id + user_id + private key
without user_id: file_id + private key
Dashboardview/dashboardfile_id, hashfile_id + private key

file_id is the id you chose at upload. user_id identifies the person using your application (letters, digits and -). It is used with view/restrictContainers to limit which containers a person can see: call view/restrictContainers once per person and file with the container numbers they may see (all_bays=1 lets them see everything) and give the same user_id here. If you get an error saying no container list exists for the user (code 502 for 2D, 602 for 3D), create the list first. bay opens the view on that bay.

6.3 The page and the script

Your page:

<iframe src="/embed.php?view=2d&file=MyVessel001"
        style="width:100%;height:80vh;border:0" allow="fullscreen"></iframe>

The script on your server, PHP shown:

<?php
$views  = ['2d' => 'view/2D', '3d' => 'view/3D', 'dashboard' => 'view/dashboard'];
$view   = $_GET['view'] ?? '';
$fileId = $_GET['file'] ?? '';
if (!isset($views[$view]) || !preg_match('/^[A-Za-z0-9_-]+$/', $fileId)) { http_response_code(400); exit; }

// YOUR check: may the signed-in user of your application see this file?

$userId = 'user-1';   // the person using your application
$hash = ($view === 'dashboard')
    ? hash('sha512', $fileId . $privateKey)
    : hash('sha512', $fileId . $userId . $privateKey);
$fields = ($view === 'dashboard') ? ['file_id' => $fileId, 'hash' => $hash]
                                  : ['file_id' => $fileId, 'user_id' => $userId, 'hash' => $hash];

$ch = curl_init('https://api-gateway.baplieviewer.com/v4/' . $views[$view]);
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query($fields),
    CURLOPT_HTTPHEADER => ['x-api-key: ' . $apiKey], CURLOPT_RETURNTRANSFER => true]);
$answer = curl_exec($ch);

if (strpos(ltrim($answer), '{') === 0) { http_response_code(502); exit($answer); }   // a JSON answer is an error
header('Content-Type: text/html; charset=utf-8');
echo $answer;

6.4 Rules to follow

  • Check access yourself. The script is the only thing standing between a user and any file of your facility. Make sure the person asking for file=... is allowed to see that file, and never take a file_id from the browser without checking it.
  • Use HTTPS on your page.
  • Your page's own Content-Security-Policy, if you have one, must allow framing https://app.baplieviewer.com (frame-src https://app.baplieviewer.com).
  • Give the iframe room. The viewers are designed for a window of at least about 1000 by 600 pixels.
  • Pass the HTML through unchanged. Do not rebuild or re-sign the form inside it; its hidden fields are part of the API and can change between releases. The sample scripts simply forward whatever the view call answers.
  • Do not link to the viewer directly, and do not put the viewer address in your own pages. Always go through your script and the API call.
  • The first time a vessel is opened, a prompt about an Open Vessel Definition file can appear inside the 2D view. Choose “Don't use OVD file” to continue.

7. Going live: what TEDIVO has to set up

The viewers only appear inside your page if TEDIVO has allowed your website. Browsers refuse to show a page inside an iframe unless the page being shown says that your site may frame it. Until TEDIVO has added your website, the iframe stays empty — a grey box with a sad face icon in most browsers — even though the API call itself works.

Please send TEDIVO, before the first test:

  1. The exact addresses of the pages that will contain the iframes, with https:// and the host name, for example https://portal.example.com. If you have several — a test site, a production site — list them all. If every sub-domain needs access, say so (https://*.example.com).
  2. The kind of browsers your users use. Some browsers block the cookies the viewer needs when it runs inside another site's page; TEDIVO can check this with you.

TEDIVO adds your addresses to the allow list on all BAPLIE Viewer Online web servers (the frame-ancestors setting). Allow some time for this; it is a configuration change on each server. Get in touch to start.

This is separate from the API calls: the API calls are made from your server and do not depend on this list. A 403 with code 9994 or 9995 (section 4) is not caused by it.

8. Code samples

Complete, runnable clients that upload a file, read it back, fetch the 2D view page and delete the file again. They use only the standard library of each language. Set the environment variables first:

export BVO_API_KEY=your-api-key
export BVO_PRIVATE_KEY=your-private-key
LanguageFileRun
Python 3bvo_api.pypython3 bvo_api.py my_baplie.edi
PHP 7.4+ (curl)bvo_api.phpphp bvo_api.php my_baplie.edi
Node.js 18+bvo_api.mjsnode bvo_api.mjs my_baplie.edi
Java 11+BvoApi.javajava BvoApi.java my_baplie.edi
Shell (curl, openssl)bvo_api.shbash bvo_api.sh my_baplie.edi
Embedded viewer (PHP)embed.php + page.htmlphp -S 127.0.0.1:8080
Embedded viewer (Node.js)embed.mjsnode embed.mjs

Every sample takes the base address from BVO_BASE_URL, defaulting to https://api-gateway.baplieviewer.com/v4.

These samples run against the live API today. They use upload, read/json, view/2D and delete/file only. Each one uploads a file, reads it back, fetches the 2D view page and deletes the file again, so run them against a file you are happy to have removed.

9. Quick reference: hash recipes

Fields are listed in the order they go into the hash. The private key is always added at the very end. A dash means the call has no hash.

CallHash = sha512 of
health/status— no hash, no fields
upload, upload/loadList, upload/csvFiledata + file_id + main_json + anomalies_json
read/json, read/anomalies, read/cargoSummary, read/baplie, read/movinsfile_id
read/urlfile_upload_id + file_id
read/container, container/deleteContainer, operations/dischargeContainersfile_id + file_upload_id + container_id
container/createContainer, container/updateContainer, operations/loadContainers, operations/restowContainers, operations/correctPositionsfile_id + file_upload_id + json_data
delete/filefile_id + file_upload_id
view/2D, view/3Dfile_id + user_id
without user_id: file_id
view/dashboardfile_id
view/restrictContainersfile_id + user_id + container_ids + all_bays
share/listGuests, account/status, account/checkAccessdate
account/usagestart_date + end_date
share/assignGuestFileguest_user_id + file_id
activity/322voyage_number + vessel_name + data
activity/recordEventevent_type + voyage_number + vessel_name + container_number + equipment_status + weight + weight_qualifier + iso_code + carrier_line + temperature

If a hash is rejected and you cannot see why, check section 3.4 first — encoding and field order account for almost every case.