Skip to main content

Invoices

Base route: /integration/invoices

Auth: API key or request signature. See Authentication. ownerAddress is required on every call.

Client setup​

The examples on this page share this client. It authenticates with an API key; for signature mode see Request signature.

sodapop-client.ts
import axios, { AxiosInstance } from "axios";

export type InvoiceStatus =
| "pending"
| "paid"
| "completed"
| "cancelled"
| "expired";

export interface Invoice {
id: string;
ownerAddress: string;
chainId: number;
invoiceId: string;
paymentAddress: string;
factoryVersion: number;
amount: string;
tokenAddress?: string; // absent for native token
description?: string;
metadata?: Record<string, unknown>;
expiresAt: number;
status: InvoiceStatus;
paid: boolean;
paidAt?: number;
withdrawn?: boolean;
withdrawnAt?: number;
requiredRaw?: string;
balanceRaw?: string;
balance?: number;
usdPrice?: number;
usdValue?: number;
lastCheckedAt?: number;
redirectUrl?: string;
createdAt: number;
updatedAt: number;
url: string;
}

export class SodaPopClient {
protected readonly http: AxiosInstance;

constructor(
protected readonly ownerAddress: string,
apiKey: string = process.env.SODAPOP_API_KEY ?? "",
baseUrl: string = process.env.SODAPOP_BASE_URL ?? "https://api.sodapopfi.com",
) {
this.http = axios.create({
baseURL: baseUrl.replace(/\/$/, ""),
headers: {
"Content-Type": "application/json",
"x-depay-api-key": apiKey,
},
});
}

/** Normalizes axios failures into a single readable error. */
protected toApiError(method: string, error: unknown): Error {
if (!axios.isAxiosError(error)) {
return error instanceof Error ? error : new Error(String(error));
}
const status = error.response?.status ?? "unknown";
const details = JSON.stringify(error.response?.data ?? error.message);
return new Error(`SodaPop API ${method} error ${status}: ${details}`);
}
}

Create an invoice​

POST /integration/invoices

FieldTypeRequiredDescription
ownerAddressaddressYesMerchant wallet that receives the funds.
chainIdnumberYesA supported chain.
amountstringYesAmount in token units, e.g. "12.5".
tokenAddressstringNoToken address or symbol (USDC, USDT, EURC). Omit for the native token.
invoiceIdstringNoYour reference, unique per owner. Generated from the owner's sequence if omitted.
descriptionstringNoShown on the payment page.
durationnumberNoLifetime in seconds. Takes precedence over expiresAt.
expiresAtnumberNoAbsolute deadline, Unix ms. Ignored if in the past.
metadataobjectNoAny JSON. Returned as is.
webhookUrlURLNoReceives a POST when the invoice is paid. See Webhooks.

Without duration or expiresAt the invoice expires after 36 hours.

Request body
{
"ownerAddress": "0x501BEF961A6f40E063efD6048768b0BC35ab1428",
"chainId": 8453,
"invoiceId": "1000456",
"amount": "12.5",
"tokenAddress": "USDC",
"description": "Payment for order #1000456",
"duration": 3600,
"metadata": { "orderId": "1000456" },
"webhookUrl": "https://merchant.example.com/webhooks/invoice"
}

Returns the invoice object. Payment tracking starts right away. usdPrice and usdValue may be absent in this response; the status endpoint returns them when a price is available.

async createInvoice(params: {
chainId: number;
amount: string;
invoiceId?: string;
tokenAddress?: string;
description?: string;
duration?: number;
expiresAt?: number;
metadata?: Record<string, unknown>;
webhookUrl?: string;
}): Promise<Invoice> {
try {
const { data } = await this.http.post<Invoice>("/integration/invoices", {
ownerAddress: this.ownerAddress,
...params,
});
return data;
} catch (error) {
throw this.toApiError("createInvoice", error);
}
}

Usage:

const client = new SodaPopClient(process.env.OWNER_ADDRESS!);

const invoice = await client.createInvoice({
chainId: 8453,
invoiceId: "1000456",
amount: "12.5",
tokenAddress: "USDC",
webhookUrl: "https://merchant.example.com/webhooks/invoice",
});

// Redirect the payer and store paymentAddress with your order.
console.log(invoice.url, invoice.paymentAddress);

Get invoice status​

GET /integration/invoices/status

Query paramRequiredDescription
ownerAddressYesMerchant wallet.
paymentAddressYespaymentAddress from the create response.

Re-reads the balance on-chain before answering, so this is the freshest and the slowest invoice call. Returns { "invoice": { … } }.

Errors: 404 Invoice not found (also for cancelled invoices), 403 Invoice does not belong to ownerAddress.

:::info Look up by paymentAddress, not invoiceId Status and delete calls are keyed by the payment address. Store paymentAddress from the create response with your order. :::

async getInvoiceStatus(paymentAddress: string): Promise<Invoice> {
try {
const { data } = await this.http.get<{ invoice: Invoice }>(
"/integration/invoices/status",
{ params: { ownerAddress: this.ownerAddress, paymentAddress } },
);
return data.invoice;
} catch (error) {
throw this.toApiError("getInvoiceStatus", error);
}
}

