Migrating from v2 to v3
This guide covers every breaking change you need to move an existing ekko Climate API integration from v2 to v3. It applies to v2.5 and v2.6 integrations; where the two differ, the difference is flagged inline. Work through each section in order, then validate against staging before cutting over.
Who this is for
Commercial partners with a working v2.5 or v2.6 integration. If you're starting fresh, see Getting started instead.
For v2.5, two things differ from v2.6: your funds endpoint is POST /projects/portfolio, not POST /funds, and the quote reference is new. v2.5 actioned funds without one, so treat the quote step as a new requirement rather than a rename.
Key changes
Two v3 changes affect how your integration is structured.
- One organisation model replaces brands and products. v3 has a single
organisationtype that nests into hierarchies throughparentOrganisationId. There's no v3 equivalent of your oldproductIdorbrandIdvalue, you work entirely withorganisationId. - A quote fixes the price for each allocation. Every funds allocation references a
quoteId, and the quote locks the pricing and tax rates calculated at quote time. Calculate a quote, then allocate against it. In v2.6 this is thecalcReferenceyou already pass, renamed. In v2.5 it's new.
What's changed at a glance
Each row links to the section explaining the change.
| Area | v2 | v3 |
|---|---|---|
| Authentication | X-Api-Key header | Authorization: Bearer |
| Base URL | api.ekko.earth/v2.5 or /v2.6 | api.ekko.earth/v3 |
| Organisation identifier | productId | organisationId |
| Consumer identifier | customerId (string) | consumer object |
| Transaction amount | amount.value | transaction.amount |
| Credits field | compensation | credits |
| Address fields | street, country | line1, countryCode |
| Footprint calculation | POST /impact_calculator | POST /quotes/carbon |
| Quote reference | calcReference | quoteId |
| Funds allocation | POST /funds (POST /projects/portfolio in v2.5) | POST /funds/allocations |
| Funds reference | reference | fundsAllocationId |
| Reversals | POST /reversal (body param) | POST /funds/allocations/{id}/reverse (path param) |
| Reconciliation | POST /impact_records | GET /funds/allocations/impact and GET /funds/allocations/profit-share |
| Projects | GET /products/{productId}/projects | GET /impact-partners/projects |
| SDK sessions | checkoutSessionId | sessionId, quoteId, expiresAt |
| Success status codes | 200 for all POSTs | 201 for all POSTs |
| Error envelope | Nested error object with statusCode, message, errorName, details, path, requestId, timestamp | Flat {error, code} |
Authentication
v2 used a custom header:
X-Api-Key: your_api_key_herev3 uses a standard Bearer token in the Authorization header:
Authorization: Bearer your_api_key_hereUpdate every request to use the new header format. You will need to generate a new API key. For full details on key management and IP allowlisting, see Authentication and security.
Base URL
Replace your v2 base URL across your entire integration.
| Environment | URL |
|---|---|
| Production (v2.5) | https://api.ekko.earth/v2.5 |
| Production (v2.6) | https://api.ekko.earth/v2.6 |
| Production (v3) | https://api.ekko.earth/v3 |
| Sandbox (v3) | https://staging.api.ekko.earth/v3 |
Status codes
Every POST endpoint that returned 200 OK in v2 returns 201 Created in v3. This applies to /quotes/carbon, /funds/allocations, /funds/allocations/{id}/reverse, /checkout/sessions and /organisations. GET endpoints continue to return 200.
If your integration checks for an exact status code (status === 200), update it to accept 201 for POST responses or check the 2xx range.
Terminology and identifier changes
These changes affect every endpoint. Apply them first, before tackling individual endpoint sections.
productId → organisationId
Every v2 request that included a productId now uses organisationId. organisationId is a separate v3 identifier with no relationship to your v2 productId. Retrieve your organisationId from ekko Hub before you start (see Finding your organisationId).
customerId → consumer object
v2 identified consumers as a flat string. v3 uses a consumer object so tax jurisdictions and project matching can use location data.
v2:
{
"customerId": "customer_a"
}v3:
{
"consumer": {
"reference": "customer_a",
"countryCode": "GBR",
"postalCode": "SW1A 1AA",
"city": "London"
}
}reference and countryCode are required. countryCode is an ISO 3166-1 alpha-3 code (GBR, USA, AUS). postalCode, city and state are optional, but providing them unlocks location-matched project filtering (see New in v3).
amount → transaction object
v2 wrapped the transaction amount in an amount object with a value field. v3 uses a transaction object with the amount inline.
v2:
{
"amount": {
"value": 169.99,
"currencyCode": "GBP"
}
}v3:
{
"transaction": {
"amount": 169.99,
"currencyCode": "GBP"
}
}compensation → credits
The field name compensation is replaced by credits across all v3 request and response bodies. v3 treats credits and contributions as two distinct mechanisms: credits are tied to specific carbon credit projects, contributions go to impact partners. The rename applies to the quote response, the funds allocation request and the funds allocation response.
Address schema
v3 uses a consistent address structure across all endpoints. Two things change from v2:
street(andaddress1/address2/address3in onboarding requests) becomesline1,line2,line3countrybecomescountryCodeand requires a three-character ISO 3166-1 alpha-3 code
v2:
{
"address": {
"street": "214 Oxford Street",
"city": "London",
"postalCode": "W1D 1LA",
"country": "GBR"
}
}v3:
{
"address": {
"line1": "214 Oxford Street",
"city": "London",
"postalCode": "W1D 1LA",
"countryCode": "GBR"
}
}line2 and line3 are optional and nullable.
Organisation management
Request changes
v2 onboarded brands through POST /organisations/brand/onboarding. That endpoint is removed in v3. Use a single POST /organisations endpoint instead:
{
"parentOrganisationId": "9e1d3a52-0e3d-4a9f-8b6e-1c2d3e4f5a6b",
"tradingName": "Blossom & Bloom",
"legalName": "BB Flowers Ltd",
"address": {
"line1": "Ground floor",
"line2": "45 Rosewood Avenue",
"city": "London",
"postalCode": "SW1A 1AA",
"countryCode": "GBR"
},
"currencyCode": "GBP",
"mcc": "5992",
"organisationReference": "MID1234567"
}v3 supports nested organisation hierarchies. You can model structures such as platform → regional office → individual organisation by chaining parentOrganisationId references. Each organisation inherits settings from its parent unless explicitly overridden.
Storing an MCC at organisation level
Setting
mccon the organisation lets you omit themerchantobject from quote and SDK session requests.
Field mapping from v2
| v2 field | v3 field |
|---|---|
tradingName | tradingName |
legalName | legalName |
currency | currencyCode |
billingEmail | billing.email |
companyRegistrationNumber | billing.companyRegistrationNumber |
vatNumber | billing.taxNumber |
brandReference | organisationReference |
brandOwner | Set parentOrganisationId to the owner organisation's ID |
brandBilling: true | Include a billing object with required billing fields |
brandBilling: false | Omit the billing object; the nearest billable parent is invoiced |
tradingAddress and legalAddress | Single address field |
Response changes
v2 returned {acknowledged, message, organisationId, brandId, productId, projects}. v3 returns the organisation record directly with organisationId as the primary identifier. brandId and productId are removed. Store the organisationId and use it in all subsequent requests. You can retrieve an existing organisation using GET /organisations/{id}.
The success status code changes from 200 to 201.
Calculating footprint
Request changes
POST /impact_calculator becomes POST /quotes/carbon. The request adopts the identifier and structural changes from Terminology and identifier changes, and adds a required locale field.
v2:
{
"productId": "c8f514a4-beb9-4d8d-8673-da82feff731e",
"customerId": "customer_a",
"amount": {
"value": 169.99,
"currencyCode": "GBP"
},
"merchant": {
"mcc": "5651",
"name": "Oxford Street Clothing Co.",
"address": {
"street": "214 Oxford Street",
"city": "London",
"postalCode": "W1D 1LA",
"country": "GBR"
}
}
}v3:
{
"organisationId": "2f6d6b1f-1c6a-4e9a-9c15-7b8a0db1a2c3",
"locale": "en-GB",
"consumer": {
"reference": "customer_a",
"countryCode": "GBR",
"postalCode": "W1D 1LA",
"city": "London"
},
"merchant": {
"name": "Oxford Street Clothing Co.",
"mcc": "5651",
"address": {
"line1": "214 Oxford Street",
"city": "London",
"postalCode": "W1D 1LA",
"countryCode": "GBR"
}
},
"transaction": {
"amount": 169.99,
"currencyCode": "GBP"
}
}locale is required. If the requested locale isn't supported, the API defaults to en-GB. calcReference is renamed to quoteId. You must pass quoteId when allocating funds, and it locks in pricing and tax rates at the time of calculation.
Response changes
The v2 carbonImpact response (with grams, ounces, equivalents, compensation and roundUp) becomes a three-part quote with footprint, credits and contribution:
footprintcontainsco2eGrams,co2eOuncesand localisedequivalentscreditscontains pricing (totalAmount,impactAmount,impactSalesTaxAmount,serviceFeeAmount,serviceFeeSalesTaxAmount) and impact partner detailscontributioncontains percentage-based pricing (impactPercentage,serviceFeePercentage,impactSalesTaxPercentage,serviceFeeSalesTaxPercentage) and impact partner details
The v2 roundUp object is not present in the v3 response. For round-up flows, apply the percentages from the contribution section to your chosen round-up amount. The success status code changes from 200 to 201. See Create a carbon quote for the full response reference.
Allocating funds
Request changes
POST /funds (POST /projects/portfolio in v2.5) becomes POST /funds/allocations. v3 separates credits from contributions: compensation is renamed to credits, and contributions target an impact partner via impactPartnerIds. The allocation is priced in the quote's currency.
v2.6:
{
"productId": "c8f514a4-beb9-4d8d-8673-da82feff731e",
"calcReference": "e9de9d42-74f1-400c-8bd2-fbc91944c2ef",
"compensation": {
"value": 0.47,
"currencyCode": "GBP"
},
"contribution": {
"value": 0.53,
"currencyCode": "GBP"
}
}v3:
{
"organisationId": "2f6d6b1f-1c6a-4e9a-9c15-7b8a0db1a2c3",
"quoteId": "d4e5f6a7-2b3c-4d8e-9f1a-5b6c7d8e9f0a",
"credits": {
"amount": 0.47
},
"contribution": {
"amount": 0.53,
"impactPartnerIds": ["9405545f-a850-4427-ab0d-10b5b734e925"]
}
}You must send at least one of credits or contribution. Both in the same request is allowed.
Routing without partner IDs
Credits always follow the projects in the quote. For contributions, omitting
impactPartnerIdsallocates across the impact partners the quote returned, which reflects the defaults configured on your organisation.
Response changes
The response shape is different in three ways that will break v2 parsers:
- Identifier: v2 returned
{acknowledged, reference, compensation, contribution}. v3 returnsfundsAllocationIdas the primary identifier. Theacknowledgedboolean is removed. - Granular breakdown: v3 returns
creditsandcontributionsarrays with per-line entries, each containing the impact amount, service fee and granular sales tax fields (impactSalesTaxAmount,impactSalesTaxRate,serviceFeeSalesTaxAmount,serviceFeeSalesTaxRate). - Tax liability: a new top-level
taxLiabilityfield (platformororganisation) indicates which party holds sales tax responsibility for the allocation.
The success status code changes from 200 to 201. See Allocate funds to a project for the full response example.
Reversals
Request changes
POST /reversal becomes POST /funds/allocations/{id}/reverse. v3 uses the fundsAllocationId as a path parameter. No request body is required:
POST /funds/allocations/b7e9c4a2-8f31-4d6e-a529-3c7b8e1f0d42/reverseResponse changes
v2 returned {acknowledged, reference, message}. v3 returns the same shape as a funds allocation response, with:
- All amounts negated
- The original
fundsAllocationIdpreserved - A new
reversalIdfield identifying the reversal record
The success status code changes from 200 to 201. See Reverse a funds allocation for the full response example.
SDK sessions
Checkout SDK
The endpoint path POST /checkout/sessions is unchanged.
Request changes
Apply the identifier changes from Terminology and identifier changes: productId → organisationId, customerId (string) → consumer object, amount → transaction.
Response changes
| v2.6 field | v3 field |
|---|---|
checkoutSessionId | sessionId |
calcReference | quoteId |
clientSecret | clientSecret (unchanged) |
| (not present) | expiresAt |
The session expires at the expiresAt timestamp. Create a new session if the current one has expired. The success status code changes from 200 to 201. See Create a checkout SDK session for the full response.
Reconciliation
v2's POST /impact_records becomes GET /funds/allocations/impact. It returns the same impact and service fee data, with the method changing from POST to GET and filters moving from the request body to query parameters. One thing moves: v2 returned the service fee split (clientServiceFeeShare and ekkoServiceFeeShare) inline on each record. In v3 that distribution lives on a separate GET /funds/allocations/profit-share endpoint, broken down across your organisation hierarchy.
Request changes
| v2 | v3 |
|---|---|
POST /impact_records with filters in body | GET /funds/allocations/impact with filters as query parameters |
POST /impact_records/transaction | GET /funds/allocations/{id}/impact |
v2 (POST body):
{
"dateTimeFrom": "2025-05-01T00:00:00Z",
"dateTimeTo": "2025-06-01T00:00:00Z",
"brandId": "9c7b2c51-36f0-4934-8428-6a77e9b01af2"
}v3 (GET query parameters):
GET /funds/allocations/impact?dateTimeFrom=2025-05-01T00:00:00Z&dateTimeTo=2025-06-01T00:00:00Z&fundsOrganisationId=2f6d6b1f-1c6a-4e9a-9c15-7b8a0db1a2c3v3 also adds a separate GET /funds/allocations/profit-share endpoint for the service fee distribution across the organisation hierarchy.
Response changes
The record shape is different in v3. The most important changes:
| v2 field | v3 equivalent |
|---|---|
transactionReferenceId | fundsAllocationId |
transactionDate | fundsDateTime |
brandId | fundsOrganisationId |
brandTradingName | No v3 equivalent |
customerId | consumerReference |
currency | fundsCurrencyCode |
impactAmount | amounts.impact |
serviceFee | amounts.serviceFee |
clientServiceFeeShare and ekkoServiceFeeShare | Moved to the new GET /funds/allocations/profit-share endpoint, broken down across your organisation hierarchy |
fundsFlow (receivable/payable) | Removed. Settlement direction is now determined by your configured settlement model (net split, gross split or invoice). See Reconciliation. |
recordType | Replaced by separate credits and contribution arrays on each record |
v3 records also add:
- An allocation-level
amountsobject (total,impact,impactTax,serviceFee,serviceFeeTax) that mirrors the quote response unitper line item (e.g.kgCO2e,bottles) with aquantityimpactPartnerIdper line item- A
taxLiabilityfield (platformororganisation) indicating sales tax responsibility. This is a new concept, not a rename offundsFlow
Reversal records appear inline when they fall within the requested date range, identified by reversal: true with all amounts negated. See Reconciliation for the full response reference.
Projects and impact partners
Request changes
GET /products/{productId}/projects is replaced by two endpoints:
| v3 endpoint | Purpose |
|---|---|
GET /impact-partners/projects | List projects across all impact partners |
GET /impact-partners | List impact partners with name, logo, descriptions and website |
Both endpoints support cursor-based pagination (startingAfter, endingBefore, limit).
Response changes
v2 returned a single response object with two arrays: compensation (carbon credit projects) and sustainability (contribution projects). Each project contained projectId, name, description, image, url and a unit object.
v3 returns a single flat list of projects. Each project adds the following fields:
type:carbon_credits,nature_creditsorcontributiontheme:pollution,climate_stress,land_useorwater_uselocation:countryCode,regionandstatesdgs: array of associated UN Sustainable Development Goalssubtype: for carbon credit projects (e.g.sequestration,carbon removal)shortDescriptionandlongDescriptionas separate fieldsimpactPartnerIdlinking each project to its impact partner
See Impact partners and projects for the current impact partner list.
Error responses
The error response envelope changes in v3. v2 returned a nested error object with multiple metadata fields. v3 returns a flat envelope with the error message and a stable code.
v2:
{
"error": {
"statusCode": 400,
"message": "Bad Request Exception",
"errorName": "BadRequestException",
"details": {
"message": [
"amount.value must be a positive number"
],
"error": "Bad Request",
"statusCode": 400
},
"path": "/v2.6/impact_calculator",
"requestId": "7AkD3H",
"timestamp": "2025-09-09T16:51:00.789Z"
}
}v3:
{
"error": "invalid request body: amount.value must be a positive number",
"code": "BAD_REQUEST"
}Key changes:
- The error is a flat object, not a nested one. Anywhere your code reads
error.statusCode,error.message,error.errorName,error.details,error.path,error.requestIdorerror.timestampmust be updated. codeis derived from the HTTP status code by default (BAD_REQUEST,UNAUTHORIZED,FORBIDDEN,NOT_FOUND,CONFLICT,REQUEST_ENTITY_TOO_LARGE,UNPROCESSABLE_ENTITY,INTERNAL_SERVER_ERROR). Some errors return a more granular domain code (for exampleMAX_KEYS_REACHEDinstead ofCONFLICT) when additional context is available.- v3 error messages are lowercase.
Use the HTTP status code as your primary signal for handling. Read code when you need to react to a specific failure mode. See Errors and error handling for the full code reference.
Validating your migration
Before cutting over, run the v3 integration check Postman collections against the staging environment. The Direct API check runs 17 requests across organisation onboarding, impact partners, quotes, funds allocations, reversal and reconciliation. The Checkout SDK check runs 14 requests across the same areas plus session creation. Each request includes automated pass/fail assertions.
A full collection run takes seconds. See Test cases for setup and credentials.
Finding your organisationId
Your v3 organisationId is shown in ekko Hub. To find it:
- Log in to ekko Hub (hub.ekko.earth for production, staging.hub.ekko.earth for sandbox)
- Click Console in the left sidebar (under Developer)
- Your organisation ID is shown at the top of the page, above the API key list
For Postman, the integration check collections expect this value in the org_id environment variable. It must be your root organisation ID, not a child organisation ID.
New in v3
v3 introduces two additions worth knowing about as you migrate. Neither blocks your migration, and you can opt in once you're on v3.
- Location-matched projects: set
matchConsumerLocationto true insideprojectFilters(undercreditsorcontribution) on quote and session requests to prioritise projects close to the consumer's location. The quote response includesconsumerLocationMatchAccuracyindicating match precision - Donation tax receipts: set
issueTaxReceipt: truewith aconsumerEmailonPOST /funds/allocationsto have ekko send a tax receipt to the consumer (US charity compliance)
Migration checklist
Work through the changes above, then run this end-to-end before you cut over:
- Review the terminology. Read Key concepts so the v3 vocabulary is settled. The biggest shifts are
compensation→creditsand the introduction oforganisation(replacing the brand/product model). - Update authentication and endpoints. Send
Authorization: Bearerinstead ofX-Api-Key, update the base URL, and apply the field, status code and error-handling changes from the sections above. - Run a parallel sandbox test. Point your integration at the v3 sandbox (
https://staging.api.ekko.earth/v3) and run your full transaction flow alongside your existing v2 traffic. Compare quote outputs, allocation records and reconciliation totals for the same inputs across both versions, and treat any unexplained mismatch as a blocker. The Direct API and Checkout SDK integration check collections automate this and give you a pass/fail signal in seconds (see Test cases). - Cut over. Once sandbox parity is solid, swap the production base URL to
https://api.ekko.earth/v3and ship. v2 keeps running in parallel, so you can roll back without coordination if you spot an issue.
Updated 11 days ago
