One API. No subscription.
The document service works independently of remittance. Use sandbox credentials to test, then production credentials to generate real invoice documents. Production credentials do not enable real payments.
All requests and responses use JSON over HTTPS. Dates use ISO 8601 and server timestamps are UTC. Your customer views display their local timezone.
Use Invoice APIAuthenticate from your server.
- Verify your email in the business workspace.
- Add your business name and country.
- Create a sandbox or production credential.
- Save the secret immediately. It is shown only once.
X-API-Key: br_test_... X-API-Secret: secret_... Content-Type: application/json Accept: application/json
Send both headers with every API request. Store the secret in a server environment variable. Never embed it in a public website, mobile app or source repository. Revocation takes effect immediately.
| Scope | Allows |
|---|---|
invoices:create | Create and cancel invoices |
invoices:read | List and retrieve your invoices |
invoices:send | Request invoice email delivery |
payment_requests:create | Include an optional payment request |
webhooks:manage | Reserved for future machine-managed endpoint configuration; manage endpoints in your verified workspace today. |
Your first invoice
Replace the sender email with the email you verified. Send amounts and quantities as decimal strings. Use a new external reference for every new invoice.
curl
curl -X POST 'https://api.bremit.online/api/v1/invoices' \
-H "X-API-Key: $BREMIT_API_KEY" \
-H "X-API-Secret: $BREMIT_API_SECRET" \
-H 'Idempotency-Key: order-778812-create-v1' \
-H 'Content-Type: application/json' \
--data '{
"reference": "ORDER-778812",
"currency": "UGX",
"issue_date": "2026-09-26",
"sender": {
"name": "Your Business",
"email": "your-verified-email@example.com",
"tin": "123456789"
},
"recipient": {
"name": "Your Customer",
"email": "customer@example.com"
},
"items": [
{
"description": "Application support",
"quantity": "2",
"unit": "hours",
"unit_price": "75000",
"discount": {
"type": "percentage",
"value": "10"
}
}
],
"discounts": [
{
"type": "fixed",
"value": "5000"
}
],
"taxes": [
{
"name": "VAT",
"rate": "18",
"inclusive": false
}
],
"output": {
"formats": [
"pdf",
"html"
]
},
"delivery": {
"email_recipient": false,
"email_sender": false
},
"payment": {
"enabled": false
}
}'JavaScript · Node.js server
const response = await fetch('https://api.bremit.online/api/v1/invoices', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.BREMIT_API_KEY,
'X-API-Secret': process.env.BREMIT_API_SECRET,
'Idempotency-Key': 'order-778812-create-v1'
},
body: JSON.stringify(invoice) // use the JSON example above
});
const result = await response.json();
if (!response.ok) throw new Error(result.message);
console.log(result.bremit_invoice_number, result.pdf_url);PHP
$invoice = json_decode(file_get_contents('invoice.json'), true);
$ch = curl_init('https://api.bremit.online/api/v1/invoices');
curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-API-Key: '.getenv('BREMIT_API_KEY'),
'X-API-Secret: '.getenv('BREMIT_API_SECRET'),
'Idempotency-Key: order-778812-create-v1'
],
CURLOPT_POSTFIELDS => json_encode($invoice, JSON_THROW_ON_ERROR)
]);
$body = curl_exec($ch);
if ($body === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$result = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
if ($status >= 400) throw new RuntimeException($result['message']);Python · standard library
import json, os, urllib.request, urllib.error
with open('invoice.json') as file:
invoice = json.load(file)
request = urllib.request.Request(
'https://api.bremit.online/api/v1/invoices',
data=json.dumps(invoice).encode(),
headers={
'Content-Type': 'application/json',
'X-API-Key': os.environ['BREMIT_API_KEY'],
'X-API-Secret': os.environ['BREMIT_API_SECRET'],
'Idempotency-Key': 'order-778812-create-v1'
}, method='POST'
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
result = json.load(response)
print(result['bremit_invoice_number'])
except urllib.error.HTTPError as error:
print(error.code, error.read().decode())Create an invoice
POST /invoicesRequires invoices:create and an Idempotency-Key header (8–120 letters, digits, periods, underscores, colons or hyphens).
| Field | Rules |
|---|---|
reference | Required, at most 120 characters. Unique within your account and environment. Bremit generates its own sequential invoice number. |
currency | Required. UGX, KES, AED, USD, EUR, GBP, SAR, TZS, RWF or KWD. UGX/RWF use zero decimal places; KWD uses three; others use two. |
issue_date / due_date | YYYY-MM-DD. Issue date required; due date optional and not before issue date. |
sender | Required name and verified email. Optional address, city, country, phone, website, tax_identifier, tax_identifier_type, tin, vat_number, registration_number, logo, contact_person and notes. |
recipient | Required name. Email needed only for recipient delivery. Optional address, city, country, phone, tax_identifier, tax_identifier_type, tin, vat_number, registration_number, customer_number, contact_person. |
items | 1–100 items. Required description (up to 1,500 characters), quantity and unit_price. Optional unit, discount and taxes. Quantity must be positive, up to 100,000 and four decimal places. |
discount / discounts | Line discount: {type: "fixed" or "percentage", value: "10"}. Invoice discounts: up to five objects using the same fields. |
taxes | Up to ten named taxes at invoice or line level. Use name, rate and inclusive; optional tax_number. A fixed invoice tax uses amount instead of rate and must be exclusive. |
charges | Up to ten objects with name and amount. Charges are added after taxes. |
notes / terms / footer | Optional plain text. Notes and terms up to 3,000 characters; footer up to 1,000. HTML is escaped. |
sender.logo | Optional base64 data URL containing PNG, JPEG or WebP. Maximum decoded 1 MB and four megapixels. Remote URLs and SVG are rejected. |
output.formats | ["pdf"], ["html"] or ["pdf","html"]. Both by default. |
payment | Optional. Omit or set enabled:false for a document-only invoice. See Payment links. |
How totals are calculated
Quantity × unit price is rounded to the currency’s minor unit, half up. Line discounts apply first, then invoice discounts in order. Invoice discounts are allocated proportionally across lines. Inclusive percentage taxes are extracted from the discounted amount; exclusive taxes are added to the same tax base. Fixed invoice taxes and additional charges are added last. Taxes are not compounded.
Calculated totals come from Bremit. Caller-supplied totals are not accepted as authoritative. An invoice must have a positive total. Blank optional fields produce no empty labels.
Successful response · 201
{
"id": "invoice-uuid",
"bremit_invoice_number": "BRINV-2026-000001",
"external_reference": "ORDER-778812",
"environment": "sandbox",
"status": "ISSUED",
"currency": "UGX",
"amount": "153400",
"total_minor": 153400,
"pdf_url": "https://bremit.online/api/v1/documents/.../pdf?expires=...&signature=...",
"html_url": "https://bremit.online/api/v1/documents/.../html?expires=...&signature=...",
"urls_expire_at": "2026-09-27T12:00:00Z",
"payment_status": "NOT_ENABLED",
"payment_request": null,
"receipt": null,
"deliveries": []
}Document links are signed and expire. Retrieve the invoice again to obtain fresh links. Treat these links as private: anyone holding a valid link can view the document. Issued invoice content is immutable; create a new invoice with a new reference if a correction is required.
Retrieve, filter and cancel
GET /invoices
GET /invoices/{bremit_invoice_number}
POST /invoices/{bremit_invoice_number}/cancelList filters: date_from, date_to, status, external_reference, recipient, currency, payment_status and page. Dates use YYYY-MM-DD. Lists contain 25 invoices and return current_page, last_page and total. Credentials can only access their own account and environment.
Cancellation requires invoices:create and refuses paid invoices or payments in progress. Payment links are cancelled with the invoice. Statuses include ISSUED, SENT, VIEWED, PAID, OVERDUE and CANCELLED. DRAFT, PARTIALLY_PAID and VOID are reserved; partial payment is not exposed in this version.
Email the invoice
{
"delivery": {
"email_recipient": true,
"email_sender": true,
"attach_pdf": true,
"email_subject": "Your invoice is ready",
"email_message": "Thank you for working with us."
}
}Both booleans false means return only. Either or both can be true. Subject is plain text, maximum 150 characters, with no line breaks. Message is plain text, maximum 2,000 characters. The verified sender email is required to prevent impersonation. Sender and recipient are deduplicated when identical.
POST /invoices/{bremit_invoice_number}/send
{"email_recipient":true,"email_sender":false,"attach_pdf":true}Requires invoices:send. Resending uses the invoice’s saved recipient and message; it cannot become an arbitrary mailing API. Sandbox deliveries are simulated. Production emails are queued. A sent status means the mail server accepted the message, not that it reached the inbox. Safety limits apply per business and recipient; wait at least five minutes before resending.
Optional payment links
Payment links are currently available only in the sandbox. Real mobile-money payments remain disabled while provider integrations are being completed. Document-only invoices can use production credentials independently.
{
"payment": {
"enabled": true,
"corridor_id": 1,
"recipient": {
"name": "Account holder",
"account": "+256770000001",
"method": "MTN Mobile Money"
},
"expires_minutes": 1440
}
}Requires payment_requests:create in addition to invoice scopes. The invoice currency must match the route’s sending currency and amount limits. The final transfer quote shows the sending amount, fees and converted recipient amount. A payment link expires after 15, 30, 60, 360, 720, 1440 or 2880 minutes, independently of the invoice due date.
The response includes a payment_request with payment_url and expires_at. Its QR encodes only that URL. Completion changes the invoice to PAID and creates a sequential receipt. Sandbox receipts explicitly state that no money moved. Expiry never deletes the invoice.
Renew an unpaid link with POST /invoices/{bremit_invoice_number}/payment-request, supplying corridor_id, recipient and expires_minutes as above. The old link is cancelled. Paid invoices and payments already processing cannot be renewed.
Payment states: NOT_ENABLED, ACTIVE, OPENED, PROCESSING, PAID, FAILED, EXPIRED and CANCELLED.
Receive signed events
Add a public HTTPS endpoint in your business workspace. Save the signing secret when it is shown. Endpoints on local networks, private IPs or Bremit infrastructure are rejected. Redirects are not followed.
Events: invoice.created, invoice.sent, invoice.viewed, invoice.payment_request.created, invoice.payment.started, invoice.paid, invoice.failed, invoice.expired and receipt.created.
X-Bremit-Event-ID: event-uuid X-Bremit-Signature: t=unix-seconds,v1=hex-hmac-sha256
Verify the HMAC over timestamp + "." + raw_request_body using your webhook secret. Reject timestamps outside a five-minute tolerance. Compare signatures in constant time. Store each event ID and ignore duplicates before applying changes. Do not parse and re-serialize JSON before verifying it.
// Node.js server: rawBody must be the original request bytes.
import { createHmac, timingSafeEqual } from 'node:crypto';
const parts = Object.fromEntries(signatureHeader.split(',').map(p => p.split('=')));
if (!/^\d+$/.test(parts.t || '') || !/^[a-f0-9]{64}$/.test(parts.v1 || ''))
throw new Error('Malformed signature');
if (Math.abs(Date.now()/1000 - Number(parts.t)) > 300)
throw new Error('Expired signature');
const expected = createHmac('sha256', process.env.BREMIT_WEBHOOK_SECRET)
.update(parts.t + '.').update(rawBody).digest();
if (!timingSafeEqual(expected, Buffer.from(parts.v1, 'hex')))
throw new Error('Invalid signature');
const event = JSON.parse(rawBody.toString('utf8'));
// Atomically deduplicate event.id, apply it, then return HTTP 2xx.Return a 2xx status within eight seconds and keep the response under 64 KB. Failed events receive up to five automatic attempts with increasing delays. You can retry failed deliveries in the workspace, up to eight total attempts. Delivery is at least once; order is not guaranteed. Fetch the invoice for its latest state.
Errors and safety limits
| Status | Meaning |
|---|---|
| 401 | Credentials missing, invalid or revoked. |
| 403 | Missing scope, suspended access, or an expired document link. |
| 404 | Resource missing or not owned by your account/environment. |
| 409 | Reference or idempotency conflict; action incompatible with current status. |
| 422 | Validation failed. See message and errors for fields to correct. |
| 429 | Safety limit reached. Slow down and retry later. |
| 503 | Email or real payments unavailable; use return-only documents or sandbox payments. |
Initial business limits are 60 requests per minute, 5,000 per day and 20 invoice emails per day. Administrators may adjust them for legitimate usage. Shared IP burst limits and recipient safeguards also apply. These protect a free service from abuse; they are not subscription tiers.
Retry without creating duplicates.
Generate one idempotency key for each intended invoice. If the request times out, resend the same body and key. A successful replay returns the original invoice. A changed body with the same key returns 409. Another key using an existing external reference also returns 409. Reusing a creation request does not resend invoice emails.
Service availability
Invoice documents: available when this release is enabled. Email: requires configured SMTP and a production credential. Payments: sandbox only. No cards, wallets or cross-provider routing are available in this version.

