Skip to content

Endpoints

Endpoints

Name

Create short links in bulk

Up to 1,000 links in one request, with one result per link and a single charge for what was created.

HTTP method and path

POST/api/v6/urlshortener/links/batch

https://developers.hablame.co/api/v6/urlshortener/links/batch

What happens when you run it

  • Creates data in your organization.
  • The call is charged to your organization.
  • After that, every visit by a person to the link is also charged (visits by bots and link previews are not).

What it does

Creates up to 1,000 links in one request. Each link in the batch takes the same four fields as creating a link and goes through exactly the same checks: destination, alias, domain and expiry.

An invalid link does not sink the batch: the response is 200 with one result per link, in the order of the body (created, replayed or failed), and each failed one carries in error.code the same code that creating that link alone would return. What does belong to the whole batch is answered without creating anything: an invalid body or one with more than 1,000 links (400), the key used with another body (422), the balance or a spend limit (402), a missing price or currency (409) and the usage limits (429).

A single charge for what was created. The price of all the valid links is reserved from your balance at once, and only the links actually created are charged: if 998 out of 1,000 are created, 998 are charged. Without balance for all of them, 402 and none is created; send a smaller batch. With an active monthly URL shortener plan there is no charge, and the batch links count against the daily creation quota: a batch that does not fit whole is rejected whole with 429 URLSHORTENER_LINK_QUOTA_EXCEEDED, and the message says how many you have left today.

Availability and prerequisites

  • Published and available in the production API v6.
  • Every path starts at https://developers.hablame.co.
  • You need a Hablame API key to call it.

Create your API key

Authentication and permissions

  • Send your API key in the Authorization header with the Bearer scheme.

Who can call it: An API key with the urlshortener service enabled.

Limits and operational notes

Usage limit: 35 requests per second.

  • The 35 requests per second are the same as single-link creation: both routes spend a single quota.
  • Additionally, up to 2,100 links per minute per organization across all its batches. A batch that does not fit is rejected whole.
  • Up to 1,000 links per request; the body, up to 3 MiB; each destination, up to 2048 characters.
  • With a plan: the batch links count against the daily creation quota (100,000 per Colombia day).

Path parameters

This endpoint has no path parameters.

Query parameters

This endpoint has no query parameters.

HTTP headers

NameTypeDescription and rules
AuthorizationRequiredstring

Your API key with the Bearer scheme.

Example:Bearer hk_TU_API_KEY

Content-TypeRequiredstring

Type of the body you send.

Fixed value:application/json

Idempotency-KeyOptionalstring

Optional idempotency key for the batch (1 to 255 characters from A-Za-z0-9_-), unique per organization. If you repeat the batch with the same key and the same body, you get the links that were already created (replayed) without creating or charging them again, and whatever was not created the first time is tried again. With a different body, 422 IDEMPOTENCY_KEY_REUSED. It is independent from the single-link creation keys. See the idempotency guide.

Pattern:^[A-Za-z0-9_-]+$Minimum length:1Maximum length:255Example:campana-octubre-lote-01

Request body

Content type:application/jsonRequired

Only links is accepted, and in each link only its four fields. One that does not exist is rejected with 400 naming the field, and the batch is not processed.

NameTypeDescription and rules
linksRequiredarray<object>

The links to create, 1 to 1,000. The position of each one is its index in the response.

Minimum items:1Maximum items:1000

links[].longUrlRequiredstring

Destination URL, with the same rules as creating a link.

Format:uriExample:https://example.com/landing?utm_source=sms

links[].domainOptionalstring

Domain to publish it on. If omitted, your organization’s default.

Example:h0b.co

links[].codeOptionalstring

Optional alias, with the same rules as creating a link. Two links of the same batch with the same alias on the same domain: the first one keeps it and the other comes back failed with ALIAS_TAKEN.

Pattern:^[a-z0-9_-]{3,32}$Example:promo-octubre

links[].expiresAtOptionalstring

Optional expiry, always in the future. Without a time zone it is read in Colombia time (-05:00).

Format:date-timeExample:2026-12-31T23:59:59-05:00

Example
{
  "links": [
    { "longUrl": "https://example.com/landing?utm_source=sms&c=1" },
    { "longUrl": "https://example.com/landing?utm_source=sms&c=2", "code": "promo-octubre" },
    { "longUrl": "https://example.com/landing?utm_source=sms&c=3", "expiresAt": "2026-12-31T23:59:59-05:00" }
  ]
}

Successful responses

200

