BERTI GRAFINTEGRATION CONSOLE
Checking service…

SERVER CONNECTION

Read the guide without signing in

An API key is required to view orders or change data. It stays in this tab's memory.

Bertigraf NL · account bertigraf-nl. Stock and mappings are live. Mail submission is available for the approved NL API account. New NL order creation and confirmation are disabled.

DEVELOPER HANDBOOK · V2.0.1

A small API.
A clear order journey.

Connect website products to JTL stock, receive orders, and follow acceptance, dispatch and delivery.

ENGLISH DOCUMENTATION
01

Map a product

Save the website product or variation ID, JTL article ID and quantity factor.

02

Read available stock

Get one combined quantity from JTL NL warehouse 12, converted to website units.

03

Follow every update

Post versioned status events. Read the timeline, carrier and tracking numbers.

Start here

Base URL: https://backend.bertigraf.nl. Send Authorization: Bearer YOUR_TOKEN with API calls and Content-Type: application/json with POST requests. Production websites must call the API from their server. This operator console accepts a key explicitly; it never saves it to local storage.

Website acceptance and fulfilment events are saved in the Operations inbox. Linking an existing JTL order adds live, read-only native carrier and dispatch information. Native JTL orders are created through SQL once confirmation and mappings allow it.

Website product fields

In WooCommerce, add the following custom fields to Products → Edit product → Product data, or to each sellable variation. Use the existing inventory SKU field. Your website developer must add these custom inputs and call the mapping endpoint when saving them.

FieldWebsite storageAPI field
Product / variation IDExisting WooCommerce IDproductId
SKUExisting inventory SKUsku
JTL article ID_bertigraf_jtl_article_idarticleId (kArtikel)
JTL units per website unit_bertigraf_jtl_factorfactor
Safety buffer, in JTL units_bertigraf_jtl_bufferbuffer
Sales unit_bertigraf_jtl_unitunit

Example factor 2.5: ordering 3 website units consumes 7.5 JTL units. This is an illustration; enter the verified factor for each actual product. Save the returned mapping revision and send it with the next edit. Use 0 to create a mapping.

ONE-TIME APPROVAL

Automatic access-token renewal

An administrator approves the website server once and supplies a private client_id and client_secret. This client grant stays valid until explicitly revoked. Access tokens expire after one hour; the website obtains new tokens by authenticating with its approved client credentials. No repeated human approval is needed, even after an access token expires.

POST https://backend.bertigraf.nl/api/v1/auth/token
Content-Type: application/json

{
  "grant_type": "client_credentials",
  "client_id": "YOUR_APPROVED_CLIENT_ID",
  "client_secret": "YOUR_PRIVATE_CLIENT_SECRET"
}

The endpoint also accepts application/x-www-form-urlencoded. No Bearer token is required for this exchange; the client credentials authenticate it. The response contains access_token, token_type: Bearer, expires_in: 3600 and the fixed account/region scope.

  1. Store the approved client credentials in the website server's private configuration, outside the public web root. Never embed the client secret in frontend JavaScript.
  2. Cache the access token and expiry. Renew about two minutes before expiry; share the cache with a lock across workers so each request does not issue a new token.
  3. Send the cached token as Authorization: Bearer ACCESS_TOKEN with business API requests.
  4. On an expired-token 401, get a new token and retry the business request once, keeping its original idempotency/event IDs. A revoked client receives 401 from the token endpoint too; stop and contact the administrator.

A newly issued token shortens older live tokens for the same client to at most five more minutes, without extending an existing expiry. The server allows one issuance per client every 15 seconds and 30 token requests per minute across this service. A 429 response includes Retry-After; reuse the cached token or back off. Client revocation immediately disables renewal and all access tokens issued to that client. Independent legacy keys are separate and retain their original expiry.

Download the PHP / WooCommerce server-side helper. It caches tokens under a file lock, renews automatically and retries a 401 once. Keep its cache outside the public web root. Configure BERTIGRAF_CLIENT_ID and BERTIGRAF_CLIENT_SECRET on the website server once. The helper never needs an administrator password.

There is no public self-approval endpoint. Grant creation and revocation remain administrator actions. An expired legacy Bearer key cannot approve a new client. This public guide never contains real client credentials.

Required customer and addresses

Every new POST /api/v1/orders requires customer, billingAddress and shippingAddress. Missing, blank, wrong-type or invalid values return 422 before saving. Send both addresses even when identical.

customer requires externalCustomerId (stable website customer ID, including for guests), firstName, lastName, email and phone. Each address requires firstName, lastName, street (including house number where applicable), postalCode, city and countryCode (assigned uppercase ISO alpha-2, for example DE or PL). Company name and VAT ID are optional business metadata. Optional address fields: companyName, addressLine2, state.

