Bjet24

API v1 · OpenAPI 3.1 · stable

API Reference

Send physical mail from your CRM, ERP or backoffice. One base URL, Bearer-key auth, wallet billing — no card-per-call hassle.

Overview

A stateless REST API, JSON in/out, versioned. All routes are prefixed by /v1.

  • Base URLhttps://bjet24.com/api
  • Versionv1 · OpenAPI 3.1
  • Formatapplication/json
  • AuthBearer / X-Api-Key

Authentication

Every request must include a valid API key in the Authorization header (Bearer format) or in the X-Api-Key header. Keys follow the format bjk_<id>_<secret>.

curl https://bjet24.com/api/v1/mails \
  -H "Authorization: Bearer bjk_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Available scopes

  • readread mails, quotes, lists
  • sendcreate mails (debits wallet)
  • webhookscreate/delete webhooks
  • addressesManage the address book (senders and recipients).

Create and revoke your keys at /account/api-keys.

Error format

All errors return a 4xx or 5xx HTTP status and a JSON body with code (machine) and message (human). Validation errors include an issues array describing each problem.

application/json
{
  "code": "VALIDATION_ERROR",
  "message": "Invalid request body",
  "issues": [
    { "path": ["pages"], "message": "Number must be greater than or equal to 1" }
  ]
}
  • VALIDATION_ERRORRequest body does not match the schema. See issues.
  • UNAUTHORIZEDAPI key missing or invalid.
  • FORBIDDENKey lacks the required scope for this route.
  • NOT_FOUNDResource not found or does not belong to your organization.
  • INSUFFICIENT_FUNDSWallet balance insufficient to cover the send cost.
  • PRICING_UNAVAILABLEPricing not configured for the requested zone/service. Contact support.

Idempotency

To avoid duplicates on network retries, pass a unique externalId (typically your CRM id) on POST /v1/mails. Repeated requests with the same externalId return the existing send — no new debit.

How it works

On the first successful create, the (organization, externalId) tuple is locked. Any subsequent POST with the same externalId returns 200 with the original mailJobId — even if the payload differs.

Rate limits

Limits apply per API key. Requests above the quota receive a 429 with a Retry-After header.

  • Read (GET)120 / min
  • Write (POST, DELETE)60 / min
  • Max burst30

Get a quote

POST/v1/quoteRequired scope: read

Calculates the total price for a service / zone / page count. No debit. Useful to display the price client-side before sending.

Request body

  • zonestringrequired
    NATIONALINTERNATIONAL

    Send zone. NATIONAL = Belgium, INTERNATIONAL = outside Belgium.

  • serviceTypestringrequired
    standardpriorregistered

    Postal service type. registered adds proof of deposit and tracking.

  • pagesintegerrequired

    Total printed pages (1 to 2000).

  • colorbooleanoptionaldefault false

    Color printing. Higher price.

  • acknowledgmentReceiptbooleanoptionaldefault false

    Acknowledgment receipt (registered only). Higher price.

  • recipientCountintegeroptionaldefault 1

    Number of distinct recipients (1 to 100). Multiplies the unit price.

Responses

  • 200Quote computed. Total includes VAT if applicable.
  • 400Invalid request body. See issues.
  • 401API key missing or invalid.
curl -X POST https://bjet24.com/api/v1/quote \
  -H "Authorization: Bearer $BJET24_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "zone": "NATIONAL",
    "serviceType": "registered",
    "pages": 3,
    "color": false,
    "acknowledgmentReceipt": false,
    "recipientCount": 1
  }'

Get a presigned upload URL

POST/v1/uploadsRequired scope: send

Returns an S3 PUT URL valid for 10 minutes. Upload your PDF or image, then pass the returned key as s3Key in POST /v1/mails. Uploads go straight to S3 — no large payloads through our API.

Request body

  • filenamestringrequired

    Original file name. Used for logs and the invoice.

  • mimeTypestringrequired
    application/pdfimage/jpegimage/pngimage/heicimage/heif

    MIME type. PDF recommended for mails; images converted automatically.

  • sizeBytesintegerrequired

    Size in bytes. Max 20 MB.

Responses

  • 200Presigned URL returned. Upload via PUT within 10 minutes.
  • 400Invalid request body. See issues.
  • 401API key missing or invalid.
curl -X POST https://bjet24.com/api/v1/uploads \
  -H "Authorization: Bearer $BJET24_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "filename": "contract.pdf",
    "mimeType": "application/pdf",
    "sizeBytes": 124532
  }'

Create and send a mail

POST/v1/mailsRequired scope: send

Queues the mail and immediately debits your wallet. If dryRun=true, the mail is validated and priced without debit or persistence. Pass externalId for idempotency and webhookUrl for a per-mail callback.

