Skip to content

Integrations data map

Domain: integrationsStatus: implementedReviewed: 2026-09-29

The integrations service turns external events into dependable delivery work. It currently has four practical uses:

  1. applications submit e-mail once and track its delivery through Microsoft Graph;
  2. a Hivebuy purchase-order event produces a branded supplier PDF and attaches it to the order;
  3. an approved Hivebuy supplier invoice becomes one preservation-safe machine-readable document and is e-mailed to accounting; and
  4. a Salesforce quote request produces a branded quote PDF and attaches it to the quote.

The service stores operational ledgers rather than master data. Those ledgers make duplicate events harmless, record each attempt, and stop uncertain external outcomes for human review instead of blindly repeating them.

flowchart TB
  caller["Application"]:::ext --> email_request["E-mail request"]:::work
  email_request --> email_attempt["Delivery attempt"]:::work
  email_attempt -->|"policy-checked sender"| m365["Microsoft Graph"]:::ext

  order_event["Hivebuy order event"]:::ext --> order_delivery["Verified webhook"]:::work
  order_delivery --> order_job["Purchase-order PDF job"]:::work
  order_job --> order_document["Hivebuy order documents"]:::ext

  invoice_event["Hivebuy invoice event"]:::ext --> invoice_delivery["Verified webhook"]:::work
  invoice_delivery --> invoice_job["Invoice conversion job"]:::work
  invoice_job -->|"direct send, own sender"| m365

  quote_request["Salesforce quote request"]:::ext --> quote_delivery["Authenticated webhook"]:::work
  quote_delivery --> quote_job["Quote PDF job"]:::work
  quote_job --> quote_documents["Salesforce quote PDFs"]:::ext

  classDef ext fill:#f9d6d6,stroke:#b04,color:#000
  classDef work fill:#fdf0d5,stroke:#b45309,color:#000

Send application e-mail

Use case

An ESZ application should not need its own Microsoft Graph credentials or retry logic merely to send a transactional message. It submits the message to the integrations API, receives a request ID immediately, and can use that ID to read the final delivery state. The integrations worker performs the external send.

Each authenticated caller maps to a sender policy. The policy can be disabled immediately and limits which envelope senders that caller may use. A caller can read only its own requests.

The service does not compose signatures. Mail sent through Graph passes the tenant's CodeTwo server-side rule, which appends the company signature, so callers send the body without one. A new sender that arrives without a signature is not yet covered by that rule; the fix is in CodeTwo, not here.

Request contract

Input Meaning
to, cc, bcc Recipient lists. At least one recipient across these lists is required.
reply_to Optional reply-to recipients.
sender Allowlisted envelope sender.
subject Non-empty subject, at most 255 characters.
html_body or text_body Exactly one non-empty body representation.
attachments Up to 10 named Base64 file attachments, at most 5 MB decoded in total.
metadata Optional caller context, at most 10 KB of JSON. Stored with the request but not sent to Graph.

A recipient can be a plain address such as "person@example.com", or an object with a display name:

{
  "address": "person@example.com",
  "name": "Example Person"
}

The object form is accepted in to, cc, bcc, and reply_to. Up to 100 entries are accepted in each list.

API and delivery states

Endpoint Result
POST /v1/emails/send/ Validates and persists an application/json request, queues delivery, and returns 202 Accepted.
GET /v1/emails/{request_id}/ Returns the caller's current request status.

Both endpoints accept a platform Bearer token with integrations:email:send. The configured static ingress token remains available for the platform e-mail gateway. The public status values are queued, sending, sent, failed, and review_required.

Retryable Graph or network failures are retried within the bounded task policy. A non-retryable or exhausted failure becomes failed. If a worker disappears after marking an attempt as sending, the outcome may already have happened, so the request becomes review_required for an operator decision.

E-mail records

SenderPolicy

Field Type Purpose
auth_client_id string(128), unique OAuth JWT client_id linked to this policy.
name string(100), unique Operator-facing policy name; also identifies the static-ingress policy.
is_active boolean Immediate policy-level enable or disable.
allowed_senders JSON list Exact sender addresses or permitted domain patterns.
created_at timestamp Audit time.

EmailRequest

Field Type Purpose
id UUID Public request identifier and status handle.
client SenderPolicy relation Authenticated caller policy.
feature string(128) Configured product or feature attribution.
sender e-mail address Envelope sender checked against the policy.
status enum Queued, sending, sent, failed, or review required.
payload JSON Validated message supplied to the Graph adapter.
retry_count positive integer Failed delivery attempts counted for this request.
provider_message_id nullable string(255) Provider message ID when one is available.
last_error nullable text Latest operator-visible delivery error.
created_at, updated_at, sent_at timestamps Request lifecycle.

