Payment Links


Payment Links are the easiest way to start collecting payments with Tsara — no code required.
You can create them directly from your dashboard or via API, then share the generated URL with customers to receive payments in NGN or USDC.


Overview

  • Create a payment link via API or dashboard.
  • Share the link via WhatsApp, email, or your website.
  • Receive instant payments (fiat or stablecoin).
  • Get notified in real-time through webhooks.

Each payment link is unique and can include metadata, customer details, and custom redirect URLs.


Create a Payment Link

Generate a new payment link that can be shared with customers.

Endpoint

POST /payment-links

Headers

HeaderValueRequired
AuthorizationBearer YOUR_SECRET_KEYYes
Content-Typeapplication/jsonYes

Request Parameters

ParameterTypeRequiredDescriptionExample
amountnumberYesPayment amount (minor units for NGN, e.g., 50000 = ₦500.00)50000
currencystringYesCurrency code. Accepts: NGN, USDC"NGN"
titlestringYesPayment link title shown to customers (max 100 chars)"Invoice #1001"
descriptionstringNoAdditional details shown on payment page (max 500 chars)"Payment for order #1001"
expires_atstringNoISO 8601 datetime when link expires. If not provided, link never expires"2025-12-31T23:59:59Z"
success_urlstringNoURL to redirect customer after successful payment"https://example.com/success"
cancel_urlstringNoURL to redirect customer if payment is cancelled"https://example.com/cancel"
fieldsarrayNoAdditional fields to collect from customer. Options: "Address", "Phone", "Email"["Address", "Phone"]
metadataobjectNoCustom key-value data for your reference (max 10 keys, 500 chars per value){"order_id": "A-1001"}
slugstringNoCustom URL slug (alphanumeric, hyphen, underscore only, max 50 chars). If not provided, random slug is generated"invoice-1001"

Request Example

{
  "amount": 50000,
  "currency": "NGN",
  "title": "Invoice #1001",
  "description": "Payment for order #1001",
  "expires_at": "2025-12-31T23:59:59Z",
  "success_url": "https://example.com/success",
  "cancel_url": "https://example.com/cancel",
  "fields": ["Address", "Phone"],
  "metadata": {
    "order_id": "A-1001",
    "customer_id": "cus_123"
  },
  "slug": "invoice-1001"
}
curl -X POST "https://sandbox.tsara.ng/v1/payment-links" \
  -H "Authorization: Bearer YOUR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 50000,
    "currency": "NGN",
    "title": "Invoice #1001",
    "description": "Payment for order #1001",
    "expires_at": "2025-12-31T23:59:59Z",
    "success_url": "https://example.com/success",
    "cancel_url": "https://example.com/cancel",
    "fields": ["Address", "Phone"],
    "metadata": {
      "order_id": "A-1001"
    },
    "slug": "invoice-1001"
  }'

Response

{
  "status": "success",
  "status_code": 200,
  "message": "Payment Link Created",
  "url": "https://pay.usetsara.com/invoice-1001",
  "data": {
    "uid": "uid_8658062823",
    "id": "plink_8658062823",
    "url": "https://pay.usetsara.com/invoice-1001",
    "amount": 50000,
    "currency": "NGN",
    "title": "Invoice #1001",
    "status": "active",
    "expires_at": "2025-12-31T23:59:59Z",
    "created_at": "2025-01-31T10:30:00Z"
  }
}

Response Fields

FieldTypeDescription
statusstringRequest status. Always "success" for successful requests
status_codenumberHTTP status code. 200 for success
messagestringHuman-readable message
urlstringDirect payment URL to share with customers
data.uidstringUnique identifier (legacy format, use id instead)
data.idstringPayment link ID (use this for API calls)
data.urlstringPayment URL (same as top-level url)
data.amountnumberPayment amount
data.currencystringCurrency code
data.titlestringPayment link title
data.statusstringLink status. Options: active, disabled, expired
data.expires_atstringExpiration datetime (null if no expiration)
data.created_atstringCreation timestamp

Field Clarification: uid vs id

  • uid - Legacy identifier format (e.g., uid_8658062823)
  • id - Current identifier format (e.g., plink_8658062823)
  • Both refer to the same payment link
  • Use id for all API operations (retrieve, disable, etc.)

💡 The returned url can be shared directly with customers to collect payments.

Error Responses

{
  "success": false,
  "status_code": 400,
  "error": {
    "code": "validation_error",
    "message": "Invalid request parameters",
    "details": [
      {
        "field": "amount",
        "message": "Amount must be greater than 0"
      }
    ]
  }
}

Common Errors

Status CodeError CodeDescription
400validation_errorInvalid request parameters (check details array)
400invalid_currencyCurrency not supported. Use NGN or USDC
400slug_already_existsCustom slug is already in use
401unauthorizedInvalid or missing API key
429rate_limit_exceededToo many requests. Retry after delay

