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.
- Log in and navigate to Instances
- Select the instance you want to integrate
- Under API Key, click Regenerate Key and confirm
- 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 secondsX-Canapi-Idempotency-Key- a string unique to this specific contribution. Reusing one rejects the request as a duplicate rather than logging it againX-Canapi-Signature-HMAC-SHA256of 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 |