Customer and address snapshots are returned with the order and do not change with status events. Older receipts can omit them. New customer identities are created in native JTL with their billing address, email, phone and business metadata. Returning customers reuse the mapping keyed by account, region and externalCustomerId. Never change that ID between orders; guest checkout also needs a stable ID. Email alone is not used for merging. Existing JTL master data is preserved; each order keeps its own current customer and address snapshots. Shipping addresses remain order snapshots. Customer creation and receipt storage commit atomically. The response includes jtlCustomer.id, customerNumber and created (whether the initial acceptance created the customer). New v1.9 receipts automatically create and link native JTL orders through SQL. Older pre-integration customers require an administrator to establish their website-ID mapping before first use to avoid a separate identity. Access requires an authorized account token.

{
  "customer": {
    "externalCustomerId": "WEB-CUSTOMER-123",
    "firstName": "Anna",
    "lastName": "Example",
    "email": "[email protected]",
    "phone": "+49 30 1234567"
  },
  "billingAddress": {
    "firstName": "Anna",
    "lastName": "Example",
    "street": "Example Street 12",
    "postalCode": "10115",
    "city": "Berlin",
    "countryCode": "NL"
  },
  "shippingAddress": {
    "firstName": "Anna",
    "lastName": "Example",
    "street": "Example Street 12",
    "postalCode": "10115",
    "city": "Berlin",
    "countryCode": "NL"
  }
}

Customer context across order endpoints

GET /orders, GET /orders/{id}, POST /orders/{id}/confirm, POST /orders/{id}/events (acceptance, rejection, cancellation, processing, shipping, delivery and completion), and POST /orders/{id}/jtl-link all return the saved customer, billingAddress, shippingAddress and jtlCustomer when available. These operations use the original order identity and never create another customer.

You do not need to resend customer data after order creation. To verify identity, optionally include customer: {"externalCustomerId":"WEB-CUSTOMER-123"}. If customer is supplied, externalCustomerId is required. Additional customer or address fields may be supplied only when they exactly match the saved order snapshot. Mismatches return 409; malformed references and attempts to write jtlCustomer return 422. Nothing is changed on failure. These endpoints do not edit customer profiles or delivery addresses. A status-event retry must still use the exact same request body and eventId.

Older receipts recover customer/address objects from their original payload when available. Missing legacy data and native customer links are not invented, and reads do not create JTL customers. Closed orders still reject every POST.

Automatic JTL orders via SQL

POST /orders with requireConfirmation omitted or false creates the customer, native sales order, both addresses, article positions, BOM components, native totals and reservations in one SQL transaction. Physical stock does not change at reception. No JTL REST API is used.

With requireConfirmation:true, reception stores the customer and receipt first. POST /orders/{id}/confirm or an accepted event creates the native order. Unmapped orders remain held. Insufficient stock in NL warehouse 12, unsupported BOMs, native validation or total mismatches roll back the entire current operation and return 422. Pre-v1.9 receipts are not retroactively imported.

Retry the same externalOrderId, Idempotency-Key and unchanged body. Open identical retries return the original native ID; different content/key returns 409. SQL locks and a unique permanent native receipt prevent concurrent duplicates. If the connection drops after commit, replay the unchanged request or GET the order. A closed order rejects every POST with 409; use GET to inspect it.

Native order number: BW-<receipt GUID>. Native external reference: BGW-<receipt GUID>. The website ID is saved as the native customer order number. Receipt GUIDs isolate accounts/regions; standard numbering counters are unchanged.

Rejection/cancellation of API-created native orders follows JTL cancellation validation and releases reservations; a non-cancellable stage returns 409. Website shipping/delivery/completion events do not dispatch stock or generate native delivery notes. This SQL integration does not invoke application-level order-created workflows or generate invoices, payments, emails or shipping labels.

Email API

POST /api/v1/mail/messages and GET /api/v1/mail/messages/{mailId} use the same Bearer token as the website integration. Your API account also needs explicit mail permission; the existing bertigraf-nl / DE account is approved. Sender is always [email protected]. Keep SMTP credentials only on the API server, never on the website frontend.

POST /api/v1/mail/messages
Authorization: Bearer ACCESS_TOKEN
Idempotency-Key: website-contact-10001
Content-Type: application/json

{
  "to": ["[email protected]"],
  "subject": "Website contact request",
  "textBody": "A customer has submitted a website enquiry.",
  "replyTo": "[email protected]"
}

The response is HTTP 202 with mailId, statusUrl, and one status per recipient. This confirms queue storage, not delivery. Poll the status URL with the same Bearer token. Reusing the same Idempotency-Key and unchanged content returns HTTP 200 with the same ID without resending. Reusing a key for different content returns 409. Use a new key only for an intentionally new email.