Retrieve a Payment Link

Get details of a specific payment link by its ID.

Endpoint

GET /payment-links?id={payment_link_id}

Query Parameters

ParameterTypeRequiredDescription
idstringYesPayment link ID (format: plink_* or uid_*)

Example Request

curl -X GET "https://sandbox.tsara.ng/v1/payment-links?id=plink_8658062823" \
  -H "Authorization: Bearer YOUR_SECRET_KEY"

Response

{
  "success": true,
  "status": "success",
  "status_code": 200,
  "message": "Payment link retrieved",
  "data": {
    "id": "plink_8658062823",
    "uid": "uid_8658062823",
    "amount": 50000,
    "currency": "NGN",
    "title": "Invoice #1001",
    "description": "Payment for order #1001",
    "status": "active",
    "url": "https://pay.usetsara.com/invoice-1001",
    "success_url": "https://example.com/success",
    "cancel_url": "https://example.com/cancel",
    "fields": ["Address", "Phone"],
    "metadata": {
      "order_id": "A-1001"
    },
    "expires_at": "2025-12-31T23:59:59Z",
    "created_at": "2025-01-31T10:30:00Z",
    "updated_at": "2025-01-31T10:30:00Z",
    "payments_count": 0,
    "total_paid": 0
  }
}

Response Fields

FieldTypeDescription
payments_countnumberNumber of successful payments received through this link
total_paidnumberTotal amount paid through this link (in minor units)

Status Values

StatusDescription
activeLink is active and accepting payments
disabledLink has been manually disabled
expiredLink has passed its expiration date
completedLink has received payment (if single-use)

List Payment Links

Retrieve all payment links with pagination support.

Endpoint

GET /payment-links?page={page}&limit={limit}

Query Parameters

ParameterTypeRequiredDefaultDescription
pagenumberNo1Page number (starts at 1)
limitnumberNo20Items per page (max: 100)
statusstringNo-Filter by status: active, disabled, expired
currencystringNo-Filter by currency: NGN, USDC

Example Request

curl -X GET "https://sandbox.tsara.ng/v1/payment-links?page=1&limit=20&status=active" \
  -H "Authorization: Bearer YOUR_SECRET_KEY"

Response

{
  "success": true,
  "status": "success",
  "status_code": 200,
  "message": "Payment links retrieved",
  "data": [
    {
      "id": "plink_001",
      "uid": "uid_001",
      "amount": 25000,
      "currency": "NGN",
      "title": "Invoice #1001",
      "status": "active",
      "url": "https://pay.usetsara.com/invoice-1001",
      "created_at": "2025-01-30T10:00:00Z",
      "payments_count": 0,
      "total_paid": 0
    },
    {
      "id": "plink_002",
      "uid": "uid_002",
      "amount": 100000,
      "currency": "NGN",
      "title": "Product Purchase",
      "status": "completed",
      "url": "https://pay.usetsara.com/product-xyz",
      "created_at": "2025-01-29T15:30:00Z",
      "payments_count": 1,
      "total_paid": 100000
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 2,
    "total_pages": 1,
    "has_more": false
  }
}

Pagination Fields

FieldTypeDescription
pagenumberCurrent page number
limitnumberItems per page
totalnumberTotal number of payment links
total_pagesnumberTotal number of pages
has_morebooleanWhether there are more pages available

Disable a Payment Link

Disable a payment link to stop accepting payments. This action is irreversible.

Endpoint

POST /payment-links/status

Request Parameters

ParameterTypeRequiredDescription
uidstringYesPayment link ID (accepts both uid_* and plink_* formats)
statusstringYesMust be "disabled"

Example Request

curl -X POST "https://sandbox.tsara.ng/v1/payment-links/status" \
  -H "Authorization: Bearer YOUR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "disabled",
    "uid": "plink_8658062823"
  }'

Response

{
  "success": true,
  "status": "success",
  "status_code": 200,
  "message": "Payment link disabled",
  "data": {
    "id": "plink_8658062823",
    "uid": "uid_8658062823",
    "status": "disabled",
    "disabled_at": "2025-01-31T12:00:00Z"
  }
}

⚠️ Note: Disabled payment links cannot be re-enabled. Create a new payment link if needed.


Webhook Notifications

When a customer completes payment through a link, Tsara sends a payment.success or payment.failed event to your configured webhook endpoint.

Event Types

EventDescription
payment.successPayment completed successfully
payment.failedPayment attempt failed
payment.pendingPayment is being processed (for bank transfers)

Example Payload: payment.success