Same call in signature mode, with the sign() helper from Authentication:

const { signature, timestamp } = await sign("invoice.status", { paymentAddress });

const { data } = await axios.get("https://api.sodapopfi.com/integration/invoices/status", {
params: { ownerAddress, paymentAddress },
headers: {
"x-depay-signature": signature,
"x-depay-timestamp": String(timestamp),
},
});

Waiting for a payment:

async function waitForPayment(
client: SodaPopClient,
paymentAddress: string,
{ intervalMs = 15_000, timeoutMs = 3_600_000 } = {},
): Promise<Invoice> {
const deadline = Date.now() + timeoutMs;

while (Date.now() < deadline) {
const invoice = await client.getInvoiceStatus(paymentAddress);
if (invoice.status !== "pending") return invoice;
await new Promise((resolve) => setTimeout(resolve, intervalMs));
}

throw new Error(`Invoice ${paymentAddress} is still pending`);
}

List invoices​

GET /integration/invoices

Query paramTypeDescription
ownerAddressaddressRequired.
pagenumberDefault 1.
limitnumberDefault 20, max 100.
chainIdnumberFilter by chain.
statusstringpending, paid, completed, cancelled or expired. Cancelled invoices appear only with status=cancelled.
paidbooleanFilter by the paid flag.
tokenAddressstringAddress or symbol. Symbols need chainId.
invoiceIdstringExact match.
fromCreatedAtnumbercreatedAt >=, Unix ms.
toCreatedAtnumbercreatedAt <=, Unix ms.

Sorted by createdAt, newest first. Returns { items, total, page, limit }.

In signature mode, sign all query params except ownerAddress, including page and limit even if you rely on the defaults.

async listInvoices(filters: {
page?: number;
limit?: number;
chainId?: number;
status?: InvoiceStatus;
tokenAddress?: string;
invoiceId?: string;
fromCreatedAt?: number;
toCreatedAt?: number;
} = {}): Promise<{ items: Invoice[]; total: number; page: number; limit: number }> {
try {
const { data } = await this.http.get("/integration/invoices", {
params: { ownerAddress: this.ownerAddress, ...filters },
});
return data;
} catch (error) {
throw this.toApiError("listInvoices", error);
}
}

Iterating over every pending invoice:

async function* iteratePending(client: SodaPopClient, chainId: number) {
const limit = 100;
for (let page = 1; ; page++) {
const { items, total } = await client.listInvoices({
chainId,
status: "pending",
page,
limit,
});
yield* items;
if (page * limit >= total) return;
}
}

Cancel an invoice​

DELETE /integration/invoices

Query paramRequired
ownerAddressYes
paymentAddressYes

Sets status to cancelled and stops tracking the invoice. Funds that already arrived are not moved or refunded.

Response
{
"deleted": true,
"invoice": { "invoiceId": "1000456", "chainId": 8453, "status": "cancelled", "...": "..." }
}
async cancelInvoice(paymentAddress: string): Promise<{ deleted: boolean; invoice: Invoice }> {
try {
const { data } = await this.http.delete("/integration/invoices", {
params: { ownerAddress: this.ownerAddress, paymentAddress },
});
return data;
} catch (error) {
throw this.toApiError("cancelInvoice", error);
}
}

Invoice object​

{
"id": "662cc88b3450ca6b40f8fd90",
"ownerAddress": "0x501BEF961A6f40E063efD6048768b0BC35ab1428",
"chainId": 8453,
"invoiceId": "1000456",
"paymentAddress": "0x6c76fD5f0b5F7924A0b6b23223A8D2398fB00E72",
"factoryVersion": 2,
"amount": "12.5",
"tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"description": "Payment for order #1000456",
"metadata": { "orderId": "1000456" },
"expiresAt": 1735593600000,
"status": "paid",
"paid": true,
"paidAt": 1735591000000,
"withdrawn": false,
"requiredRaw": "12500000",
"balanceRaw": "12500000",
"balance": 12.5,
"usdPrice": 0.9998,
"usdValue": 12.4975,
"lastCheckedAt": 1735591001000,
"createdAt": 1735590000000,
"updatedAt": 1735591001000,
"url": "https://app.sodapopfi.com/pay/0x6c76fD5f0b5F7924A0b6b23223A8D2398fB00E72"
}
FieldDescription
paymentAddressAddress the payer sends funds to. Deterministic per (ownerAddress, invoiceId).
factoryVersionVersion of the PaymentFactory that controls paymentAddress. Currently 2.
statusSee Invoice lifecycle.
tokenAddressResolved token address. Absent for the native token.
requiredRaw / balanceRawRequired and current amount in the token's smallest unit.
balanceCurrent balance in token units.
usdPrice / usdValueUSD price of the token and the value of balance (or amount before payment). Absent if no price is available.
withdrawn / withdrawnAtSet once the withdrawal to the payout address is confirmed.
redirectUrlWhere the payment page sends the payer after paying, if configured.
urlHosted payment page. Safe to redirect payers to.