Request body

  • zonestringrequired
    NATIONALINTERNATIONAL

    Send zone. NATIONAL = Belgium, INTERNATIONAL = outside Belgium.

  • serviceTypestringrequired
    standardpriorregistered

    Postal service type. registered adds proof of deposit and tracking.

  • pagesintegerrequired

    Total printed pages (1 to 2000).

  • colorbooleanoptionaldefault false

    Color printing. Higher price.

  • acknowledgmentReceiptbooleanoptionaldefault false

    Acknowledgment receipt (registered only). Higher price.

  • scheduledDropAtstring (ISO 8601)optional

    Scheduled drop date/time (ISO 8601). If absent, drop at the next cutoff.

  • recipientsRecipient[]required

    List of recipients (1 to 100). Each receives a printed copy.

  • senderSenderoptional

    Sender address shown on the envelope. If absent, uses your account's default address.

  • filesFile[]required

    Files to print (1 to 20). Concatenated in order. s3Key comes from POST /v1/uploads.

  • externalIdstringoptional

    Caller-supplied id (e.g. CRM id). Guarantees idempotency.

  • webhookUrlstring (uri)optional

    Per-mail webhook URL. Overrides the global config.

  • dryRunbooleanoptionaldefault false

    If true, validate and price without debit or persistence. Response identical but mailJobId is null.

Responses

  • 201Mail created and queued. Wallet debited.
  • 200Idempotency hit — mail already existed with the same externalId.
  • 400Invalid request body. See issues.
  • 402Wallet balance insufficient. Top up and retry.
  • 503Pricing not configured for zone/service.
curl -X POST https://bjet24.com/api/v1/mails \
  -H "Authorization: Bearer $BJET24_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "zone": "NATIONAL",
    "serviceType": "registered",
    "pages": 3,
    "color": false,
    "recipients": [{
      "name": "Jean Dupont",
      "line1": "Rue de la Loi 16",
      "zip": "1000",
      "city": "Brussels",
      "country": "BE"
    }],
    "files": [{
      "s3Key": "uploads/2026-05-19/8af9...c12.pdf",
      "filename": "contract.pdf",
      "mimeType": "application/pdf",
      "sizeBytes": 124532
    }],
    "externalId": "crm-contract-7281"
  }'

List mails

GET/v1/mailsRequired scope: read

Returns your mails, newest first. Cursor-paginated.

Query parameters

  • statusstringoptional

    Filter by status (queued, printed, posted, delivered, failed).

  • externalIdstringoptional

    Filter by exact externalId.

  • limitintegeroptionaldefault 20

    Results per page (1 to 100).

  • cursorstringoptional

    Pagination cursor returned by the previous response.

Responses

  • 200Paginated list of mails.
cURL
curl "https://bjet24.com/api/v1/mails?status=queued&limit=20" \
  -H "Authorization: Bearer $BJET24_KEY"

Get a mail

GET/v1/mails/{id}Required scope: read

Returns the full detail: status, recipients, files, amounts, timestamps.

Path parameters

  • idstringrequired

    Mail id (mj_…).

Responses

  • 200Mail detail.
  • 404Resource not found.
cURL
curl https://bjet24.com/api/v1/mails/mj_01HZX5K8... \
  -H "Authorization: Bearer $BJET24_KEY"

Simulate the lifecycle (sandbox)

POST/v1/mails/{id}/simulateRequired scope: send

Test mode only. Advances a sandbox mail through its lifecycle and fires the matching `test` webhook. Without `to`, the mail moves one step (queued → printed → posted → delivered); with `to`, it jumps straight to that status — e.g. {"to":"failed"} to test failure handling. Sandbox mails also progress on their own: this endpoint is for deterministic, on-demand testing. Rejected (403) with a live key.

Path parameters

  • idstringrequired

    Mail id (mj_…).

Request body

  • tostringoptional
    printedposteddeliveredfailed

    Target status. Omitted: advance one step. Values: printed, posted, delivered, failed. A direct jump only fires the webhook of the reached status.

Responses

  • 200Transition applied — returns the previous and new status.
  • 400Invalid request body. See issues.
  • 403Live credentials — simulation is test mode only.
  • 404Resource not found.
  • 409Transition not possible (already terminal, or unreachable target).
cURL
# Test mode only — advance the sandbox mail one step
curl -X POST https://bjet24.com/api/v1/mails/mj_01HZX5K8.../simulate \
  -H "Authorization: Bearer $BJET24_TEST_KEY"

# …or jump straight to a status (e.g. test failure handling)
curl -X POST https://bjet24.com/api/v1/mails/mj_01HZX5K8.../simulate \
  -H "Authorization: Bearer $BJET24_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":"failed"}'

List addresses

GET/v1/addressesRequired scope: addresses

