Skip to main content

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:

ResponseAction
200Return the JSON body
429Read Retry-After, wait, retry
5xxWait 1s, 2s, 4s (exponential backoff), retry
4xxThrow immediately — the request needs fixing
All attempts exhaustedThrow "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.