Dev Portal

The Developer API lets third-party systems integrate with the Invoicely platform — manage customers, create and read invoices, and look up reference codes. All endpoints are versioned under /v1/external.

Try it

Call the API straight from this page. Enter your sandbox API key and secret, authenticate to get a bearer token, then pick an example request (or edit the method, path, and body) and send it. Responses are shown live, exactly as your integration will receive them.

Try it — live API console
Use test credentials only. Your API secret is sent directly from this browser to the API — never paste a production secret here. Requests run against the base URL below and may create real data in that environment.
Tip: authenticate first — every endpoint except the token exchange needs the bearer token.

Authentication

Authentication is a two-step flow:

  1. Exchange your API credentials for an access token. Your merchant API credentials — generated from the API Keys page in your merchant portal — are presented once to the token endpoint.
  2. Call every other endpoint with that token as a Bearer token.

This keeps your long-lived secret off the wire on every request; a leaked access token expires on its own within a couple of hours.

Get an access token

POST/v1/external/auth/token

Send your credentials as headers (no request body):

X-API-Key:     pk_live_your_api_key
X-API-Secret:  sk_live_your_api_secret

Response

{
  "accessToken": "eyJhbGciOiJIUzUxMiJ9...",
  "tokenType": "Bearer",
  "expiresIn": 7200,
  "expiresAt": "2026-05-20T13:30:00Z"
}

The token is valid for 2 hours (expiresIn is in seconds). Request a new one when it expires — a 401 on any endpoint means the token is missing, invalid, or expired.

Use the access token