EmailDeliveryAttempt

Field Type Purpose
email_request EmailRequest relation Parent request.
provider_request_id UUID, unique Correlation ID for this Graph attempt.
status enum Sending, sent, retryable failure, terminal failure, or unknown.
status_code nullable integer Graph HTTP status when available.
error nullable text Error for this attempt.
started_at, finished_at, updated_at timestamps Attempt timeline.

Generate a Hivebuy purchase-order PDF

Use case

When Hivebuy creates a purchase order, the supplier needs a consistent ESZ document rather than a raw webhook payload. A verified event creates one job, which renders Bestellung-{purchase_order_id}.pdf and uploads it back to the same order in Hivebuy. Repeated delivery of the same event does not create a second job or document.

The PDF contains the order number and date, supplier address, line items, totals, and printable custom fields. Certificate instructions and the certificate address are collected into their own block in postal order. Escaped \n in the certificate text becomes a real line break.

Hide an internal custom field

Custom fields normally appear when they have a printable value. To keep an order-level or item-level field internal, set its Hivebuy placeholder exactly to [[Nicht auf Bestellung PDF anzeigen]]. Surrounding whitespace is ignored. The field remains in Hivebuy and in the verified webhook payload, but is omitted from the supplier PDF. Inactive, empty, and unresolved <<ccr:...>> values are also omitted.

Webhook and upload behavior

POST /v1/webhooks/hivebuy/order-created verifies the Svix signature and queues the first event for a purchase-order ID. A valid event returns 202 Accepted; an invalid signature or body returns 400.

The company-scoped Hivebuy v2 document route returns the order's complete document list rather than only the created row. The client snapshots the IDs before upload and records success only if exactly one new ID appears afterward. It stores that ID in upload_reference and discards the short-lived signed URL.

If the upload may have happened but cannot be confirmed, the job becomes review required: retrying could attach a duplicate. Operators must inspect and, if needed, delete the document in the Hivebuy UI because the v2 API has no document deletion route.

PurchaseOrderDocumentJob

Field Type Purpose
source_system, source_id strings Hivebuy purchase-order identity; unique together with document type.
document_type enum Currently HIVEBUY_PURCHASE_ORDER.
webhook_delivery webhook relation Verified event that created the job.
status enum Queued, processing, uploaded, failed, or review required.
payload_hash, pdf_hash string(64) Input and generated PDF fingerprints.
upload_reference JSON Confirmed Hivebuy document ID.
last_error, retry_count text, integer Retry and operator-review state.
uploaded_at, created_at, updated_at timestamps Job lifecycle.

Deliver an approved supplier invoice

Use case

Accounting needs a readable invoice and machine-readable data without losing the supplier's original document. When a Hivebuy invoice reaches approved status A, the service builds one document, preserves trustworthy supplier data, adds ESZ booking context, and sends the result to the configured accounting mailbox through Microsoft Graph.

Events before approval are ignored because OCR and totals may still change. Duplicate approved events are also ignored: recovery happens from the existing job in Django admin, not by creating a second e-mail.

How the supplier document is handled

Every delivery is the same two things: a record, the machine-readable XML, and a page, the readable PDF a person looks at. A payload can arrive as a page, as a record, or as both, and the same three steps apply to all of them.

  1. The record is the supplier's own XML wherever they sent one — as a separate file, or embedded in their PDF. It keeps its syntax and its profile and none of its content is rewritten, because only the supplier states their bank details and their line detail. It is re-serialized when the identifiers are written, so comments and namespace prefixes are normalized and a signature inside the document itself does not survive. A record is authored from Hivebuy data solely when the supplier left none, and that one stays Factur-X MINIMUM so a Hivebuy misread cannot contradict the page.
  2. The page is the supplier's own PDF wherever they sent a usable one. It is forwarded as it stands and never re-rendered. A page is drawn from Hivebuy data only for a record that was authored from Hivebuy data too; a supplier's own record travels alone rather than gaining a face ESZ invented for it.
  3. The two are packaged into one hybrid PDF, the record attached to the page: factur-x.xml at the record's profile for CII, xrechnung.xml for UBL. With no page, the record is delivered on its own.

Between steps one and two the record is enriched, and a supplier UBL is reconciled against Hivebuy first. A supplier PDF counts as usable when it was fetched, is a PDF, and is small enough to forward. Packaging strips two things from it: the record it already carried, which is the un-enriched copy of the one we are attaching, and its signature fields, which a rewrite invalidates anyway. Any other file it carries, such as a timesheet the supplier attached, leaves with the invoice untouched.

Reading that against the shapes suppliers actually send:

Supplier input Record Page
PDF with embedded CII / Factur-X XML Supplier CII, enriched, at its own profile Supplier PDF
XML and PDF as separate files Supplier XML, enriched Supplier PDF
PDF with embedded UBL Supplier UBL, reconciled and enriched Supplier PDF
Standalone CII or UBL XML Supplier XML, enriched None — the record goes out alone
XML with an oversized or unusable page Supplier XML, enriched None — the record goes out alone
Plain PDF Generated Factur-X MINIMUM Supplier PDF
Oversized plain PDF Generated Factur-X MINIMUM Rendered from Hivebuy data
Missing or unavailable original Generated Factur-X MINIMUM Rendered from Hivebuy data

Supplier structured data is never silently replaced by OCR data. Unrecognized, malformed, invalid, or non-reconciling structured invoices stop for review, as does a supplier page that cannot be repackaged. The outgoing filename is based on the supplier invoice number, so that business identifier remains recognizable after conversion.

Enrichment writes two identifiers and nothing else: the ESZ supplier number as BT-29 and the Hivebuy order number as BT-10, so accounting's own supplier and order records are pre-filled and only have to be confirmed. Either is written only where the supplier left the element empty or filled it with one of the placeholders they actually send — 0, undefiniert, or a serialized None. A value they stated themselves is never overwritten, and a supplier number ESZ does not have is never invented.

The order number written as BT-10 is the joined ESZ number, taken from the linked purchase order where one is present and otherwise from the invoice's own external identifier, which is the only place a webhook states it. The vendor's own order number carried on the invoice is never sent as ours. An invoice that names no supplier at all stops for review rather than being delivered with a guessed one.

Empty CII elements are removed before validation because they can invalidate an otherwise usable supplier invoice, except for the one element the Factur-X schema requires to be present and empty.

The Factur-X MINIMUM profile has no field for BT-29, for a free-text note, or for the rest of ESZ's booking context. Where it is the profile in use, order number, requester, department, project, product group, supplier, and supplier number appear in the readable PDF or e-mail note rather than being invented as invoice XML fields.

Delivery safety

POST /v1/webhooks/hivebuy/invoice-updated uses a separate Svix secret from the purchase-order subscription. It accepts invoice and e-invoice document types, then queues only an approved invoice with a stable source identity.

The invoice worker calls Microsoft Graph directly rather than submitting an e-mail request to the service's own e-mail API. It therefore has no SenderPolicy, no request ID, and no EmailRequest status handle: the InvoiceDocumentJob row is the only delivery record, its sender is HIVEBUY_XRECHNUNG_SENDER, and its recipient is HIVEBUY_XRECHNUNG_RECIPIENT. Reading invoice delivery state means reading the job, not the e-mail ledger.

The final attachment is limited to 3 MB so Microsoft Graph accepts the request. Retryable failures use bounded retries. If a worker disappears after the job enters processing, the e-mail may already have been sent; the job becomes interrupted mid-send and bulk retry deliberately refuses it until an operator checks the mailbox.

InvoiceDocumentJob

Field Type Purpose
source_system, source_id strings Stable Hivebuy invoice identity; unique together.
webhook_delivery webhook relation Approved verified event that created the job.
status enum Queued, processing, sent, failed, review required, or interrupted mid-send.
payload_hash string(64) Verified Hivebuy payload fingerprint.
xml_hash string(64) Machine-readable invoice XML fingerprint.
original_hash string(64) Fingerprint of the supplier source used for the delivery. The source bytes are not stored here.
recipient e-mail address Accounting mailbox used for the successful delivery.
last_error, retry_count text, integer Retry and operator-review state.
sent_at, created_at, updated_at timestamps Job lifecycle.

Generate a Salesforce quote PDF

Use case

Sales prepares an quote as a Salesforce quote. A Flow on the quote asks the integrations service for the quote document; one job reads the quote, renders the quote PDF, and attaches it to the quote's native Quote PDFs list as a QuoteDocument. Every request produces a new document, so a changed quote is re-issued by requesting it again. Salesforce names and versions the documents.

This is a direct authenticated HTTP POST from Flow; Salesforce does not need Svix or a webhook subscription. The quote and purchase-order PDFs share documents/letter_a4.html for their logo, sender, recipient window, metadata placement and footer, and documents/base.css for tables and pagination. Their body content and vendor-specific job identities remain separate. Both jobs run through the shared lifecycle in lib/documents/jobs.py: a failure before the upload call fails or retries the job, an unexpected error there fails it with the error recorded, and an upload with an unknown outcome needs review. An unexpected error during the upload, or while saving a confirmed upload, also needs review with the error recorded, and still propagates to task error reporting. A job that ends failed or in review from an expected error logs integrations.documents.job-needs-attention at error level with its identifiers and status only; the error text stays on the job.