The batch was processed: the totals and one result per link, in the order of the body. With Idempotency-Key, the Idempotency-Status header says replayed if this request created nothing new and returned the earlier results, and created if it created something. When the key returns earlier links, whole-batch rejections (402, 409, 429, 5xx) are not answered as an error: they go in each pending link, with their code, and the replayed links arrive anyway.

Response example
{
  "success": true,
  "data": {
    "created": 2,
    "replayed": 0,
    "failed": 1,
    "items": [
      {
        "index": 0,
        "status": "created",
        "link": {
          "domain": "h0b.co",
          "code": "a1b2c3d",
          "shortUrl": "https://h0b.co/a1b2c3d",
          "longUrl": "https://example.com/landing?utm_source=sms&c=1",
          "isAlias": false,
          "status": "active",
          "domainStatus": "active",
          "expiresAt": null,
          "createdAt": "2026-09-30T15:04:05Z",
          "clicks": 0,
          "uniqueClicks": 0,
          "firstClickAt": null,
          "lastClickAt": null
        }
      },
      {
        "index": 1,
        "status": "failed",
        "error": { "code": "ALIAS_TAKEN", "message": "Ese código ya se usó en el dominio y no se puede volver a usar." }
      },
      {
        "index": 2,
        "status": "created",
        "link": {
          "domain": "h0b.co",
          "code": "k9x8w7v",
          "shortUrl": "https://h0b.co/k9x8w7v",
          "longUrl": "https://example.com/landing?utm_source=sms&c=3",
          "isAlias": false,
          "status": "active",
          "domainStatus": "active",
          "expiresAt": "2027-01-01T04:59:59Z",
          "createdAt": "2026-09-30T15:04:05Z",
          "clicks": 0,
          "uniqueClicks": 0,
          "firstClickAt": null,
          "lastClickAt": null
        }
      }
    ]
  },
  "meta": {
    "requestId": "8f0c0e2a4b1d4c8f",
    "clientIp": "203.0.113.45",
    "timestamp": "2026-09-23T18:30:00+00:00",
    "responseTimeMs": 7.2
  }
}

The example values are illustrative.

Response fields

NameTypeDescription and rules
successAlways presentboolean

dataAlways presentobject

What happened with the batch: the totals and one result per link, in the same order as the body.

data.createdAlways presentinteger

Links created (and charged) in this request.

data.replayedAlways presentinteger

Links this batch had already created with the same Idempotency-Key: returned without creating or charging them again.

data.failedAlways presentinteger

Links that were not created, each with its error.

data.itemsAlways presentarray<object>

One result per link in the body, in its order.

data.items[].indexAlways presentinteger

Position of the link in links, starting at 0.

data.items[].statusAlways presentstring

created: created and charged now. replayed: this batch had already created it with the same key. failed: not created; see error.

Allowed values:created replayed failed

data.items[].linkobject

The link, with created and replayed.

data.items[].link.domainAlways presentstring

Domain of the link, lowercase. If the domain no longer belongs to your organization (domainStatus: released), it is the name it had.

data.items[].link.codeAlways presentstring

Link code, lowercase: 7 0-9a-z characters when the service generates it, or the alias chosen at creation. Unique per domain.

data.items[].link.shortUrlAlways presentstring

The full short URL: always https://{domain}/{code}.

Format:uri

data.items[].link.longUrlAlways presentstring

Destination URL.

Format:uri

data.items[].link.isAliasAlways presentboolean

true when the code was chosen by whoever created the link (the code field on creation); false when the service generated it.

data.items[].link.statusAlways presentstring

active, disabled or blocked (and deleted, only in the case below). Expired is not a status: an active link whose expiresAt has passed no longer redirects, but keeps active. blocked is a link Hablame blocked for security: it does not redirect and cannot be edited, deleted or reactivated through the API; if you think it is a mistake, contact us. Deleted links do not appear in responses, except in the replay of a creation with Idempotency-Key whose link was deleted afterwards: it comes as it is, with deleted.

Allowed values:active disabled blocked deleted

data.items[].link.domainStatusstring

Whether the domain lets the link open, apart from its own status. active: it does. suspended: the domain is suspended; released: the domain no longer belongs to your organization; pending: it is an own domain that is not verified yet. With those three the link does not redirect (the visitor gets 404) even if its status is active. It comes in the list, when getting a link and when creating, editing or restoring it; if it does not come, treat the link as before the field existed. New values may appear: one you do not know does not mean the link does not open.

Allowed values:active pending suspended released

data.items[].link.expiresAtAlways presentstring

Expiry in UTC, or null if it never expires.

May come as null.

Format:date-time

