Rest V1 Create or Update End Point: "BILLING Region is invalid" Bug?

Hi @Timothy_Withers, @Todd_Stoker and @Jeffrey_Chimene

After reviewing the behavior, we determined that fixing this fully to enforce consistent country/region handling would unfortunately break existing integrations.

For now, the behavior will remain as-is in V2, but we’ve added clarifying notes in the documentation. The recommended approach for consistent behavior is to just use country_code and region_code . We also added deprecation notes to highlight this and to guide future migrations.

The proper behavior will be enforced in future API versions.

Thanks for your patience and for helping us document this clearly!

Omar

Hi @Timothy_Withers @Todd_Stoker @Pav @Jeffrey_chimene, quick update on this: CreateOrUpdate-style behavior is now supported in v2 via a new query parameter on the contacts endpoint. You can use POST /v2/contacts?duplicate_option=... to control how duplicates are handled, which effectively lets you perform create-or-update without needing to rely on v1 anymore. Full docs are here: Keap REST API. Thanks for your patience while we got this in place.

-Omar

@OmarAlmonte
I see this is marked Released. However, as I’m using the API, that feature is not yet available. I see this is similar to the issue of missing the file upload API support. It looks like this is symptomatic of the API missing from the known issue tracking list.

Hi @Jeffrey_Chimene, this is actually available now. Here’s a working curl example you can try:

curl --request POST \
  --url "https://api.infusionsoft.com/crm/rest/v2/contacts?duplicate_option=Email" \
  --header "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{
    "family_name": "John",
    "given_name": "Doe",
    "email_addresses": [
      {
        "email": "johndoe@yopmail.com",
        "field": "EMAIL1"
      }
    ]
  }'

For the duplicate_option parameter, the available values are:

  • Email
  • EmailAndName
  • EmailAndNameAndCompany

For reference:

  • If a duplicate is found, the API returns 200 (existing contact matched).
  • If no duplicate is found and a new contact is created, the API returns 201 .

Let me know if you’re seeing different behavior on your end, happy to dig into it with you.

@OmarAlmonte
I’m not doubting the raw POST works. I’m quite sure it performs as expected.
I’m not seeing support for that additional argument in the API call that uses that POST.

@Jeffrey_Chimene The additional argument isn’t exposed in the older SDK method, even though the underlying POST supports it. That’s why it wouldn’t appear in the API call you’re referring to.

Here’s an example using the PHP SDK (which I know you’ve been using) showing how it works in 2.0.4+:

<?php


require_once __DIR__ . '/run-remote/vendor/autoload.php';

$token = getenv('KEAP_ACCESS_TOKEN');
if (!$token) {
    die("ERROR: KEAP_ACCESS_TOKEN environment variable is not set.\n");
}

$config = Keap\Core\V2\Configuration::getDefaultConfiguration()
    ->setAccessToken($token);

$contactApi = new Keap\Core\V2\Api\ContactApi(
    new GuzzleHttp\Client(),
    $config
);

$emailAddress = new Keap\Core\V2\Model\EmailAddress([
    'email' => 'johndoe@yopmail.com',
    'field' => 'EMAIL1',
]);

$contactRequest = new Keap\Core\V2\Model\CreateUpdateContactRequest([
    'given_name'      => 'John',
    'family_name'     => 'Doe',
    'email_addresses' => [$emailAddress],
]);

echo "========================================\n";
echo "  CREATE CONTACT  (duplicate_option=Email)\n";
echo "========================================\n";
echo "Email : johndoe@yopmail.com\n";
echo "Name  : John Doe\n\n";

try {
    // Pass duplicate_option as the 3rd argument (added in SDK 2.0.4)
    $contact = $contactApi->createContact(
        create_update_contact_request: $contactRequest,
        duplicate_option: 'Email'
    );

    echo "SUCCESS:\n";
    echo json_encode(json_decode((string) $contact), JSON_PRETTY_PRINT) . "\n";

} catch (\Keap\Core\V2\ApiException $e) {
    $code    = $e->getCode();
    $body    = $e->getResponseBody();
    $decoded = json_decode($body, true);

    echo "ApiException caught:\n";
    echo "  HTTP status : $code\n";
    if (isset($decoded['message'])) {
        echo "  Message     : {$decoded['message']}\n";
    }
    if (isset($decoded['code'])) {
        echo "  Error code  : {$decoded['code']}\n";
    }
    echo "  Raw body    : $body\n";

} catch (\Exception $e) {
    echo "Unexpected exception: " . $e->getMessage() . "\n";
}

This support was introduced in SDK version 2.0.4, released last Thursday. If you’re on an earlier version, that would explain why the argument isn’t visible.

@OmarAlmonte

Meh. I missed the new argument in the recent version. Thanks!

@OmarAlmonte It looks like only Contacts created using country_code will get the country displayed in the UI?
When I create a Contact using country = “United States”, region = Arizona, the state displays in the UI, but not the country.
When I create a Contact using country_code = “USA”, region = Arizona, the state displays in the UI, but not the country.
Also, it looks like only the 3 digit ISO code is accepted not the 2 digit?

Hi @Jeffrey_Chimene,

Thanks for calling this out.

Just to clarify, the country and region fields are deprecated because they can cause inconsistent behavior. For POST/PATCH requests, you’ll want to use country_code and region_code instead. country_code should be the 3-letter ISO format (e.g., “USA”).

If you’re already using the 3-letter country_code and the country still isn’t showing in the UI, that would be a bug on our side. Can you confirm if that’s what you’re seeing? If so, we’ll dig into it.

Appreciate you flagging this.

@OmarAlmonte
It looks like “USA” works, but not “US”. I was under the impression that both abbreviations are acceptable. My question yesterday used “USA” instead of “US”

I’m not sure why we’d use country if it won’t show in the UI. My question is why region shows, but not country

As a side note, I just noticed that the Locale country name can be formatted as COUNTRYNAME (some text). If this isn’t set in stone, I’d recommend dropping the parenthetical text as it prevents a keyed lookup on the country name to obtain the abbreviation. As it is, I’ll filter the text before caching the result.

Thanks for the detailed notes. On the country code, only the 3-letter ISO format is supported, so “USA” works while “US” does not. Regarding the UI behavior where region shows but country doesn’t, that’s due to internal system handling of address fields, which can lead to that inconsistency with deprecated data. And on the Locale formatting suggestion, appreciate the recommendation. it’s helpful feedback.

  • I see the Locale formatting surfaces on the UI, so I assume it’s not going to change.

  • The issue is reconcilling the Country name in Keap’s 3 letter ISO list with the Country name in the 2 letter ISO table in a WordPress environment. It’s not possible to make the Country dropdown in the WordPress envinment match Keap’s version. It’s been technically solved e.g. mapping “Bermuda (the)” to “Bermuda”, but I have to ask, what is the source for these 3 letter ISO codes? They aren’t reflected in any publically available (Ok, Wikipedia) page or this WordPress environment.

  • Why send the 2 digit ISO code? It can’t be reflected in any Address.

I retract my assertion that Keap is the outlier on using ISO Country names. Amazon uses a similar syntax: COUNTRY, article