Integrations data map¶
The integrations service turns external events into dependable delivery work. It currently has four practical uses:
- applications submit e-mail once and track its delivery through Microsoft Graph;
- a Hivebuy purchase-order event produces a branded supplier PDF and attaches it to the order;
- an approved Hivebuy supplier invoice becomes one preservation-safe machine-readable document and is e-mailed to accounting; and
- 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.
- 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.
- 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.
- The two are packaged into one hybrid PDF, the record attached to the
page:
factur-x.xmlat the record's profile for CII,xrechnung.xmlfor 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.