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 organisation type that nests into hierarchies through parentOrganisationId. There's no v3 equivalent of your old productId or brandId value, you work entirely with organisationId.
  • 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 the calcReference you already pass, renamed. In v2.5 it's new.

What's changed at a glance

Each row links to the section explaining the change.

Areav2v3
AuthenticationX-Api-Key headerAuthorization: Bearer
Base URLapi.ekko.earth/v2.5 or /v2.6api.ekko.earth/v3
Organisation identifierproductIdorganisationId
Consumer identifiercustomerId (string)consumer object
Transaction amountamount.valuetransaction.amount
Credits fieldcompensationcredits
Address fieldsstreet, countryline1, countryCode
Footprint calculationPOST /impact_calculatorPOST /quotes/carbon
Quote referencecalcReferencequoteId
Funds allocationPOST /funds (POST /projects/portfolio in v2.5)POST /funds/allocations
Funds referencereferencefundsAllocationId
ReversalsPOST /reversal (body param)POST /funds/allocations/{id}/reverse (path param)
ReconciliationPOST /impact_recordsGET /funds/allocations/impact and GET /funds/allocations/profit-share
ProjectsGET /products/{productId}/projectsGET /impact-partners/projects
SDK sessionscheckoutSessionIdsessionId, quoteId, expiresAt
Success status codes200 for all POSTs201 for all POSTs
Error envelopeNested error object with statusCode, message, errorName, details, path, requestId, timestampFlat {error, code}

Authentication

v2 used a custom header:

X-Api-Key: your_api_key_here

v3 uses a standard Bearer token in the Authorization header:

Authorization: Bearer your_api_key_here

Update 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.

EnvironmentURL
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 (and address1/address2/address3 in onboarding requests) becomes line1, line2, line3
  • country becomes countryCode and 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 mcc on the organisation lets you omit the merchant object from quote and SDK session requests.

Field mapping from v2

v2 fieldv3 field
tradingNametradingName
legalNamelegalName
currencycurrencyCode
billingEmailbilling.email
companyRegistrationNumberbilling.companyRegistrationNumber
vatNumberbilling.taxNumber
brandReferenceorganisationReference
brandOwnerSet parentOrganisationId to the owner organisation's ID
brandBilling: trueInclude a billing object with required billing fields
brandBilling: falseOmit the billing object; the nearest billable parent is invoiced
tradingAddress and legalAddressSingle 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:

  • footprint contains co2eGrams, co2eOunces and localised equivalents
  • credits contains pricing (totalAmount, impactAmount, impactSalesTaxAmount, serviceFeeAmount, serviceFeeSalesTaxAmount) and impact partner details
  • contribution contains 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 impactPartnerIds allocates 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:

  1. Identifier: v2 returned {acknowledged, reference, compensation, contribution}. v3 returns fundsAllocationId as the primary identifier. The acknowledged boolean is removed.
  2. Granular breakdown: v3 returns credits and contributions arrays with per-line entries, each containing the impact amount, service fee and granular sales tax fields (impactSalesTaxAmount, impactSalesTaxRate, serviceFeeSalesTaxAmount, serviceFeeSalesTaxRate).
  3. Tax liability: a new top-level taxLiability field (platform or organisation) 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/reverse

Response changes

v2 returned {acknowledged, reference, message}. v3 returns the same shape as a funds allocation response, with:

  • All amounts negated
  • The original fundsAllocationId preserved
  • A new reversalId field 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: productIdorganisationId, customerId (string) → consumer object, amounttransaction.

Response changes

v2.6 fieldv3 field
checkoutSessionIdsessionId
calcReferencequoteId
clientSecretclientSecret (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

v2v3
POST /impact_records with filters in bodyGET /funds/allocations/impact with filters as query parameters
POST /impact_records/transactionGET /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-7b8a0db1a2c3

v3 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 fieldv3 equivalent
transactionReferenceIdfundsAllocationId
transactionDatefundsDateTime
brandIdfundsOrganisationId
brandTradingNameNo v3 equivalent
customerIdconsumerReference
currencyfundsCurrencyCode
impactAmountamounts.impact
serviceFeeamounts.serviceFee
clientServiceFeeShare and ekkoServiceFeeShareMoved 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.
recordTypeReplaced by separate credits and contribution arrays on each record

v3 records also add:

  • An allocation-level amounts object (total, impact, impactTax, serviceFee, serviceFeeTax) that mirrors the quote response
  • unit per line item (e.g. kgCO2e, bottles) with a quantity
  • impactPartnerId per line item
  • A taxLiability field (platform or organisation) indicating sales tax responsibility. This is a new concept, not a rename of fundsFlow

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 endpointPurpose
GET /impact-partners/projectsList projects across all impact partners
GET /impact-partnersList 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_credits or contribution
  • theme: pollution, climate_stress, land_use or water_use
  • location: countryCode, region and state
  • sdgs: array of associated UN Sustainable Development Goals
  • subtype: for carbon credit projects (e.g. sequestration, carbon removal)
  • shortDescription and longDescription as separate fields
  • impactPartnerId linking 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.requestId or error.timestamp must be updated.
  • code is 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 example MAX_KEYS_REACHED instead of CONFLICT) 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:

  1. Log in to ekko Hub (hub.ekko.earth for production, staging.hub.ekko.earth for sandbox)
  2. Click Console in the left sidebar (under Developer)
  3. 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 matchConsumerLocation to true inside projectFilters (under credits or contribution) on quote and session requests to prioritise projects close to the consumer's location. The quote response includes consumerLocationMatchAccuracy indicating match precision
  • Donation tax receipts: set issueTaxReceipt: true with a consumerEmail on POST /funds/allocations to 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:

  1. Review the terminology. Read Key concepts so the v3 vocabulary is settled. The biggest shifts are compensationcredits and the introduction of organisation (replacing the brand/product model).
  2. Update authentication and endpoints. Send Authorization: Bearer instead of X-Api-Key, update the base URL, and apply the field, status code and error-handling changes from the sections above.
  3. 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).
  4. Cut over. Once sandbox parity is solid, swap the production base URL to https://api.ekko.earth/v3 and ship. v2 keeps running in parallel, so you can roll back without coordination if you spot an issue.

Did this page help you?