Open Collections

Setup

Configure the URLs we'll deliver payments to.

Requirements

  • Open Collections enabled on your account (contact sales)
  • Every SIM assigned to one of your numbers (below)
  • A publicly reachable HTTPS endpoint
  • A secret API key (hpn_live_sk_*) with the webhooks:manage scope, or a dashboard session

Step 1: say which SIM each number is in

Set it in Payment methods, or on the number:

Bash
POST /v1/payment-methods
Content-Type: application/json

{
  "provider": "momo_mtn",
  "phone_number": "0201234567",
  "account_name": "Adom Ventures",
  "sim": { "device_id": "550e8400-e29b-41d4-a716-446655440000", "slot_index": 0 }
}

PATCH /v1/payment-methods/{id} takes the same field. Omit it to leave the SIM alone, null to take the number out, a value to move it. Moving clears the old SIM; one that already holds another number is refused.

GET /v1/payment-methods/sims lists the phones and slots to choose from.

Endpoint creation is refused until every SIM has a number:

Bash
GET /v1/open-collections/lines
JSON
{
  "success": true,
  "data": {
    "ready": false,
    "unplaced_payments": 3,
    "phones": [
      {
        "device_id": "550e8400-e29b-41d4-a716-446655440000",
        "name": "Shop counter",
        "reads_payments": true,
        "sims": [
          {
            "slot_index": 0,
            "network": "MTN",
            "unplaced_payments": 0,
            "line": {
              "payment_method_id": "8f14e45f-ceea-467a-9d0f-2b6f0e4c4b71",
              "phone_number": "0201234567",
              "provider": "MTN MoMo",
              "account_name": "Adom Ventures",
              "merchant_code": "554120"
            }
          },
          { "slot_index": 1, "network": "Telecel", "unplaced_payments": 3, "line": null }
        ]
      }
    ]
  }
}

ready is true when every SIM has a number. A phone that has never read a payment message does not hold it up.

unplaced_payments counts payments from the last 30 days that matched none of your numbers. Non-zero while ready is true usually means a SIM moved phones, and those payments went to your catch-all rule.

Step 2: create an endpoint

Bash
POST /v1/open-collections/endpoints
Authorization: Bearer hpn_live_sk_xxxxxxxxxxxxx
Content-Type: application/json

{
  "url": "https://your-server.com/harpoon/payments",
  "description": "Shop orders"
}
JSON
{
  "success": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "url": "https://your-server.com/harpoon/payments",
    "description": "Shop orders",
    "is_active": true,
    "is_sandbox": false,
    "auto_paused_at": null,
    "last_success_at": null,
    "last_failure_at": null,
    "consecutive_failures": 0,
    "created_at": "2026-09-04T12:00:00Z",
    "updated_at": "2026-09-04T12:00:00Z"
  },
  "secret": "whsec_xxxxxxxxxxxxxxxxxxxxx"
}

The secret is returned here and on rotation only. Reveal it later from the dashboard.

A new endpoint takes any payment no other rule claims. Narrow it in Routing.

Set "is_sandbox": true for an endpoint that only receives test payments.

Step 3: test it

Bash
POST /v1/open-collections/endpoints/{id}/test
Authorization: Bearer hpn_live_sk_xxxxxxxxxxxxx
JSON
{
  "success": true,
  "status_code": 200,
  "response_body": "{"received": true}",
  "message": "Endpoint accepted the test delivery."
}

Same delivery path and signature as a real event, with made-up data and "test": true in the payload.