Bivoj Email API
This page defines the public HTTP/JSON contract used by Bivoj to send one plain-text email through Reply Pilot and create the related company, activity, local task, and Jira communication ticket.
Endpoint and authentication
POST /api/v1/bivoj/emails
Content-Type: application/json
Authorization: Bearer <BIVOJ_API_TOKEN>
The deployment must expose the backend base URL through HTTPS. This repository does not currently define a production public hostname for reply-pilot-be, so examples use REPLY_PILOT_BE_URL:
export REPLY_PILOT_BE_URL="https://<public-reply-pilot-be-host>"
export BIVOJ_API_TOKEN="<secret-token>"
The server compares the bearer token against the secret BIVOJ_API_TOKEN. Missing or invalid credentials return 401 before any email or follow-up data is created.
The sender is fixed to velkoobchody@internet-handel.cz. The request has no sender, CC, or BCC field, and accepts exactly one recipient.
Request structure
| Field | Type | Required | Contract |
|---|---|---|---|
recipient |
string | yes | Exactly one valid email address. |
subject |
string | yes | Non-empty email subject. |
text |
string | yes | Non-empty plain text. HTML or other formatting is not rendered. |
attachments |
array | no | Defaults to an empty list; at most 10 items. |
attachments[].filename |
string | yes | Non-empty filename without a path or line break. |
attachments[].content_type |
string | yes | Non-empty MIME type without a line break. |
attachments[].content_base64 |
string | yes | File bytes encoded with standard Base64. |
Attachment limits are evaluated after Base64 decoding:
- 10 MB per file
- 20 MB for all files combined
- 10 files per request
Invalid Base64 and limit violations are rejected before Gmail is called.
Example request:
{
"recipient": "buyer@acme.example",
"subject": "Wholesale offer",
"text": "Hello,\nplease find our wholesale offer attached.",
"attachments": [
{
"filename": "offer.txt",
"content_type": "text/plain",
"content_base64": "SGVsbG8gZnJvbSBSZXBseSBQaWxvdC4="
}
]
}
Processing rules
Before sending, Reply Pilot resolves the recipient company in this order:
- An existing company directly linked to the complete email address, including a company reached through an active
CONTACT_FORrelationship. - An existing company with the recipient domain.
- A newly created company for an unknown non-public domain, using the current Reply Pilot email-import rules.
For a public mailbox domain such as gmail.com, outlook.com, or seznam.cz, the complete email address must already be linked to a company. Otherwise the request returns 422 and no email is sent.
After Gmail confirms the send, Reply Pilot loads the canonical Gmail message, imports it into the activity/contact model, creates one local Email Thread Reply task for the Gmail thread, creates the corresponding Jira ticket using the existing issue-type and assignee rules, and moves it to Waiting for Reply.
Success response
Success returns HTTP 201:
{
"status": "ok",
"email_status": "sent",
"sender": "velkoobchody@internet-handel.cz",
"message_id": "19f4a25e25a12345",
"thread_id": "19f4a25e25a12345",
"company_id": 42,
"idempotent": false,
"task_id": 61,
"jira_key": "RP-4001"
}
Error and partial-result structure
Error responses use the same core envelope:
| Field | Meaning |
|---|---|
status |
error when the workflow did not confirm a sent email; partial when Gmail confirmed the send but a later step failed. |
code |
Stable machine-readable error code. |
phase |
authentication, validation, company_resolution, email_send, canonical_email, activity_import, or task_creation. |
email_status |
not_sent, sent, or unknown. |
message_id, thread_id |
Gmail identifiers when known; otherwise null. |
company_id |
Resolved Reply Pilot company ID when known; otherwise null. |
task_id, jira_key |
Currently null on errors because a failure can occur during task/Jira creation. |
idempotent |
Always false. Repeating a request can send a duplicate email. |
reason |
Human-readable explanation. |
Validation example (400):
{
"status": "error",
"email_status": "not_sent",
"sender": "velkoobchody@internet-handel.cz",
"message_id": null,
"thread_id": null,
"company_id": null,
"idempotent": false,
"code": "validation_error",
"phase": "validation",
"task_id": null,
"jira_key": null,
"reason": "recipient must be one valid email address."
}
Partial result example (502):
{
"status": "partial",
"email_status": "sent",
"sender": "velkoobchody@internet-handel.cz",
"message_id": "19f4a25e25a12345",
"thread_id": "19f4a25e25a12345",
"company_id": 42,
"idempotent": false,
"code": "task_creation_failed",
"phase": "task_creation",
"task_id": null,
"jira_key": null,
"reason": "Email was sent and linked, but the local task or Jira ticket could not be completed."
}
If the Gmail send call fails without a confirmed response, the API returns email_status: "unknown". Delivery may have occurred, so retrying can create a duplicate.
| HTTP | Typical codes |
|---|---|
400 |
invalid_json, validation_error, attachment_limit_exceeded |
401 |
unauthorized |
422 |
recipient_not_linked |
502 |
delivery_unknown, canonical_email_failed, sender_mismatch, activity_import_failed, task_creation_failed |
503 |
company_resolution_failed |
The workflow is intentionally non-atomic. Completed external or local steps are not rolled back after a partial failure.
curl examples
Send a plain-text message without attachments:
curl --fail-with-body \
--request POST \
--url "${REPLY_PILOT_BE_URL}/api/v1/bivoj/emails" \
--header "Authorization: Bearer ${BIVOJ_API_TOKEN}" \
--header "Content-Type: application/json" \
--data '{
"recipient": "buyer@acme.example",
"subject": "Wholesale offer",
"text": "Hello, this is a plain-text message.",
"attachments": []
}'
Send one attachment:
ATTACHMENT_BASE64="$(base64 < offer.pdf | tr -d '\n')"
curl --fail-with-body \
--request POST \
--url "${REPLY_PILOT_BE_URL}/api/v1/bivoj/emails" \
--header "Authorization: Bearer ${BIVOJ_API_TOKEN}" \
--header "Content-Type: application/json" \
--data @- <<JSON
{
"recipient": "buyer@acme.example",
"subject": "Wholesale offer",
"text": "Hello, please find the offer attached.",
"attachments": [
{
"filename": "offer.pdf",
"content_type": "application/pdf",
"content_base64": "${ATTACHMENT_BASE64}"
}
]
}
JSON
Local development uses http://127.0.0.1:9091 as the backend base URL.