{
  "id": "evt_456",
  "type": "payment.success",
  "created_at": "2025-01-31T12:00:00Z",
  "data": {
    "payment": {
      "id": "pay_001",
      "payment_link_id": "plink_8658062823",
      "amount": 50000,
      "currency": "NGN",
      "reference": "order_001",
      "status": "success",
      "customer": {
        "email": "[email protected]",
        "phone": "08012345678",
        "address": "123 Main St, Lagos"
      },
      "metadata": {
        "order_id": "A-1001"
      },
      "paid_at": "2025-01-31T11:59:30Z"
    }
  }
}

Webhook Payload Fields

FieldTypeDescription
idstringUnique event ID
typestringEvent type
created_atstringEvent timestamp
data.payment.idstringPayment transaction ID
data.payment.payment_link_idstringPayment link that generated this payment
data.payment.referencestringYour custom reference (from metadata)
data.payment.customerobjectCustomer details collected during payment
data.payment.metadataobjectMetadata you attached to the payment link

Verifying Webhook Signatures

Always verify webhook signatures to ensure requests are from Tsara:

const crypto = require('crypto');

function verifyWebhook(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha512', secret)
    .update(payload)
    .digest('hex');

  return signature === expected;
}

const isValid = verifyWebhook(
  req.body,
  req.headers['x-tsara-signature'],
  process.env.TSARA_WEBHOOK_SECRET
);

if (!isValid) {
  return res.status(400).send('Invalid signature');
}

Use Cases & Examples

Single-Use Payment Link

Create a payment link that automatically disables after one successful payment:

{
  "amount": 150000,
  "currency": "NGN",
  "title": "Invoice #INV-2025-001",
  "description": "Website Design - Final Payment",
  "metadata": {
    "invoice_id": "INV-2025-001",
    "auto_disable": "true"
  }
}

Monitor the payment.success webhook and disable the link:

app.post('/webhooks/tsara', (req, res) => {
  const event = req.body;

  if (event.type === 'payment.success') {
    const autoDisable = event.data.payment.metadata.auto_disable;

    if (autoDisable === 'true') {
      await disablePaymentLink(event.data.payment.payment_link_id);
    }
  }

  res.sendStatus(200);
});

Subscription Payment Link

Create a reusable link for recurring payments:

{
  "amount": 5000,
  "currency": "NGN",
  "title": "Monthly Subscription - Pro Plan",
  "description": "Renews monthly",
  "slug": "pro-plan-monthly",
  "metadata": {
    "plan": "pro",
    "billing_cycle": "monthly"
  }
}

USDC Payment Link

Accept stablecoin payments:

{
  "amount": 50,
  "currency": "USDC",
  "title": "USDC Payment",
  "description": "Pay 50 USDC",
  "metadata": {
    "crypto_payment": "true"
  }
}

Note: USDC amounts are in full units (e.g., 50 = 50 USDC), not minor units.


Tips & Best Practices

  1. Always verify payment status using the Verify Payment API (GET /payments?reference={reference}) before fulfilling orders, even after receiving a webhook.

  2. Use metadata to track payments. Add order IDs, invoice numbers, or any custom data you need:

    "metadata": {
      "order_id": "ORD-123",
      "customer_id": "cus_456",
      "source": "website"
    }
  3. Handle webhooks asynchronously. Process them in a queue to avoid blocking your webhook endpoint:

    app.post('/webhooks/tsara', (req, res) => {
      queue.add('process-payment', req.body);
      res.sendStatus(200);
    });
  4. Set expiration dates for time-sensitive payments:

    "expires_at": "2025-02-05T23:59:59Z"
  5. Use custom slugs for branded payment URLs:

    "slug": "invoice-jan-2025-acme-corp"

    Results in: https://pay.usetsara.com/invoice-jan-2025-acme-corp

  6. Test in sandbox before going live. Use sandbox API keys and test the full payment flow.

  7. Monitor link performance using the payments_count and total_paid fields from the retrieve endpoint.

  8. Collect additional information using the fields parameter:

    "fields": ["Address", "Phone", "Email"]

    This data will be included in webhook payloads.


Troubleshooting

Payment link shows as expired immediately

Cause: expires_at is in the past or incorrect format.

Solution: Use ISO 8601 format and ensure the date is in the future:

"expires_at": "2025-12-31T23:59:59Z"

Custom slug not working

Cause: Slug contains invalid characters or is already in use.

Solution: Use only alphanumeric characters, hyphens, and underscores. Check if slug is available first:

curl -X GET "https://sandbox.tsara.ng/v1/payment-links?slug=my-custom-slug"

Webhook not receiving events

Cause: Webhook URL not configured or signature validation failing.

Solution:

  1. Configure webhook URL in dashboard: Settings → Webhooks
  2. Verify signature using HMAC-SHA512
  3. Ensure endpoint returns 200 OK within 10 seconds

Amount showing incorrectly

Cause: NGN amounts should be in kobo (minor units).

Solution:

  • For ₦500.00, use 50000 (500 × 100)
  • For USDC, use full units: 50 = 50 USDC

Related Pages