Send Invoice — TypeScript Examples
No SDK required. These examples use native fetch (Node.js 18+ or any modern runtime).
Setup
Create a reusable API client:
const API_BASE = "https://api.invostaq.com/api";
const API_KEY = process.env.INVOSTAQ_API_KEY!; // sk_test_... or sk_live_...
async function invostaq(path: string, options: RequestInit = {}) {
const res = await fetch(`${API_BASE}${path}`, {
...options,
headers: {
"x-api-key": API_KEY,
"Content-Type": "application/json",
...options.headers,
},
});
if (!res.ok) {
const problem = await res.json();
throw new Error(`${problem.title}: ${problem.detail}`);
}
return res.json();
}
1. Look up a recipient
const lookup = await invostaq(
"/v1/lookup?participantId=0196:971501234567"
);
console.log(lookup);
// {
// participantId: "0196:971501234567",
// isRegistered: true,
// accessPointRef: "AP-0196-971501234567",
// supportedDocTypes: ["urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"],
// provider: "Invostaq"
// }
if (!lookup.isRegistered) {
throw new Error("Recipient is not registered on the Peppol network");
}
2. Send an invoice
const result = await invostaq("/v1/invoices/send", {
method: "POST",
headers: { "Idempotency-Key": crypto.randomUUID() },
body: JSON.stringify({
senderParticipantId: "0196:971500000001",
receiverParticipantId: "0196:971501234567",
accessPointRef: lookup.accessPointRef,
extracted: {
invoiceNumber: "INV-2024-001",
issueDate: "2024-06-15T00:00:00Z",
currencyCode: "AED",
totalAmount: 1050.0,
totalTaxAmount: 50.0,
vendor: {
name: "Acme Trading LLC",
taxId: "123456789012345",
address: "Dubai, UAE",
},
lineItems: [
{
description: "Consulting services — June 2024",
quantity: 1,
unitPrice: 1000.0,
vatRate: 0.05,
},
],
},
}),
});
console.log(result);
// {
// status: "success",
// transactionId: "txn_abc123def456",
// databaseRecordId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
// provider: "Invostaq"
// }
3. End-to-end: lookup + send
A complete function that looks up the recipient and sends the invoice.
interface InvoiceInput {
invoiceNumber: string;
issueDate: string;
currency: string;
totalAmount: number;
totalTaxAmount: number;
vendorName: string;
vendorTaxId: string;
customerName?: string;
lineItems: Array<{
description: string;
quantity: number;
unitPrice: number;
vatRate: number;
productCode?: string;
}>;
}
async function sendInvoice(
senderPeppolId: string,
receiverPeppolId: string,
invoice: InvoiceInput
) {
// Step 1: Verify the recipient is on Peppol
const lookup = await invostaq(
`/v1/lookup?participantId=${encodeURIComponent(receiverPeppolId)}`
);
if (!lookup.isRegistered) {
throw new Error(
`Recipient ${receiverPeppolId} is not registered on the Peppol network`
);
}
// Step 2: Send the invoice
const result = await invostaq("/v1/invoices/send", {
method: "POST",
headers: { "Idempotency-Key": crypto.randomUUID() },
body: JSON.stringify({
senderParticipantId: senderPeppolId,
receiverParticipantId: receiverPeppolId,
accessPointRef: lookup.accessPointRef,
extracted: {
invoiceNumber: invoice.invoiceNumber,
issueDate: invoice.issueDate,
currencyCode: invoice.currency,
totalAmount: invoice.totalAmount,
totalTaxAmount: invoice.totalTaxAmount,
vendor: {
name: invoice.vendorName,
taxId: invoice.vendorTaxId,
},
customerName: invoice.customerName,
lineItems: invoice.lineItems,
},
}),
});
return {
transactionId: result.transactionId,
recordId: result.databaseRecordId,
};
}
Usage:
const { transactionId, recordId } = await sendInvoice(
"0196:971500000001",
"0196:971501234567",
{
invoiceNumber: "INV-2024-001",
issueDate: "2024-06-15T00:00:00Z",
currency: "AED",
totalAmount: 1050.0,
totalTaxAmount: 50.0,
vendorName: "Acme Trading LLC",
vendorTaxId: "123456789012345",
customerName: "Gulf Imports Co",
lineItems: [
{
description: "Consulting services — June 2024",
quantity: 1,
unitPrice: 1000.0,
vatRate: 0.05,
},
],
}
);
console.log(`Sent! Transaction: ${transactionId}, Record: ${recordId}`);
4. Error handling
Handle specific error types for better UX:
async function sendWithErrorHandling(invoice: any) {
const res = await fetch(`${API_BASE}/v1/invoices/send`, {
method: "POST",
headers: {
"x-api-key": API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify(invoice),
});
if (res.ok) return await res.json();
const problem = await res.json();
switch (problem.type) {
case "https://invostaq.com/errors/totals-mismatch":
throw new Error(`Totals don't match: ${problem.detail}`);
case "https://invostaq.com/errors/validation-failed":
throw new Error(
`Validation failed:\n${problem.validationErrors.join("\n")}`
);
case "https://invostaq.com/errors/network-failed":
throw new Error(
`Peppol error ${problem.errorCode} (HTTP ${problem.upstreamStatus})`
);
default:
throw new Error(`${problem.title}: ${problem.detail}`);
}
}
5. Production client with retry
Handles rate limiting (429) and transient server errors (5xx) automatically.
async function invostaqWithRetry(
path: string,
options: RequestInit = {},
maxAttempts = 3
) {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const res = await fetch(`${API_BASE}${path}`, {
...options,
headers: {
"x-api-key": API_KEY,
"Content-Type": "application/json",
...options.headers,
},
});
// Rate limited — wait and retry
if (res.status === 429) {
const retryAfter = parseInt(
res.headers.get("Retry-After") ?? "60",
10
);
console.log(`Rate limited. Waiting ${retryAfter}s...`);
await new Promise((r) => setTimeout(r, retryAfter * 1000));
continue;
}
// Server error — exponential backoff
if (res.status >= 500 && attempt < maxAttempts - 1) {
const delay = 2 ** attempt * 1000; // 1s, 2s, 4s
console.log(`Server error ${res.status}. Retrying in ${delay}ms...`);
await new Promise((r) => setTimeout(r, delay));
continue;
}
// Client error — throw immediately
if (!res.ok) {
const problem = await res.json();
throw new Error(`${problem.title}: ${problem.detail}`);
}
return await res.json();
}
throw new Error("Max retries exceeded");
}
Usage: Drop-in replacement for the basic invostaq() helper:
const result = await invostaqWithRetry("/v1/invoices/send", {
method: "POST",
headers: { "Idempotency-Key": crypto.randomUUID() },
body: JSON.stringify(invoicePayload),
});
How it works:
| Response | Action |
|---|---|
200 | Return the JSON body |
429 | Read Retry-After, wait, retry |
5xx | Wait 1s, 2s, 4s (exponential backoff), retry |
4xx | Throw immediately — the request needs fixing |
| All attempts exhausted | Throw "Max retries exceeded" |
The Idempotency-Key header ensures retries are safe — even if the first request succeeded but you didn't get the response, the retry returns the original result without resubmitting to Peppol.