Returns saved address book entries — senders and recipients. Filterable by kind and free-text search.

Query parameters

  • kindstringoptional
    SENDERRECIPIENT

    Filter by address kind.

  • qstringoptional

    Free-text search on name, city, postal code or label.

  • limitintegeroptionaldefault 100

    Maximum number of addresses returned (1–200).

Responses

  • 200The list of address book entries.
  • 401API key missing or invalid.
cURL
curl "https://bjet24.com/api/v1/addresses?kind=RECIPIENT&limit=50" \
  -H "Authorization: Bearer $BJET24_KEY"

Create an address

POST/v1/addressesRequired scope: addresses

Saves an address to the book — for example to sync your CRM contacts.

Request body

  • kindstringrequired
    SENDERRECIPIENT

    Address kind: sender (SENDER) or recipient (RECIPIENT).

  • namestringrequired

    Recipient or sender name.

  • line1stringrequired

    First address line (street and number).

  • line2stringoptional

    Optional address complement (box, floor…).

  • zipstringrequired

    Postal code.

  • citystringrequired

    City.

  • countrystringrequired

    ISO 3166-1 alpha-2 country code (e.g. BE).

  • labelstringoptional

    Optional internal label, e.g. Head office or Client X.

  • isDefaultbooleanoptionaldefault false

    Sets this address as the default for its kind.

Responses

  • 201The address, in its public form.
  • 400Invalid request body. See issues.
curl -X POST https://bjet24.com/api/v1/addresses \
  -H "Authorization: Bearer $BJET24_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "RECIPIENT",
    "name": "Jean Dupont",
    "line1": "Rue de la Loi 16",
    "zip": "1000",
    "city": "Brussels",
    "country": "BE",
    "label": "Client 7281"
  }'

Get an address

GET/v1/addresses/{id}Required scope: addresses

Returns a single address book entry.

Path parameters

  • idstringrequired

    Address identifier.

Responses

  • 200The address, in its public form.
  • 404Resource not found.
cURL
curl https://bjet24.com/api/v1/addresses/adr_01HZX5K8... \
  -H "Authorization: Bearer $BJET24_KEY"

Update an address

PATCH/v1/addresses/{id}Required scope: addresses

Updates the provided fields of an existing address. Omitted fields are left unchanged.

Path parameters

  • idstringrequired

    Address identifier.

Request body

  • kindstringoptional
    SENDERRECIPIENT

    Address kind: sender (SENDER) or recipient (RECIPIENT).

  • namestringoptional

    Recipient or sender name.

  • line1stringoptional

    First address line (street and number).

  • line2stringoptional

    Optional address complement (box, floor…).

  • zipstringoptional

    Postal code.

  • citystringoptional

    City.

  • countrystringoptional

    ISO 3166-1 alpha-2 country code (e.g. BE).

  • labelstringoptional

    Optional internal label, e.g. Head office or Client X.

  • isDefaultbooleanoptional

    Sets this address as the default for its kind.

Responses

  • 200The address, in its public form.
  • 400Invalid request body. See issues.
  • 404Resource not found.
cURL
curl -X PATCH https://bjet24.com/api/v1/addresses/adr_01HZX5K8... \
  -H "Authorization: Bearer $BJET24_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "city": "Bruxelles", "isDefault": true }'

Delete an address

DELETE/v1/addresses/{id}Required scope: addresses

Permanently removes an address from the book.

Path parameters

  • idstringrequired

    Address identifier.

Responses

  • 200Address deleted.
  • 404Resource not found.
cURL
curl -X DELETE https://bjet24.com/api/v1/addresses/adr_01HZX5K8... \
  -H "Authorization: Bearer $BJET24_KEY"

List your webhooks

GET/v1/webhooksRequired scope: read

Returns all active webhook endpoints on your account.

Responses

  • 200List of webhooks (without the secret).
cURL
curl https://bjet24.com/api/v1/webhooks \
  -H "Authorization: Bearer $BJET24_KEY"

Register a webhook

POST/v1/webhooksRequired scope: webhooks

Creates a webhook endpoint. The signing secret is returned ONLY ONCE — store it immediately, it cannot be retrieved again.

Request body

  • urlstring (uri)required

    HTTPS URL where events will be posted.

  • eventsstring[]required
    MAIL_QUEUEDMAIL_PRINTEDMAIL_POSTEDMAIL_DELIVEREDMAIL_FAILED

    List of events you want to receive. At least one required.

Responses

  • 201Webhook created. Save the returned secret.
  • 400Invalid request body. See issues.
curl -X POST https://bjet24.com/api/v1/webhooks \
  -H "Authorization: Bearer $BJET24_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.example.com/webhooks/bjet24",
    "events": ["MAIL_QUEUED", "MAIL_POSTED", "MAIL_DELIVERED", "MAIL_FAILED"]
  }'