data.items[].link.createdAtAlways presentstring

Creation date, in UTC.

Format:date-time

data.items[].link.clicksAlways presentinteger

Lifetime visits of the link. A background process consolidates them: a visit can take from seconds to a few minutes to show up. Bots and the previews generated by social networks and chat apps are not counted.

data.items[].link.uniqueClicksAlways presentinteger

Sum of each day’s unique visits: the first visit of the day from each browser. Not distinct people over the life of the link.

data.items[].link.firstClickAtAlways presentstring

First visit, in UTC. null if it never received one.

May come as null.

Format:date-time

data.items[].link.lastClickAtAlways presentstring

Last visit, in UTC. null if it never received one.

May come as null.

Format:date-time

data.items[].errorobject

Why it was not created, with failed.

data.items[].error.codeAlways presentstring

The error catalog code: the same one creating that link alone would return (URL_INVALID, ALIAS_TAKEN, DOMAIN_NOT_VERIFIED…).

data.items[].error.messageAlways presentstring

The detail, in Spanish.

metaAlways presentobject

Response metadata.

meta.requestIdAlways presentstring

Unique request identifier (16 hexadecimal characters). It matches the X-Request-Id header. Quote it when contacting support.

Example:b9b1704baffab211

meta.clientIpAlways presentstring

IP the API saw your request come from. Useful if your API key only accepts certain networks.

Example:203.0.113.45

meta.timestampAlways presentstring

Date and time the response was built (ISO 8601 with offset).

Format:date-timeExample:2026-06-04T19:43:59+00:00

meta.responseTimeMsAlways presentnumber

Time (ms) the server took to process the request.

Example:4.24

Error codes

Every error response has the same shape: success set to false and an error object whose code says what happened. They are grouped here by kind, with the codes this endpoint can return.

Shape of an error response
NameTypeDescription and rules
successAlways presentboolean

Fixed value:false

metaAlways presentobject

Response metadata.

meta.requestIdAlways presentstring

Unique request identifier (16 hexadecimal characters). It matches the X-Request-Id header. Quote it when contacting support.

Example:b9b1704baffab211

meta.clientIpAlways presentstring

IP the API saw your request come from. Useful if your API key only accepts certain networks.

Example:203.0.113.45

meta.timestampAlways presentstring

Date and time the response was built (ISO 8601 with offset).

Format:date-timeExample:2026-06-04T19:43:59+00:00

meta.responseTimeMsAlways presentnumber

Time (ms) the server took to process the request.

Example:4.24

errorAlways presentobject

error.codeAlways presentstring

Stable UPPER_SNAKE_CASE identifier. Clients branch on this value.

Example:AUTH_REQUIRED

error.legacyCodeinteger

Legacy v5 numeric code, only when a bridge exists. Omitted for codes born in v6.

Example:40002

error.typeAlways presentstring

URL to the error documentation.

Format:uriExample:https://developers.hablame.co/docs/v6/errors/auth-required

error.messageAlways presentstring

Human readable text, in English. It is for the developer log, not the end user: do not parse it.

error.detailsAlways presentarray<object>

Structured context (validation issues, affected resource). Empty array when there is no extra context.

Validation

  • 400The body cannot be read, has no links, has more than 1,000 (URLSHORTENER_BATCH_TOO_LARGE) or a field that does not exist, or the Idempotency-Key has another shape. Nothing was created.
  • 422The Idempotency-Key already created links with a different body. Use a new key for a new batch.
  • 400VALIDATION_INVALID_PARAMETER

    A parameter or a body field is not valid.

  • 400URLSHORTENER_BATCH_TOO_LARGE

    The batch has more links than one request accepts.

  • 400IDEMPOTENCY_KEY_INVALID

    The idempotency key does not match the format.

  • 422IDEMPOTENCY_KEY_REUSED

    The key already exists, but with a different body.

  • 400URL_INVALID

    The destination URL is malformed.

    Message the API returns: The destination URL is not valid. Provide an absolute `http`/`https` URL with a host.

  • 400URL_SCHEME_NOT_ALLOWED

    Only http/https destinations are allowed.

    Message the API returns: Only `http` and `https` destination URLs are allowed.

  • 400URL_TOO_LONG

    The destination URL exceeds the maximum length.

    Message the API returns: The destination URL exceeds the maximum allowed length.

  • 400URL_BLOCKED_HOST

    The destination host is private or reserved.

    Message the API returns: The destination host is not allowed (private, reserved, or loopback addresses are blocked).

  • 400ALIAS_INVALID

    The custom alias doesn't match the allowed format.

    Message the API returns: The alias must be 3-32 characters of lowercase letters, digits, hyphen or underscore.

  • 400EXPIRES_AT_INVALID

    expiresAt must be a future ISO date-time.

    Message the API returns: `expiresAt` must be a future ISO-8601 date-time.

  • 400DOMAIN_NOT_ALLOWED

    The chosen domain isn't available to your account.

    Message the API returns: The requested domain is not available for this account.