Send it on every other /v1/external/** request:

Authorization: Bearer eyJhbGciOiJIUzUxMiJ9...
Content-Type:  application/json

Every request is automatically scoped to the organisation that owns the API key — you never pass an organisation id, and you can only ever see or modify your own data.

SecurityNever expose your API Secret in client-side code, mobile apps, or public repositories. Use it only from your backend server, and only against the token endpoint. Treat the access token as a short-lived secret too: keep it server-side and never embed it in a browser or mobile client.

Managing API keys (merchant portal)

These endpoints are used by signed-in portal users (JWT), not by the API key.

MethodPathDescription
POST/v1/organisations/api-keys/{organisationId}/generateGenerate / regenerate the key pair
GET/v1/organisations/api-keys/{organisationId}Read the public key
DELETE/v1/organisations/api-keys/{organisationId}Revoke the key pair

Generate response — the secret is shown once and is stored only as a hash; it can never be retrieved again. Regenerate if it is lost (this invalidates the previous pair).

{
  "organisationId": 42,
  "apiKey": "pk_live_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
  "apiSecret": "sk_live_9z8y7x6w5v4u3t2s1r0q9z8y7x6w5v4u3t2s1r0q9z8y7x6w",
  "warning": "Store the API secret now. It is shown only once and cannot be retrieved again. Regenerate if it is lost."
}

Errors

Errors use a consistent JSON shape:

{
  "timestamp": "2026-05-20 11:30:00",
  "status": 400,
  "message": "email is required"
}
StatusMeaning
400Validation failed / malformed request
401Invalid X-API-Key/X-API-Secret, or a missing/invalid/expired access token
404Resource not found, or it does not belong to your organisation
500Unexpected server error

Reference codes

List HS codes

GET/v1/external/hs-codes
GET/v1/external/hs-codes/search?query=horses

Get one HS code

GET/v1/external/hs-codes/{code}
{ "code": "01012900", "description": "Live horses,nes" }

List currencies

GET/v1/external/currencies

Returns ["USD", "ZWG"].

List tax codes

GET/v1/external/tax-codes

The tax codes applicable to your organisation. Use these values on fiscal invoice line items.

[
  {
    "taxID": 517,
    "taxName": "Standard rated 15.5%",
    "taxPercent": 15.5,
    "taxValidFrom": "2024-01-01",
    "taxValidTill": null
  }
]

Customers

Create a customer

POST/v1/external/customers
{
  "contactPerson": "Jane Doe",
  "companyName": "Acme Pvt Ltd",
  "email": "jane@acme.co.zw",
  "phone": "+263772000000",
  "buyerTIN": "1234567890",
  "vatNumber": "220123456",
  "customerType": "BUSINESS",
  "province": "Harare",
  "city": "Harare",
  "street": "Samora Machel Ave",
  "houseNo": "12",
  "district": "Harare Central"
}

customerType is BUSINESS or INDIVIDUAL. buyerTIN, when supplied, must be 10 digits.

Response

{
  "id": 501,
  "contactPerson": "Jane Doe",
  "companyName": "Acme Pvt Ltd",
  "email": "jane@acme.co.zw",
  "phone": "+263772000000",
  "buyerTIN": "1234567890",
  "vatNumber": "220123456",
  "customerType": "BUSINESS",
  "province": "Harare",
  "city": "Harare",
  "street": "Samora Machel Ave",
  "houseNo": "12",
  "district": "Harare Central"
}

List customers

GET/v1/external/customers?page=0&size=20

Paged, most recent first.

Get a customer

GET/v1/external/customers/{customerId}

Invoices (standard)

Create an invoice

POST/v1/external/invoices
{
  "customerId": 501,
  "salesPerson": "John Banda",
  "subject": "Website development",
  "invoicingDate": "2026-05-20",
  "dueDate": "2026-06-03",
  "terms": "DUE_ON_RECEIPT",
  "customerNote": "Thank you for your business",
  "termsAndConditions": "Payment due within 14 days",
  "totalAmount": 1500.00,
  "currency": "USD",
  "items": [
    {
      "itemDescription": "Design & build",
      "quantity": 1,
      "price": 1500.00,
      "totalAmount": 1500.00
    }
  ]
}

Response — includes the generated invoiceNumber, status, and line items.

{
  "id": 9001,
  "invoiceNumber": "0009001",
  "customerId": 501,
  "salesPerson": "John Banda",
  "subject": "Website development",
  "status": "UNPAID",
  "paid": false,
  "currency": "USD",
  "totalAmount": 1500.00,
  "invoicingDate": "2026-05-20",
  "dueDate": "2026-06-03",
  "customerNote": "Thank you for your business",
  "termsAndConditions": "Payment due within 14 days",
  "items": [
    { "id": 1, "itemDescription": "Design & build", "quantity": 1, "price": 1500.00, "totalAmount": 1500.00 }
  ]
}

List invoices

GET/v1/external/invoices?page=0&size=20&customerId=501

Paged; customerId is optional. List responses omit items.

Get an invoice

GET/v1/external/invoices/{invoiceId}

Full invoice including items.

Edit an invoice

PUT/v1/external/invoices/{invoiceId}

Updates an existing invoice. The body is the same shape as create, without items — editing an invoice does not change its line items. Returns the updated invoice (with items). 404 if the invoice does not belong to your organisation.

{
  "customerId": 501,
  "salesPerson": "John Banda",
  "subject": "Website development (revised)",
  "invoicingDate": "2026-05-20",
  "dueDate": "2026-06-10",
  "terms": "DUE_ON_RECEIPT",
  "customerNote": "Updated scope",
  "termsAndConditions": "Payment due within 14 days",
  "totalAmount": 1800.00,
  "currency": "USD"
}

Fiscal invoices (ZIMRA)

Fiscal invoices are submitted to ZIMRA. Your organisation must have a configured fiscal device and certificate; otherwise creation fails.

Create a fiscal invoice

POST/v1/external/fiscal-invoices
{
  "customerId": 501,
  "salesPerson": "John Banda",
  "subject": "Consulting services",
  "invoicingDate": "2026-05-20",
  "customerNote": "Thank you",
  "currency": "USD",
  "taxIncludedInProductPrice": true,
  "items": [
    {
      "itemDescription": "Advisory retainer",
      "quantity": 1.0,
      "taxId": 517,
      "taxName": "Standard rated 15.5%",
      "taxPercent": 15.5,
      "hsCode": "99001000",
      "price": 1000.00,
      "totalAmount": 1000.00
    }
  ]
}

Obtain valid hsCode values from /v1/external/hs-codes. Tax identifiers (taxId, taxName, taxPercent) must match your organisation's ZIMRA tax configuration.

List fiscal invoices

GET/v1/external/fiscal-invoices?page=0&size=20&customerId=501

Get a fiscal invoice

GET/v1/external/fiscal-invoices/{fiscalInvoiceId}

Delete a fiscal invoice advice

DELETE/v1/external/fiscal-invoices/{fiscalInvoiceId}

Deletes an unpaid fiscal invoice advice. Returns 204 No Content. Only advices that are still unpaid and not yet fiscalised can be deleted — otherwise 400. 404 if it does not belong to your organisation.

{fiscalInvoiceId} here is the advice id (as returned by create / list / get), the same id used by the GET and download endpoints.

Pay a fiscal invoice (submit to ZIMRA)

POST/v1/external/fiscal-invoices/{fiscalInvoiceId}/pay

Fiscalises the receipt and submits it to ZIMRA. The combined payment amounts should settle the invoice total.

{
  "payments": [
    { "moneyTypeCode": "Cash", "paymentAmount": 600.00 },
    { "moneyTypeCode": "Card", "paymentAmount": 555.00 }
  ]
}

moneyTypeCode is one of Cash, Card, MobileWallet, Coupon, Credit, BankTransfer, Other.

Response — 200 on success, 400 if ZIMRA rejects the submission (with errors).

{
  "invoiceNumber": "00090012",
  "receiptId": 88123,
  "invoiceId": 9001,
  "status": "SUBMITTED",
  "message": "Receipt submitted",
  "errors": null
}

Download a fiscal invoice

GET/v1/external/fiscal-invoices/{fiscalInvoiceId}/download

Returns the fiscal invoice as a PDF (application/pdf).

Only fiscal invoices can be downloaded. Standard invoices are not available as PDFs.

Credit note (cancel a fiscalised invoice)

POST/v1/external/fiscal-invoices/{invoiceId}/credit-note

Reverses (cancels) a fiscalised invoice with ZIMRA and records the reason. Use this to refund or void a receipt.

{ "reason": "Customer returned goods" }
{invoiceId} here is the fiscalised invoice id — the invoiceId returned by the pay endpoint — not the advice id. An invoice can only be cancelled within 12 months of its invoicing date.

Response — same shape as the pay endpoint: 200 on success, 400 if ZIMRA rejects the submission (with errors).

Debit note (increase a fiscalised invoice)

POST/v1/external/fiscal-invoices/{invoiceId}/debit-note

Raises a debit note against a fiscalised invoice — increasing the amount owed — and submits it to ZIMRA. Each item adds to a line on the original invoice (referenced by its invoiceItemId).

{
  "reason": "Additional services rendered",
  "items": [
    {
      "invoiceItemId": 456,
      "additionalItemPrice": 100.00,
      "invoiceItemTotal": 1100.00,
      "quantity": 1.0
    }
  ],
  "payments": [
    { "moneyTypeCode": "Cash", "paymentAmount": 100.00 }
  ]
}
{invoiceId} here is the fiscalised invoice id returned by the pay endpoint, not the advice id. moneyTypeCode values are the same as for the pay endpoint.

Response — same shape as the pay endpoint: 200 on success, 400 if ZIMRA rejects it.

Bulk operations

For migrations and high-volume integrations you can create and fiscalise many records in a single call. Batch endpoints are partial-success: each item is processed independently, so one bad item never fails the rest. They always return 200 with a summary envelope — inspect successCount/failureCount and the per-item results. Each result's index matches the position of the item in the array you sent.

LimitA batch is capped at 100 items per request; exceeding this returns 400.

Envelope

{
  "total": 3,
  "successCount": 2,
  "failureCount": 1,
  "results": [
    { "index": 0, "success": true,  "data": { /* the created/processed record */ } },
    { "index": 1, "success": false, "error": "email is required" },
    { "index": 2, "success": true,  "data": { /* ... */ } }
  ]
}