The PDF contains the quote number, date and validity, customer number and VAT ID, the handler, the customer's payment and delivery terms, the line items, the net total, and the quote owner's signature block. The date is the day the quote was created in Salesforce (German time), so a re-rendered PDF keeps its date; only a quote without CreatedDate falls back to the render date. The customer is the quote account, falling back to the contact's account; the quote billing address wins over the account billing address. BillingCountry is free text: a two-letter code prefixes the postal code (DE-85649), anything else is printed on its own line. Picklist values still set to to be mapped are left out. Delivery time and pricing basis are fixed texts until Salesforce carries them. Amounts are printed in euros because the org has no multi-currency; a zero price prints as 0,00.

The position column uses Position__c, a free-text field (maximum 10 characters) typed by sales reps. Most quotes leave it empty on every line; their main positions are then numbered 1., 2., … in print order. Once any main position carries a value, Position__c is used as typed: blank values stay blank, digit-only values get a trailing period, and other text is printed unchanged. The PDF preserves Salesforce's SortOrder, LineNumber order, even when position labels suggest another order. The article column prints the article number and description without a separate Salesforce product-name line.

A line whose Zugehoerig__c points at another line of the same quote is an alternative to it. It is printed directly below that position, marked "Alternativ", unnumbered, and left out of the net total. An alternative whose main position is missing is printed after the last position. The relationship, not the position text, determines whether a line is an alternative. If the trimmed position text is alternativ (case-insensitive) without Zugehoerig__c, the document job fails with a correction message and no PDF is rendered or uploaded. Correct the position text or set the relationship, then retry the job.

Webhook and upload behavior

The legacy /v1/webhooks/salesforce/offer-document route remains available, and either the new integrations:salesforce:quote-document scope or the legacy integrations:salesforce:offer-document scope authorizes both routes. They share the request ledger and replay protection. The German PDF title remains Angebot.

POST /v1/webhooks/salesforce/quote-document expects the JSON body {"quote_id": "<Quote.Id>", "request_id": "<Flow interview GUID>"} and a machine access token with the integrations:salesforce:quote-document scope. Salesforce sends it from a Flow HTTP Callout; its Named Credential fetches the token from the auth issuer with the client credentials grant. A valid request returns 202 Accepted; a missing or invalid token returns 401, a token without the scope returns 403, and a body that is not a JSON object or a missing or invalid request_id returns 400. The request ID must be a non-empty printable string of at most 255 characters; other content types return 415.

request_id is the idempotency key: a redelivered request is stored once and queues no second job. Reusing a request_id with a different body returns 409 and queues nothing. A malformed quote_id marks the delivery failed without a job. A quote that is missing or has no customer address fails the job without an upload; a customer or contact name alone is not an address. A quote whose line item response has done: false also fails without uploading a partial quote.

Salesforce creates the QuoteDocument in one call and returns its ID, which is stored in upload_reference. A connection that fails before the body is sent is retried. A timeout, dropped connection, or retryable error after that cannot prove the document was not stored, so the job becomes review required. A malformed creation response or a response without a document ID also requires review rather than an automatic second upload. Unexpected client errors propagate to task error reporting rather than being classified as an unverifiable upload response; the job still needs review. Operators check the quote's PDFs in Salesforce, delete a duplicate there if needed, and use the admin retry action only when no document was attached.

Configure the Flow callout and its credentials using the operations procedure.

QuoteDocumentJob

Field Type Purpose
quote_id string(18) Salesforce quote the document belongs to.
webhook_delivery webhook relation Request that created the job; one job per request.
status enum Queued, processing, uploaded, failed, or review required.
quote_number string Quote number printed on the PDF.
pdf_hash string(64) Generated PDF fingerprint.
upload_reference JSON Created QuoteDocument ID.
last_error, retry_count text, integer Retry and operator-review state.
uploaded_at, created_at, updated_at timestamps Job lifecycle.

The request ledger uses the same WebhookDelivery shape as the Hivebuy workflows below; svix_message_id holds the Salesforce request_id.

Shared Hivebuy webhook ledger

Purchase orders and invoices use separate WebhookDelivery tables because they are separate workflows and secrets, but both tables share the same shape and replay rules.

Field Type Purpose
provider string(64) Webhook provider key.
svix_message_id string(255) Signed message ID; unique with provider for replay protection.
event_type string(255) Hivebuy event name when the payload provides one.
payload_hash string(64) Fingerprint of the verified raw body.
payload JSON Verified original event used by the job.
status enum Received, ignored, queued, or failed.
last_error text Processing failure visible to operators.
processed_at, created_at, updated_at timestamps Receipt and processing timeline.

The Svix message ID rejects transport redelivery. The source identity on each document job provides a second idempotency boundary when Hivebuy sends the same business event under a different message ID.