Authentication

  • 401Invalid or missing credentials.
  • 401AUTH_REQUIRED

    No Bearer token was sent, and every call requires one.

    Message the API returns: Authentication is required. Send your API key as `Authorization: Bearer <token>`.

  • 401AUTH_INVALID_KEY

    The API key is not recognized: wrong format, revoked or expired.

    Message the API returns: The provided API key is invalid or has expired. Please verify the value sent in the `Authorization: Bearer` header.

Permissions

  • 403The API key does not have the URL shortener enabled (or it is turned off for your organization), the key cannot be used from the request network, or the organization is not active.
  • 403AUTH_SERVICE_NOT_ALLOWED

    The key is not allowed to use the service behind this endpoint.

    Message the API returns: This API key is not authorized to use the requested service.

  • 403AUTH_IP_NOT_ALLOWED

    The key only works from certain networks and your IP is not one of them.

    Message the API returns: This API key cannot be used from this IP address. The address we saw is in `meta.clientIp`: call from one of the networks allowed for the key, or use a key that allows this one.

  • 403ACCOUNT_NOT_ACTIVE

    Your organization is suspended or closed.

    Message the API returns: The organization associated with this API key is not active. Please contact support.

  • 403ACCOUNT_CONFIG_NOT_FOUND

    We could not load the organization behind your key.

    Message the API returns: Organization configuration could not be loaded. Please contact support.

Usage limits

  • 429The request limit, the per-minute link limit of batches or (with a plan) the daily creation quota was exceeded. The whole batch is rejected without creating anything; Retry-After says how long to wait.
  • 429URLSHORTENER_LINK_QUOTA_EXCEEDED

    Your organization reached its plan’s daily link quota.

  • 429RATE_TPS_EXCEEDED

    You exceeded your organization quota for this endpoint.

    Message the API returns: You have exceeded the allowed request rate for this endpoint. Please wait before retrying.

Resource not found

This endpoint does not declare responses of this kind.

Internal errors and availability

  • 500Internal error. Retry with the same Idempotency-Key: whatever was already created comes back as replayed.
  • 503Balance could not be checked right now. Nothing was created; you can retry (better with Idempotency-Key).

Other responses

  • 402Your organization has no balance for all the valid links of the batch (or no active billing account), or creating them would exceed a spend limit it configured itself. None was created.
  • 409The URL shortener has no price for your organization’s currency, or your organization does not have a currency yet. Retrying does not fix it.

See it in the error catalog

Special considerations

  • Supports the Idempotency-Key header: if a request is cut off and you repeat it with the same key, it is not duplicated. The details are in the idempotency guide.

Retry without duplicates with Idempotency-Key. The key belongs to the batch: if a request is cut off and you repeat it with the same key and the same body, the links already created come back as replayed (the same ones, without creating others or charging twice, even while the first one is still being processed) and the failed ones are tried again. What was already created is never hidden: if what was missing runs into the balance, a limit or a failure on our side, the response is still 200 with the replayed links, and each pending link comes back failed with that rejection’s code. With the key and a different body, 422 IDEMPOTENCY_KEY_REUSED. Without a key, each retry creates and charges another batch.

The limit counts links, not requests. Besides the request quota, which it shares with single-link creation, your organization can create up to 2,100 links per minute in batches (35 per second, the same pace as single creation). Two batches of 1,000 fit back to back; a batch that does not fit is rejected whole with 429 RATE_TPS_EXCEEDED and Retry-After, without creating or charging anything. Single-link creation does not spend this limit.

A key with the URL shortener also creates and deletes

The urlshortener service grants the whole URL shortener: with it, the API key lists and reads links, and also creates them (each creation is charged), edits, deletes and restores them. There are no read-only keys. Only the owner of your organization creates keys: if one must not create links, do not give it this service. Without it, the key gets 403 AUTH_SERVICE_NOT_ALLOWED.

Related documentation

Need help?

Was this page helpful?

See all support channels

Ask for help about this: this endpoint

We build a message with the details that help us look into your case. Read it, complete it and send it yourself: nothing is sent on its own.

The message never carries your API key or passwords.