Create invoices in bulk

POST/v1/external/invoices/batch
{
  "invoices": [
    { "customerId": 501, "salesPerson": "John Banda", "subject": "Website", "invoicingDate": "2026-05-20", "totalAmount": 1500.00, "currency": "USD", "items": [ { "itemDescription": "Design", "quantity": 1, "price": 1500.00, "totalAmount": 1500.00 } ] },
    { "customerId": 502, "salesPerson": "John Banda", "subject": "Hosting", "invoicingDate": "2026-05-20", "totalAmount": 200.00, "currency": "USD", "items": [ { "itemDescription": "Annual hosting", "quantity": 1, "price": 200.00, "totalAmount": 200.00 } ] }
  ]
}

Each successful item's data is the full invoice (same shape as the single create endpoint).

Create fiscal invoices in bulk

POST/v1/external/fiscal-invoices/batch

Creates the advices only — submit them to ZIMRA with the bulk pay endpoint. Body is { "invoices": [ ... ] } where each item is the same shape as the single create-fiscal-invoice request.

Pay (fiscalise) in bulk

POST/v1/external/fiscal-invoices/pay/batch

Each advice is submitted to ZIMRA individually. An item's data is the same response as the single pay endpoint; on a ZIMRA rejection the item is marked success: false and data.status is FAILED with data.errors populated.

{
  "items": [
    { "fiscalInvoiceId": 9001, "payments": [ { "moneyTypeCode": "Cash", "paymentAmount": 1155.00 } ] },
    { "fiscalInvoiceId": 9002, "payments": [ { "moneyTypeCode": "Card", "paymentAmount": 600.00 }, { "moneyTypeCode": "Cash", "paymentAmount": 555.00 } ] }
  ]
}

Example (cURL)

1. Exchange credentials for an access token

curl -X POST https://your-host/v1/external/auth/token \
  -H "X-API-Key: pk_live_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4" \
  -H "X-API-Secret: sk_live_9z8y7x6w5v4u3t2s1r0q9z8y7x6w5v4u3t2s1r0q9z8y7x6w"
# => { "accessToken": "eyJhbGciOiJIUzUxMiJ9...", "tokenType": "Bearer", "expiresIn": 7200, ... }

2. Call any endpoint with the token

curl -X POST https://your-host/v1/external/invoices \
  -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9..." \
  -H "Content-Type: application/json" \
  -d '{
        "customerId": 501,
        "salesPerson": "John Banda",
        "subject": "Website development",
        "invoicingDate": "2026-05-20",
        "dueDate": "2026-06-03",
        "totalAmount": 1500.00,
        "currency": "USD",
        "items": [
          { "itemDescription": "Design & build", "quantity": 1, "price": 1500.00, "totalAmount": 1500.00 }
        ]
      }'