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 URL
https://bjet24.com/api - Version
v1 · OpenAPI 3.1 - Format
application/json - Auth
Bearer / 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
read— read mails, quotes, listssend— create mails (debits wallet)webhooks— create/delete webhooksaddresses— Manage 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.
{
"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
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 burst
30
Get a quote
/v1/quoteRequired scope: readCalculates the total price for a service / zone / page count. No debit. Useful to display the price client-side before sending.
Request body
zonestringrequiredNATIONALINTERNATIONALSend zone. NATIONAL = Belgium, INTERNATIONAL = outside Belgium.
serviceTypestringrequiredstandardpriorregisteredPostal service type. registered adds proof of deposit and tracking.
pagesintegerrequiredTotal printed pages (1 to 2000).
colorbooleanoptionaldefaultfalseColor printing. Higher price.
acknowledgmentReceiptbooleanoptionaldefaultfalseAcknowledgment receipt (registered only). Higher price.
recipientCountintegeroptionaldefault1Number 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
/v1/uploadsRequired scope: sendReturns 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
filenamestringrequiredOriginal file name. Used for logs and the invoice.
mimeTypestringrequiredapplication/pdfimage/jpegimage/pngimage/heicimage/heifMIME type. PDF recommended for mails; images converted automatically.
sizeBytesintegerrequiredSize 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
/v1/mailsRequired scope: sendQueues 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
zonestringrequiredNATIONALINTERNATIONALSend zone. NATIONAL = Belgium, INTERNATIONAL = outside Belgium.
serviceTypestringrequiredstandardpriorregisteredPostal service type. registered adds proof of deposit and tracking.
pagesintegerrequiredTotal printed pages (1 to 2000).
colorbooleanoptionaldefaultfalseColor printing. Higher price.
acknowledgmentReceiptbooleanoptionaldefaultfalseAcknowledgment receipt (registered only). Higher price.
scheduledDropAtstring (ISO 8601)optionalScheduled drop date/time (ISO 8601). If absent, drop at the next cutoff.
recipientsRecipient[]requiredList of recipients (1 to 100). Each receives a printed copy.
senderSenderoptionalSender address shown on the envelope. If absent, uses your account's default address.
filesFile[]requiredFiles to print (1 to 20). Concatenated in order. s3Key comes from POST /v1/uploads.
externalIdstringoptionalCaller-supplied id (e.g. CRM id). Guarantees idempotency.
webhookUrlstring (uri)optionalPer-mail webhook URL. Overrides the global config.
dryRunbooleanoptionaldefaultfalseIf 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
/v1/mailsRequired scope: readReturns your mails, newest first. Cursor-paginated.
Query parameters
statusstringoptionalFilter by status (queued, printed, posted, delivered, failed).
externalIdstringoptionalFilter by exact externalId.
limitintegeroptionaldefault20Results per page (1 to 100).
cursorstringoptionalPagination cursor returned by the previous response.
Responses
- 200Paginated list of mails.
curl "https://bjet24.com/api/v1/mails?status=queued&limit=20" \
-H "Authorization: Bearer $BJET24_KEY"Get a mail
/v1/mails/{id}Required scope: readReturns the full detail: status, recipients, files, amounts, timestamps.
Path parameters
idstringrequiredMail id (mj_…).
Responses
- 200Mail detail.
- 404Resource not found.
curl https://bjet24.com/api/v1/mails/mj_01HZX5K8... \
-H "Authorization: Bearer $BJET24_KEY"Simulate the lifecycle (sandbox)
/v1/mails/{id}/simulateRequired scope: sendTest 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
idstringrequiredMail id (mj_…).
Request body
tostringoptionalprintedposteddeliveredfailedTarget 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).
# 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
/v1/addressesRequired scope: addressesReturns saved address book entries — senders and recipients. Filterable by kind and free-text search.
Query parameters
kindstringoptionalSENDERRECIPIENTFilter by address kind.
qstringoptionalFree-text search on name, city, postal code or label.
limitintegeroptionaldefault100Maximum number of addresses returned (1–200).
Responses
- 200The list of address book entries.
- 401API key missing or invalid.
curl "https://bjet24.com/api/v1/addresses?kind=RECIPIENT&limit=50" \
-H "Authorization: Bearer $BJET24_KEY"Create an address
/v1/addressesRequired scope: addressesSaves an address to the book — for example to sync your CRM contacts.
Request body
kindstringrequiredSENDERRECIPIENTAddress kind: sender (SENDER) or recipient (RECIPIENT).
namestringrequiredRecipient or sender name.
line1stringrequiredFirst address line (street and number).
line2stringoptionalOptional address complement (box, floor…).
zipstringrequiredPostal code.
citystringrequiredCity.
countrystringrequiredISO 3166-1 alpha-2 country code (e.g. BE).
labelstringoptionalOptional internal label, e.g. Head office or Client X.
isDefaultbooleanoptionaldefaultfalseSets 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
/v1/addresses/{id}Required scope: addressesReturns a single address book entry.
Path parameters
idstringrequiredAddress identifier.
Responses
- 200The address, in its public form.
- 404Resource not found.
curl https://bjet24.com/api/v1/addresses/adr_01HZX5K8... \
-H "Authorization: Bearer $BJET24_KEY"Update an address
/v1/addresses/{id}Required scope: addressesUpdates the provided fields of an existing address. Omitted fields are left unchanged.
Path parameters
idstringrequiredAddress identifier.
Request body
kindstringoptionalSENDERRECIPIENTAddress kind: sender (SENDER) or recipient (RECIPIENT).
namestringoptionalRecipient or sender name.
line1stringoptionalFirst address line (street and number).
line2stringoptionalOptional address complement (box, floor…).
zipstringoptionalPostal code.
citystringoptionalCity.
countrystringoptionalISO 3166-1 alpha-2 country code (e.g. BE).
labelstringoptionalOptional internal label, e.g. Head office or Client X.
isDefaultbooleanoptionalSets this address as the default for its kind.
Responses
- 200The address, in its public form.
- 400Invalid request body. See issues.
- 404Resource not found.
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
/v1/addresses/{id}Required scope: addressesPermanently removes an address from the book.
Path parameters
idstringrequiredAddress identifier.
Responses
- 200Address deleted.
- 404Resource not found.
curl -X DELETE https://bjet24.com/api/v1/addresses/adr_01HZX5K8... \
-H "Authorization: Bearer $BJET24_KEY"List your webhooks
/v1/webhooksRequired scope: readReturns all active webhook endpoints on your account.
Responses
- 200List of webhooks (without the secret).
curl https://bjet24.com/api/v1/webhooks \
-H "Authorization: Bearer $BJET24_KEY"Register a webhook
/v1/webhooksRequired scope: webhooksCreates a webhook endpoint. The signing secret is returned ONLY ONCE — store it immediately, it cannot be retrieved again.
Request body
urlstring (uri)requiredHTTPS URL where events will be posted.
eventsstring[]requiredMAIL_QUEUEDMAIL_PRINTEDMAIL_POSTEDMAIL_DELIVEREDMAIL_FAILEDList 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
/v1/webhooks/{id}Required scope: webhooksDisables the endpoint immediately. Events will no longer be delivered.
Path parameters
idstringrequiredWebhook id (wh_…).
Responses
- 200Webhook deleted.
- 404Resource not found.
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
{
"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>
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
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?
Register your application
The Authorization Code + PKCE flow
1. Redirect the user to the consent screen.
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=S2562. Exchange the returned code for a token pair.
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{
"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 https://bjet24.com/api/v1/wallet \
-H "Authorization: Bearer bja_..."4. Refresh the expired access token with the refresh 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_TOKENLifetime and revocation
OAuth scopes
The scopes requested at authorization time determine what your application is allowed to do.
sendcreate mails (debits wallet)readread mails, quotes, listswebhookscreate/delete webhookswalletRead the user's wallet balance.addressesManage the address book (senders and recipients).