Delete a webhook

DELETE/v1/webhooks/{id}Required scope: webhooks

Disables the endpoint immediately. Events will no longer be delivered.

Path parameters

  • idstringrequired

    Webhook id (wh_…).

Responses

  • 200Webhook deleted.
  • 404Resource not found.
cURL
curl -X DELETE https://bjet24.com/api/v1/webhooks/wh_01HZX5K8... \
  -H "Authorization: Bearer $BJET24_KEY"

Webhook events

Five events cover the lifecycle of a mail. Each delivery includes the JSON payload, the X-Bjet24-Event header and the signature.

  • MAIL_QUEUEDMail queued after wallet debit.
  • MAIL_PRINTEDDocument printed at the workshop.
  • MAIL_POSTEDDropped at the post office.
  • MAIL_DELIVEREDDelivered (registered only).
  • MAIL_FAILEDPrint, drop or cancellation failure.

Sample payload

application/json
{
  "event": "MAIL_POSTED",
  "mailJobId": "mj_01HZX5K8...",
  "externalId": "crm-contract-7281",
  "status": "posted",
  "postedAt": "2026-05-19T16:42:11.000Z"
}

Signature & verification

Every webhook delivery is signed with HMAC-SHA256 using the secret returned at creation. Verify the signature before processing the payload.

Signature header

X-Bjet24-Signature: t=<ts>,v1=<hex>

The timestamp is in Unix seconds. The signature is computed on <ts>.<rawBody> using your webhook secret.
Node.js
import crypto from 'node:crypto'

export function verifyBjet24(req, secret) {
  const header = req.headers['x-bjet24-signature']
  // header looks like: t=1714940000,v1=<hmac-sha256-hex>
  const [tPart, sPart] = header.split(',')
  const ts = tPart.split('=')[1]
  const sig = sPart.split('=')[1]

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${ts}.${req.rawBody}`)
    .digest('hex')

  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    throw new Error('Bad signature')
  }
  if (Date.now() / 1000 - Number(ts) > 300) {
    throw new Error('Replay')
  }
}

Replay protection

Reject deliveries whose timestamp is older than 5 minutes. Combine valid signature + fresh timestamp to block replay attacks.

Connect a third-party application (OAuth2)

When an application (a CRM, a business tool) needs to act on behalf of several Bjet24 users — each with their own wallet — use OAuth2 rather than an API key. The user connects their account in one click, with no key to copy and paste.

API key or OAuth2?

An API key is fine when you only send mail for your own account. OAuth2 is required when your application serves several distinct Bjet24 customers: each user grants access and their own wallet is charged.

Register your application

A Bjet24 administrator registers your application and gives you a client_id, a client_secret and the allowed redirect_uri values.

The Authorization Code + PKCE flow

1. Redirect the user to the consent screen.

GET /oauth/authorize
https://bjet24.com/fr/oauth/authorize
  ?response_type=code
  &client_id=bjc_xxxxxxxxxxxxxxxxxxxxxxxx
  &redirect_uri=https://your-app.example.com/callback
  &scope=send%20read%20wallet
  &state=<opaque-random>
  &code_challenge=<base64url(sha256(code_verifier))>
  &code_challenge_method=S256

2. Exchange the returned code for a token pair.

POST /api/oauth/token
curl -X POST https://bjet24.com/api/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=authorization_code \
  -d client_id=bjc_xxxxxxxxxxxxxxxxxxxxxxxx \
  -d client_secret=$BJET24_CLIENT_SECRET \
  -d code=$AUTH_CODE \
  -d redirect_uri=https://your-app.example.com/callback \
  -d code_verifier=$PKCE_VERIFIER
200 OK
{
  "access_token": "bja_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "bjr_...",
  "scope": "send read wallet"
}

3. Call the API with the access token as a Bearer token.

cURL
curl https://bjet24.com/api/v1/wallet \
  -H "Authorization: Bearer bja_..."

4. Refresh the expired access token with the refresh token.

POST /api/oauth/token
curl -X POST https://bjet24.com/api/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=refresh_token \
  -d client_id=bjc_xxxxxxxxxxxxxxxxxxxxxxxx \
  -d client_secret=$BJET24_CLIENT_SECRET \
  -d refresh_token=$REFRESH_TOKEN

Lifetime and revocation

An access token lives 1 h, a refresh token 60 days and rotates on every use. The user can revoke access at any time from their Bjet24 account; your application can also call POST /api/oauth/revoke.

OAuth scopes

The scopes requested at authorization time determine what your application is allowed to do.

  • sendcreate mails (debits wallet)
  • readread mails, quotes, lists
  • webhookscreate/delete webhooks
  • walletRead the user's wallet balance.
  • addressesManage the address book (senders and recipients).