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:

  1. An existing company directly linked to the complete email address, including a company reached through an active CONTACT_FOR relationship.
  2. An existing company with the recipient domain.
  3. 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.