to: 1–10 unique plain email addresses. subject: 1–200 characters. Provide non-blank textBody (up to 20,000 characters), htmlBody (up to 40,000), or both. Optional replyTo. Whole JSON request: 64 KiB maximum. Sender overrides, display-name recipients, attachments, cc and bcc are unsupported. Default account limits: 30 new messages/minute and 500 recipient deliveries/rolling 24h; 429 includes Retry-After. Idempotent replays do not consume new quota.

StatusMeaning
queued / preparing / submittingWaiting, connecting securely, or submitting to STRATO.
smtp_acceptedSTRATO acknowledged SMTP acceptance. Final recipient delivery is not confirmed.
deliveredA correlated positive delivery report or verified receipt in our authenticated INBOX confirms delivery. This does not mean read or guaranteed inbox placement at another provider.
bounced / failedReported final delivery failure / permanent submission failure.
delayedA matched report says delivery is delayed. The API does not resend it.
unknownThe SMTP connection or worker stopped during submission. To avoid duplicates, automatic resend is stopped; inspect the status and mailbox before intentionally submitting a new message.
waiting_configurationMessage is durably stored but sending is disabled or credentials are missing.
mixedRecipients have different states; inspect the recipients array.

Messages and recipient history survive service restarts. Known pre-submission failures retry with backoff, up to five failed connection/submission attempts. An uncertain post-submission result never automatically retries. deliveryConfirmed becomes true only when every recipient is confirmed delivered. deliveryEvidence identifies mailbox_receipt or delivery_report. No tracking pixels or opened/read status are used.

STRATO authentication was verified using smtp.strato.de:587 with mandatory STARTTLS; the VPS could not connect on port 465. TLS and certificate validation are required, with no plaintext fallback. This STRATO server does not advertise positive SMTP DSN support. External recipients may not send delivery reports, so a successfully submitted external message can remain smtp_accepted indefinitely. Failure reports are matched when available. Only delivery-report messages and known IDs addressed to our own mailbox are examined through read-only IMAP; messages are not marked read or deleted.

Endpoints & request examples

Status rules

Received→Accepted→Processing→Shipped→Delivered→Completed
  • Received: accept, reject, or cancel. Acceptance requires resolved product mappings.
  • Accepted: start processing, reject, or cancel.
  • Processing: ship or cancel. Shipping requires carrier and tracking number for every package.
  • Shipped: mark delivered. Delivered: mark completed.
  • Completed, rejected and cancelled: permanently closed to every POST, including identical retries of creation, confirmation, linking and status events. Read with GET after a network timeout.

Each status event needs a unique eventId and the latest expectedVersion. A stale version or invalid transition returns 409. An identical event retry replays safely while the order is open; reusing its ID for different content returns 409. Rejection and cancellation require a reason.

The original status field describes integration processing. Use orderStatus for the website order journey, closed to lock editing, version for updates and allowedTransitions to build your UI. Check jtlSubmitted, jtlOrderId and jtlImport.status for native creation.

Carrier, tracking and JTL

Send one shipment per package in an order event. A carrier can be planned during acceptance; provide tracking numbers when shipping. Fields: shipmentId, carrier, trackingNumber, optional HTTPS trackingUrl. The shipment array replaces the previous array, so always send all packages.

New SQL-created orders are linked automatically. For an older receipt whose importer already created the native order, link it with POST /orders/{id}/jtl-link. JTL's external order reference must exactly match the website ID. One JTL order can only be linked once. GET order details then reads the planned JTL carrier and all native parcels. jtl.status is separate from partner-reported delivery events: dispatch is not proof of delivery. Raw JTL status codes are returned without guessing their meaning. Native dates use the JTL server's local time; API timestamps use UTC. If JTL cannot be verified, updates to linked orders fail closed with 503. A natively cancelled order cannot receive further updates.

Stock & integration checklist

Only NL warehouse ID 12 enter the physical stock pool. Blocked stock and aggregate open-order demand are deducted conservatively, with a global JTL availability cap. BOM components use the same pool. Website units = floor(max(0, available − buffer) / factor). A missing or unavailable source returns quantity: null; do not treat it as a fresh zero. Stock reads do not reserve stock.

  • Keep client credentials private on your server and never commit them. Approved clients renew access tokens automatically; revoke the grant when access should end.
  • Use the same Idempotency-Key and unchanged body when retrying order creation.
  • Poll GET order details for progress. No webhook delivery is configured.
  • 409 means conflict or a closed order; GET current state before deciding what to do.
  • 401: expired/invalid key. 403: account mismatch. 404: unknown order/product. 422: invalid input. 503: source unavailable; retry reads with backoff.
  • This API owns native creation for new v1.9 orders. Disable competing importers for the same website orders.