Canapi Documentation

Getting Started

Canapi lets you attach a charitable cause to any action in your app or website - a purchase, a signup, a completed task. When your users do something, Canapi logs a contribution toward a cause on your behalf. Your users see the impact. The contribution is tracked, billed to you monthly, and fulfilled by the cause's provider.

Getting up and running takes about 15 minutes and requires no changes to your existing infrastructure beyond a single API call.

Before you begin, you will need:

  • A Canapi account - sign in through the login page or request a Beta account via email
  • An active Canapi instance configured with a cause
  • An API key and signing secret generated for that instance

Getting Your API Key and Signing Secret

Each Canapi instance has its own API key and signing secret, generated together. Both are generated from the instance page in your portal.

  1. Log in and navigate to Instances
  2. Select the instance you want to integrate
  3. Under API Key, click Regenerate Key and confirm
  4. Copy both values immediately - they are shown once and never stored

Keep your API key and signing secret secret. Neither should ever be committed to source control or exposed in client-side code. Store both as environment variables on your server, or in your platform's secrets manager.

Your key will look like this:

xxx_aLotOfNumb3rsAndCharact3rsThatL00ksScaryButIsJustSecur3

Your signing secret is a separate value shown alongside it at the same time.

Making a Contribution

When something meaningful happens in your app - a completed order, a finished workout, a booked appointment - send a single POST request to the Canapi contribute endpoint. Canapi handles the rest.

Endpoint

POST https://us-central1-terrabyte-canapi.cloudfunctions.net/contribute

Authentication

Pass your API key as a Bearer token in the Authorization header:

Authorization: Bearer xxx_your_api_key_here

Request signing

Every request must also be signed using your instance's signing secret, so that a leaked API key alone isn't enough to make a request on your behalf. Send three additional headers:

  • X-Canapi-Timestamp - the current Unix timestamp, in seconds
  • X-Canapi-Idempotency-Key - a string unique to this specific contribution. Reusing one rejects the request as a duplicate rather than logging it again
  • X-Canapi-Signature - HMAC-SHA256 of the string {timestamp}.{idempotencyKey}, using your signing secret as the HMAC key, hex-encoded

Requests with a timestamp more than 5 minutes old (or in the future) are rejected, so make sure your server's clock is accurate.

Choosing your idempotency key matters. A random value (e.g. a UUID) on every call only protects against someone else intercepting and replaying your request - it does nothing to stop your own code from accidentally billing the same real-world event twice (a retry, a double-click, a webhook firing more than once). To protect against that too, derive the key from something stable and unique to the event itself - your internal order ID, transaction ID, or similar - so that if your own code calls contribute twice for the same order, the second call is rejected as a duplicate instead of logging (and billing) a second contribution.

Example — JavaScript (Node.js)

import crypto from 'crypto';

const timestamp = Math.floor(Date.now() / 1000).toString();
// Use your own stable order/transaction ID here (not a random UUID) so that
// if this code ever runs twice for the same order, the second call is
// rejected as a duplicate instead of billing the order a second time.
const idempotencyKey = yourOrderId;
const signature = crypto
  .createHmac('sha256', process.env.CANAPI_SIGNING_SECRET)
  .update(`${timestamp}.${idempotencyKey}`)
  .digest('hex');

const response = await fetch(
  'https://us-central1-terrabyte-canapi.cloudfunctions.net/contribute',
  {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.CANAPI_API_KEY}`,
      'X-Canapi-Timestamp': timestamp,
      'X-Canapi-Idempotency-Key': idempotencyKey,
      'X-Canapi-Signature': signature
    }
  }
);

const data = await response.json();

if (data.status === 'success') {
  console.log(`Contribution logged - action ID: ${data.actionId}`);
  console.log(`Cause: ${data.causeId}`);
  // e.g. "Your order helped plant a tree!"
}

Example — cURL

TIMESTAMP=$(date +%s)
# Use your own stable order/transaction ID here (not a random value) so a
# retried call for the same order gets rejected as a duplicate, not re-billed.
IDEMPOTENCY_KEY="your-order-id"
SIGNATURE=$(printf "%s.%s" "$TIMESTAMP" "$IDEMPOTENCY_KEY" | openssl dgst -sha256 -hmac "$CANAPI_SIGNING_SECRET" | sed 's/^.* //')

curl -X POST https://us-central1-terrabyte-canapi.cloudfunctions.net/contribute \
  -H "Authorization: Bearer $CANAPI_API_KEY" \
  -H "X-Canapi-Timestamp: $TIMESTAMP" \
  -H "X-Canapi-Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "X-Canapi-Signature: $SIGNATURE"

Where to place the call

Call the endpoint at the moment the meaningful action is confirmed - after a payment is captured, after a form is successfully submitted, after an order is fulfilled. Avoid calling it speculatively (e.g. when a user adds something to a cart) since contributions are billed and logged as completed actions. Always call it from your server, never from client-side code - your API key and signing secret must never be exposed to a browser.

Labeling by domain (optional)

Your instance settings include an optional "Allowed Domains" list. This is for your own labeling and analytics only - it has no effect on which requests are accepted. Every contribution's authenticity comes from the API key and signature, not from where it says it's calling from.

Rate limiting

Each instance can optionally be configured with an hourly contribution cap in its dashboard settings. Requests that would exceed it return 429 and are not logged as billable actions - they don't count against the cap themselves, so once the hourly window resets, contributions resume normally without any action needed on your part.

Response Reference

A successful contribution returns HTTP 200 with the following JSON body:

{
  "status": "success",
  "actionId": "abc123xyz",
  "causeId": "plant-a-tree"
}
Field Type Description
status string Always "success" on a 200 response
actionId string Unique ID for this contribution. Keep this if you want to reference the action later or display it to your user
causeId string The cause tied to your instance - use this to show your users what their action supported (e.g. "Your order planted a tree")

Error Reference

All error responses return JSON with a single error field describing the problem.

Status Error What it means
401 Missing API key No Authorization header was sent
401 Missing signature headers One or more of X-Canapi-Timestamp, X-Canapi-Idempotency-Key, or X-Canapi-Signature was not sent
401 Invalid API key The key doesn't match any active instance - check for typos or regenerate your key
403 Instance is not active The instance tied to this key is inactive or archived
401 Invalid or expired timestamp X-Canapi-Timestamp is more than 5 minutes old or in the future - check your server's clock
401 Invalid signature X-Canapi-Signature doesn't match what we computed - check you're signing {timestamp}.{idempotencyKey} with the correct signing secret
409 Duplicate request X-Canapi-Idempotency-Key was already used - the request was not logged again as a billable action
429 Rate limit exceeded Your instance has hit its configured hourly contribution cap - the request was not logged as a billable action
500 Internal server error Something went wrong on our end - try again, and contact support if the problem persists

<script type="text/javascript" data-canapi-init data-host="https://www.app.canapi.io" src="https://app.canapi.io/js/canapi.js"></script>

<div data-canapi-click-trigger data-key="XX-XXXX-XXXX">